M0-M11 狩夜人笔记初始实现:11里程碑+86测试
This commit is contained in:
@@ -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);
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user