M0-M11 狩夜人笔记初始实现:11里程碑+86测试

This commit is contained in:
hunter-dev
2026-08-24 20:35:22 +08:00
commit 3ebd33536b
106 changed files with 10490 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
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;
// actions
loadVault: () => Promise<void>;
loadTree: () => Promise<void>;
openDoc: (path: string) => Promise<void>;
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<void>;
}
// 简单的每文档保存队列(M1 单文档编辑)
let saveTimer: ReturnType<typeof setTimeout> | null = null;
export const useStore = create<EditorState>((set, get) => ({
vault: null,
tree: [],
treeLoading: false,
currentPath: null,
title: '',
body: '',
meta: {},
saveState: 'saved',
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) => {
// 切换前冲刷当前文档
await 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' });
// 同步到标签页(如已存在则激活,否则新建)
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' });
// 防抖自动保存(M1:固定 600ms,M9 接设置间隔)
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => { get().saveNow(); }, 600);
},
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);
}
},
}));