import { create } from 'zustand'; import { IPC } from '@hunter/shared'; import type { TreeNode, VaultMeta, DocMeta, ReadDocResult } from '@hunter/shared'; import { invoke } from './ipc-client'; import { useTabs } from './features/tabs/tab-store'; export type SaveState = 'saved' | 'saving' | 'error'; interface EditorState { vault: VaultMeta | null; tree: TreeNode[]; treeLoading: boolean; currentPath: string | null; title: string; body: string; meta: DocMeta; saveState: SaveState; docMtime: number; // FR-S6:已加载文档的 mtime,用于检测外部改动 tagFilter: string[]; // FR-R7:选中标签列表,FileTree 据此 AND 过滤 // actions loadVault: () => Promise; loadTree: () => Promise; openDoc: (path: string) => Promise; setBody: (body: string) => void; setMeta: (meta: DocMeta) => void; addTag: (tag: string) => void; removeTag: (tag: string) => void; toggleStar: () => void; addLink: (link: string) => void; removeLink: (link: string) => void; saveNow: () => Promise; reloadDoc: () => Promise; // FR-S6:重新加载——检测 mtime 变化,有未保存改动时返回 false 需确认 resetEditor: () => void; // FR-V6: 切库时重置编辑区 syncCurrentPath: (oldPath: string, newPath: string) => void; // FR-T7: 重命名/移动后同步当前文档路径 } // 简单的每文档保存队列(M1 单文档编辑) let saveTimer: ReturnType | null = null; export const useStore = create((set, get) => ({ vault: null, tree: [], treeLoading: false, currentPath: null, title: '', body: '', meta: {}, saveState: 'saved', tagFilter: [], docMtime: 0, loadVault: async () => { const vault = await invoke(IPC.vaultList, undefined as never); set({ vault }); }, loadTree: async () => { set({ treeLoading: true }); try { const tree = await invoke(IPC.treeList, undefined as never); set({ tree, treeLoading: false }); } catch (e) { set({ treeLoading: false }); console.error('loadTree failed', e); } }, openDoc: async (path: string) => { // 切换前冲刷当前文档——先同步编辑器内容到 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 }); // 同步到标签页(如已存在则激活,否则新建) useTabs.getState().openTab(path); }, setMeta: (meta: DocMeta) => { set({ meta }); get().saveNow(); }, addTag: (tag: string) => { const tags = [...(get().meta.tags ?? []), tag].filter((t, i, a) => a.indexOf(t) === i); set({ meta: { ...get().meta, tags } }); get().saveNow(); }, removeTag: (tag: string) => { const tags = (get().meta.tags ?? []).filter(t => t !== tag); set({ meta: { ...get().meta, tags } }); get().saveNow(); }, toggleStar: () => { set({ meta: { ...get().meta, star: !get().meta.star } }); get().saveNow(); }, addLink: (link: string) => { const links = [...(get().meta.links ?? []), link].filter((l, i, a) => a.indexOf(l) === i); set({ meta: { ...get().meta, links } }); get().saveNow(); }, removeLink: (link: string) => { const links = (get().meta.links ?? []).filter(l => l !== link); set({ meta: { ...get().meta, links } }); get().saveNow(); }, setBody: (body: string) => { 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 ?? 3) * 1000; } catch { return 3000; } })(); saveTimer = setTimeout(() => { get().saveNow(); }, interval); }, saveNow: async () => { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } const { currentPath, body, meta } = get(); if (!currentPath) return; try { await invoke(IPC.docWrite, { path: currentPath, meta, body }); set({ saveState: 'saved' }); } catch (e) { set({ saveState: 'error' }); console.error('save failed', e); } }, // FR-S6:重新加载文档——检测 mtime 变化,有未保存改动时返回 false 需确认 reloadDoc: async () => { const { currentPath, docMtime, saveState } = get(); if (!currentPath) return false; try { const doc: ReadDocResult = await invoke(IPC.docRead, { path: currentPath }); if (doc.mtime === docMtime) return true; // 未变 if (saveState !== 'saved') return false; // 有未保存改动,需确认 set({ body: doc.body, meta: doc.meta, docMtime: doc.mtime, saveState: 'saved' }); return true; } catch { return false; } }, resetEditor: () => { set({ currentPath: null, title: '', body: '', meta: {}, saveState: 'saved', docMtime: 0 }); }, syncCurrentPath: (oldPath, newPath) => { const { currentPath } = get(); if (!currentPath) return; if (currentPath === oldPath) { set({ currentPath: newPath }); return; } if (oldPath && currentPath.startsWith(oldPath + '/')) set({ currentPath: newPath + currentPath.slice(oldPath.length) }); }, })); // FR-S6:暴露 store 供 E2E 验证 if (typeof window !== 'undefined') (window as any).__STORE__ = useStore;