diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx index 429c950..b6eae5c 100644 --- a/apps/desktop/src/renderer/App.tsx +++ b/apps/desktop/src/renderer/App.tsx @@ -49,7 +49,7 @@ export function App() { }).catch(() => {}); } }, [activeId]); // FR-S4:关窗前冲刷未保存内容 useEffect(() => { - const onBeforeUnload = () => { try { useStore.getState().saveNow(); } catch {} }; + const onBeforeUnload = () => { try { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); } catch {} }; window.addEventListener('beforeunload', onBeforeUnload); return () => window.removeEventListener('beforeunload', onBeforeUnload); }, []); @@ -62,7 +62,7 @@ export function App() { await useStore.getState().loadTree(); await useStore.getState().openDoc(path); }, - onSave: () => useStore.getState().saveNow(), + onSave: () => { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); }, onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); }, onToggleOutline: () => layout.toggleOutline(), onCheat: () => setShowCheat(true), diff --git a/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx b/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx index 7f60c13..bac14a4 100644 --- a/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx +++ b/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx @@ -44,6 +44,7 @@ export function TiptapEditor() { const setBody = useStore(s => s.setBody); const skipUpdate = useRef(false); const editorRef = useRef(null); + const mdTimer = useRef | null>(null); // editor 实例稳定,不随文档切换重建(避免销毁/重建过渡期 commands 为 null) const editor = useEditor({ @@ -92,7 +93,14 @@ export function TiptapEditor() { return false; }, }, - onUpdate: ({ editor }) => { if (!skipUpdate.current) setBody((editor.storage as Record).markdown.getMarkdown()); }, + onUpdate: ({ editor }) => { + if (skipUpdate.current) return; + // 大文档防抖——getMarkdown() 对 2000+ 行文档开销显著,不必每次按键都序列化 + if (mdTimer.current) clearTimeout(mdTimer.current); + mdTimer.current = setTimeout(() => { + setBody((editor.storage as Record).markdown.getMarkdown()); + }, 300); + }, }); // 切换文档时只同步内容,不重建 editor @@ -111,6 +119,17 @@ export function TiptapEditor() { skipUpdate.current = false; }, [currentPath, editor]); + // 监听保存前冲刷——Ctrl+S / 关窗前同步编辑器内容到 store + useEffect(() => { + if (!editor) return; + const onFlush = () => { + if (mdTimer.current) { clearTimeout(mdTimer.current); mdTimer.current = null; } + setBody((editor.storage as Record).markdown.getMarkdown()); + }; + window.addEventListener('dsh-flush-body', onFlush); + return () => window.removeEventListener('dsh-flush-body', onFlush); + }, [editor, setBody]); + if (!currentPath) { return (
diff --git a/apps/desktop/src/renderer/store.ts b/apps/desktop/src/renderer/store.ts index df0a18f..f51b7ba 100644 --- a/apps/desktop/src/renderer/store.ts +++ b/apps/desktop/src/renderer/store.ts @@ -66,8 +66,9 @@ export const useStore = create((set, get) => ({ }, openDoc: async (path: string) => { - // 切换前冲刷当前文档 - await get().saveNow(); + // 切换前冲刷当前文档——先同步编辑器内容到 store 再保存 + window.dispatchEvent(new CustomEvent('dsh-flush-body')); + void get().saveNow(); const doc: ReadDocResult = await invoke(IPC.docRead, { path }); set({ currentPath: path, body: doc.body, meta: doc.meta, title: doc.meta.title ?? path.split('/').pop()!.replace(/\.md$/, ''), saveState: 'saved', docMtime: doc.mtime }); // 同步到标签页(如已存在则激活,否则新建) @@ -103,7 +104,7 @@ export const useStore = create((set, get) => ({ set({ body, saveState: 'saving' }); // FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒) if (saveTimer) clearTimeout(saveTimer); - const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 1) * 1000; } catch { return 1000; } })(); + const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 3) * 1000; } catch { return 3000; } })(); saveTimer = setTimeout(() => { get().saveNow(); }, interval); }, diff --git a/apps/desktop/tests/e2e/save-mechanism.spec.ts b/apps/desktop/tests/e2e/save-mechanism.spec.ts index b913367..01de35f 100644 --- a/apps/desktop/tests/e2e/save-mechanism.spec.ts +++ b/apps/desktop/tests/e2e/save-mechanism.spec.ts @@ -62,7 +62,8 @@ test('FR-S4:关窗冲刷未保存内容', async () => { await a.page.keyboard.type('before-close-flush'); // 等一下让内容进入 store await a.page.waitForTimeout(200); - // 手动触发 saveNow(模拟 beforeunload 冲刷) + // 冲刷编辑器内容到 store 再保存(模拟 beforeunload 冲刷) + await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body'))); await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.()); await a.page.waitForTimeout(500); await a.app.close(); @@ -86,7 +87,10 @@ test('FR-S6:reloadDoc 检测外部改动', async () => { await a.page.locator('.ProseMirror').click(); await a.page.keyboard.type('original'); - await a.page.waitForTimeout(2000); // 等待自动保存 + await a.page.waitForTimeout(200); + await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body'))); + await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.()); + await a.page.waitForTimeout(500); // 等待保存完成 // 外部修改文件 const filePath = nodePath.join(vault, '重载测试.md');