修掉导出面板卡在「正在读取导出信息…」

- `usePanel` 的注册表开关改成「进过小红书 tab」或「当前目标就是小红书」两个条件取或:从 tab 条切过来的路径原本既不开开关、也不发生一次文件访问,面板于是永远等不到第一次读取;
- 两条入口(点目标大卡、点 tab 条)合并到同一个 `enterTarget`,不再各写一份;
- `visited` 单独留着,是为了离开 tab 之后仍保有开关,表单自动保存的 600ms 窗口不被切 tab 掐掉;
- 新增 `tests/xhsMinitoolPanelTargets.test.tsx`,三条入口各钉一条。
This commit is contained in:
2026-10-06 14:31:40 +08:00
parent d80ce78fd8
commit 000e3172fb
2 changed files with 110 additions and 9 deletions
@@ -39,21 +39,36 @@ export function usePanel({
*/
const [workspaceEntered, setWorkspaceEntered] = useState(false);
/**
* 导出注册表的开关比订阅更窄:只有真的点进过小红书 tab 才打开。进陶泥儿不该在用户项目里
* 导出注册表的开关比订阅更窄:只有真的用过小红书 tab 才打开。进陶泥儿不该在用户项目里
* 补出一份 `.export/xhs-minitool.json`。
*
* 判据是**两个条件取或**,缺一不可:
* - `visited` 让「离开小红书 tab 之后」继续保有开关(表单自动保存的 600ms 窗口不该因为切
* tab 被掐掉);
* - `activeTarget` 让「从别的 tab 切过来」当场打开开关。只认 `visited` 是不够的——`visited`
* 只在点目标大卡(`openTarget`)时置真,而从 tab 条切过来走的是 `selectTarget`,于是面板
* 会永远停在「正在读取导出信息…」:一次文件访问都没发生。
*/
const [xiaohongshuEntered, setXiaohongshuEntered] = useState(false);
const [xiaohongshuVisited, setXiaohongshuVisited] = useState(false);
const xiaohongshuEntered =
xiaohongshuVisited || activeTarget === Target.Xiaohongshu;
const openTarget = useCallback((target: Target) => {
/** 切到某个目标时的共同动作:两条入口(点卡片、点 tab 条)都必须经过它。 */
const enterTarget = useCallback((target: Target) => {
setActiveTarget(target);
setStage(Stage.Workspace);
setWorkspaceEntered(true);
if (target === Target.Xiaohongshu) setXiaohongshuEntered(true);
if (target === Target.Xiaohongshu) setXiaohongshuVisited(true);
}, []);
const selectTarget = useCallback((target: Target) => {
setActiveTarget(target);
}, []);
const openTarget = useCallback(
(target: Target) => {
enterTarget(target);
setStage(Stage.Workspace);
setWorkspaceEntered(true);
},
[enterTarget],
);
const selectTarget = enterTarget;
const taonier: TaonierTabState = useTaonierTab({
enabled: workspaceEntered && Boolean(projectPath),
@@ -0,0 +1,86 @@
// @vitest-environment jsdom
/**
* 面板目标切换与「导出注册表开关」的接线。
*
* 回归的是一次真实故障:点目标大卡进来会打开注册表开关,从顶部 tab 条切过来却不会——那时
* `useXhsMinitoolExport` 一次文件访问都没发生,`form` 永远是 `null`,左半面板于是永久停在
* 「正在读取导出信息…」上,看起来像卡死。这个开关只该由「当前目标 / 曾经到过」决定,
* 不该由「从哪条入口进来」决定。
*/
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Target } from '../src/view/project-development/export/model';
const { xiaohongshuInputs } = vi.hoisted(() => ({
xiaohongshuInputs: [] as { enabled: boolean; minitoolEnabled: boolean }[],
}));
vi.mock(
'../src/view/project-development/export/state/useXiaohongshuTab',
() => ({
useXiaohongshuTab: (input: {
enabled: boolean;
minitoolEnabled: boolean;
}) => {
xiaohongshuInputs.push(input);
return {} as never;
},
}),
);
vi.mock('../src/view/project-development/export/state/useTaonierTab', () => ({
useTaonierTab: () => ({}),
}));
const { usePanel } = await import(
'../src/view/project-development/export/state/usePanel'
);
function renderPanel() {
return renderHook(() =>
usePanel({
projectPath: '/tmp/project',
ensureConversationReadAllowed: async () => true,
ensureConversationWriteAllowed: async () => true,
onClose: () => {},
}),
);
}
function lastMinitoolEnabled(): boolean | undefined {
return xiaohongshuInputs.at(-1)?.minitoolEnabled;
}
describe('导出面板的目标切换', () => {
it('从目标大卡直接进小红书:注册表开关打开', () => {
xiaohongshuInputs.length = 0;
const panel = renderPanel();
act(() => panel.result.current.openTarget(Target.Xiaohongshu));
expect(lastMinitoolEnabled()).toBe(true);
});
it('先进陶泥儿再从 tab 条切到小红书:注册表开关一样要打开', () => {
xiaohongshuInputs.length = 0;
const panel = renderPanel();
act(() => panel.result.current.openTarget(Target.Taonier));
// 进陶泥儿不该在用户项目里补出一份 `.export/xhs-minitool.json`。
expect(lastMinitoolEnabled()).toBe(false);
act(() => panel.result.current.selectTarget(Target.Xiaohongshu));
expect(lastMinitoolEnabled()).toBe(true);
});
it('到过小红书之后再切走:开关保持打开,表单自动保存的窗口不被掐掉', () => {
xiaohongshuInputs.length = 0;
const panel = renderPanel();
act(() => panel.result.current.openTarget(Target.Xiaohongshu));
act(() => panel.result.current.selectTarget(Target.Taonier));
expect(lastMinitoolEnabled()).toBe(true);
});
});