diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx
index 369393a..429c950 100644
--- a/apps/desktop/src/renderer/App.tsx
+++ b/apps/desktop/src/renderer/App.tsx
@@ -29,7 +29,9 @@ import { OutlinePanel } from './features/outline/OutlinePanel';
import { DocInfoPanel } from './features/doc-info/DocInfoPanel';
export function App() {
- const { loadVault, loadTree, openDoc } = useStore();
+ const loadVault = useStore(s => s.loadVault);
+ const loadTree = useStore(s => s.loadTree);
+ const openDoc = useStore(s => s.openDoc);
const { restore, tabs, activeId } = useTabs();
const [rightTab, setRightTab] = useState<'outline' | 'info'>('outline');
const [view, setView] = useState<'editor' | 'graph'>('editor');
diff --git a/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx b/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx
index 38796fc..57a4c93 100644
--- a/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx
+++ b/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx
@@ -5,7 +5,15 @@ import { IPC } from '@hunter/shared';
import type { TreeNode } from '@hunter/shared';
export function DocInfoPanel() {
- const { currentPath, title, meta, tree, toggleStar, addTag, removeTag, addLink, removeLink } = useStore();
+ const currentPath = useStore(s => s.currentPath);
+ const title = useStore(s => s.title);
+ const meta = useStore(s => s.meta);
+ const tree = useStore(s => s.tree);
+ const toggleStar = useStore(s => s.toggleStar);
+ const addTag = useStore(s => s.addTag);
+ const removeTag = useStore(s => s.removeTag);
+ const addLink = useStore(s => s.addLink);
+ const removeLink = useStore(s => s.removeLink);
const [tagInput, setTagInput] = useState('');
const [linkInput, setLinkInput] = useState('');
diff --git a/apps/desktop/src/renderer/features/editor/Editor.tsx b/apps/desktop/src/renderer/features/editor/Editor.tsx
index 42c35d7..ad9d01e 100644
--- a/apps/desktop/src/renderer/features/editor/Editor.tsx
+++ b/apps/desktop/src/renderer/features/editor/Editor.tsx
@@ -10,7 +10,9 @@ function saveLabel(s: SaveState): string {
}
export function Editor() {
- const { currentPath, title, saveState } = useStore();
+ const currentPath = useStore(s => s.currentPath);
+ const title = useStore(s => s.title);
+ const saveState = useStore(s => s.saveState);
if (!currentPath) {
return (
diff --git a/apps/desktop/src/renderer/features/editor/MoreActions.tsx b/apps/desktop/src/renderer/features/editor/MoreActions.tsx
index f4abae8..4245d4d 100644
--- a/apps/desktop/src/renderer/features/editor/MoreActions.tsx
+++ b/apps/desktop/src/renderer/features/editor/MoreActions.tsx
@@ -4,7 +4,9 @@ import { IPC } from '@hunter/shared';
import { useState } from 'react';
export function MoreActions() {
- const { currentPath, body, openDoc } = useStore();
+ const currentPath = useStore(s => s.currentPath);
+ const body = useStore(s => s.body);
+ const openDoc = useStore(s => s.openDoc);
const [open, setOpen] = useState(false);
if (!currentPath) return null;
// FR-X1:复制全文——成功/失败均提示
diff --git a/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx b/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx
index 73d89f0..7f60c13 100644
--- a/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx
+++ b/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx
@@ -40,7 +40,8 @@ const Img = Image.extend({
});
export function TiptapEditor() {
- const { currentPath, body, setBody } = useStore();
+ const currentPath = useStore(s => s.currentPath);
+ const setBody = useStore(s => s.setBody);
const skipUpdate = useRef(false);
const editorRef = useRef(null);
@@ -61,7 +62,7 @@ export function TiptapEditor() {
MathBlock,
InlineMath,
],
- content: body,
+ content: useStore.getState().body,
editorProps: {
// FR-E6:剪贴板粘贴图片 -> 入库 .attachments -> 插入相对路径
handlePaste: (_view, event) => {
@@ -104,6 +105,7 @@ export function TiptapEditor() {
return;
}
useEditorShared.getState().setEditor(editor);
+ const body = useStore.getState().body;
skipUpdate.current = true;
editor.commands.setContent(body || '', { emitUpdate: false });
skipUpdate.current = false;
diff --git a/apps/desktop/src/renderer/features/file-tree/FileTree.tsx b/apps/desktop/src/renderer/features/file-tree/FileTree.tsx
index ce5825b..0d3105f 100644
--- a/apps/desktop/src/renderer/features/file-tree/FileTree.tsx
+++ b/apps/desktop/src/renderer/features/file-tree/FileTree.tsx
@@ -7,7 +7,13 @@ import { useDialog } from '../dialog/dialog-store';
import { useTabs } from '../tabs/tab-store';
export function FileTree() {
- const { tree, loadTree, treeLoading, openDoc, currentPath, syncCurrentPath, tagFilter } = useStore();
+ const tree = useStore(s => s.tree);
+ const loadTree = useStore(s => s.loadTree);
+ const treeLoading = useStore(s => s.treeLoading);
+ const openDoc = useStore(s => s.openDoc);
+ const currentPath = useStore(s => s.currentPath);
+ const syncCurrentPath = useStore(s => s.syncCurrentPath);
+ const tagFilter = useStore(s => s.tagFilter);
const { showInput, showConfirm, showAlert } = useDialog();
const [collapsed, setCollapsed] = useState>(new Set());
const [renaming, setRenaming] = useState(null);
diff --git a/apps/desktop/src/renderer/features/graph/GraphView.tsx b/apps/desktop/src/renderer/features/graph/GraphView.tsx
index 9e78bd4..604d80e 100644
--- a/apps/desktop/src/renderer/features/graph/GraphView.tsx
+++ b/apps/desktop/src/renderer/features/graph/GraphView.tsx
@@ -53,7 +53,8 @@ const MARKMAP_OPTIONS = {
function MindmapTree() {
const svgRef = useRef(null);
const mmRef = useRef(null);
- const { tree, openDoc } = useStore();
+ const tree = useStore(s => s.tree);
+ const openDoc = useStore(s => s.openDoc);
useEffect(() => {
const svg = svgRef.current;
diff --git a/apps/desktop/src/renderer/features/graph/LinkGraph.tsx b/apps/desktop/src/renderer/features/graph/LinkGraph.tsx
index 548a0ef..6baf81b 100644
--- a/apps/desktop/src/renderer/features/graph/LinkGraph.tsx
+++ b/apps/desktop/src/renderer/features/graph/LinkGraph.tsx
@@ -13,7 +13,7 @@ const truncate = (s: string, n: number) => (s.length > n ? s.slice(0, n - 1) + '
export function LinkGraph({ onSwitchToEditor }: { onSwitchToEditor?: () => void }) {
const svgRef = useRef(null);
- const { openDoc } = useStore();
+ const openDoc = useStore(s => s.openDoc);
const [data, setData] = useState(null);
const [err, setErr] = useState(null);
const zoomRef = useRef<{ zoomBeh: any; svg: any; g: any } | null>(null);
diff --git a/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx b/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx
index 685892f..51b71eb 100644
--- a/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx
+++ b/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx
@@ -12,7 +12,7 @@ export function OutlinePanel() {
const [activeIdx, setActiveIdx] = useState(-1);
const listRef = useRef(null);
- // 收集标题——currentPath 变化时重新收集(setContent 用 emitUpdate:false 不触发 update 事件)
+ // 收集标题——debounce 300ms,避免每次按键都遍历文档树
useEffect(() => {
if (!editor) return;
const doCollect = () => {
@@ -24,11 +24,11 @@ export function OutlinePanel() {
});
setHeadings(items);
};
- // rAF 延迟到下一帧,确保 TiptapEditor 的 setContent 已完成
const raf = requestAnimationFrame(doCollect);
- // 编辑时实时跟随
- editor.on('update', doCollect);
- return () => { editor.off('update', doCollect); cancelAnimationFrame(raf); };
+ let timer: ReturnType | null = null;
+ const debounced = () => { if (timer) clearTimeout(timer); timer = setTimeout(doCollect, 300); };
+ editor.on('update', debounced);
+ return () => { editor.off('update', debounced); cancelAnimationFrame(raf); if (timer) clearTimeout(timer); };
}, [editor, currentPath]);
// FR-O3:编辑器滚动联动——计算当前可见标题,高亮大纲并自动滚动大纲
@@ -43,24 +43,30 @@ export function OutlinePanel() {
}
if (!scrollEl) return;
+ // rAF 节流——滚动事件高频触发,coordsAtPos 计算开销大
+ let ticking = false;
const onScroll = () => {
- const containerTop = scrollEl!.getBoundingClientRect().top;
- let current = 0; // 默认第一个标题
- for (let i = 0; i < headings.length; i++) {
- const coords = editor!.view.coordsAtPos(headings[i].pos + 1);
- if (coords.top - containerTop <= 100) current = i;
- else break;
- }
- if (current !== activeRef.current) {
- activeRef.current = current;
- setActiveIdx(current);
- // 大纲自身滚动到当前项
- const el = listRef.current?.querySelector(`[data-idx="${current}"]`) as HTMLElement | null;
- el?.scrollIntoView({ block: 'nearest' });
- }
+ if (ticking) return;
+ ticking = true;
+ requestAnimationFrame(() => {
+ const containerTop = scrollEl!.getBoundingClientRect().top;
+ let current = 0;
+ for (let i = 0; i < headings.length; i++) {
+ const coords = editor!.view.coordsAtPos(headings[i].pos + 1);
+ if (coords.top - containerTop <= 100) current = i;
+ else break;
+ }
+ if (current !== activeRef.current) {
+ activeRef.current = current;
+ setActiveIdx(current);
+ const el = listRef.current?.querySelector(`[data-idx="${current}"]`) as HTMLElement | null;
+ el?.scrollIntoView({ block: 'nearest' });
+ }
+ ticking = false;
+ });
};
scrollEl.addEventListener('scroll', onScroll, { passive: true });
- onScroll(); // 初始计算
+ onScroll();
return () => { scrollEl!.removeEventListener('scroll', onScroll); };
}, [editor, headings]);
diff --git a/apps/desktop/src/renderer/features/search/SearchBox.tsx b/apps/desktop/src/renderer/features/search/SearchBox.tsx
index f48e91a..138931b 100644
--- a/apps/desktop/src/renderer/features/search/SearchBox.tsx
+++ b/apps/desktop/src/renderer/features/search/SearchBox.tsx
@@ -5,7 +5,8 @@ import type { SearchHit } from '@hunter/shared';
import { useStore } from '../../store';
export function SearchBox() {
- const { openDoc, loadTree } = useStore();
+ const openDoc = useStore(s => s.openDoc);
+ const loadTree = useStore(s => s.loadTree);
const [q, setQ] = useState('');
const [hits, setHits] = useState([]);
const [searching, setSearching] = useState(false);
diff --git a/apps/desktop/src/renderer/features/search/TagFilter.tsx b/apps/desktop/src/renderer/features/search/TagFilter.tsx
index cb6590d..94ff338 100644
--- a/apps/desktop/src/renderer/features/search/TagFilter.tsx
+++ b/apps/desktop/src/renderer/features/search/TagFilter.tsx
@@ -4,7 +4,7 @@ import { IPC, type TagStat } from '@hunter/shared';
import { useStore } from '../../store';
export function TagFilter() {
- const { tree } = useStore();
+ const tree = useStore(s => s.tree);
const [tags, setTags] = useState([]);
const [selected, setSelected] = useState([]);
const [collapsed, setCollapsed] = useState(false);
diff --git a/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx b/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx
index a2a6a78..bb4cfdd 100644
--- a/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx
+++ b/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx
@@ -103,7 +103,9 @@ function dot(on: boolean): React.CSSProperties { return { position: 'absolute',
// FR-V5: 默认库路径设置页——查看当前库、浏览选择、立即切换
function PathPage() {
- const { vault, loadVault, loadTree } = useStore();
+ const vault = useStore(s => s.vault);
+ const loadVault = useStore(s => s.loadVault);
+ const loadTree = useStore(s => s.loadTree);
const [val, setVal] = useState(vault?.currentRoot ?? '');
const [msg, setMsg] = useState('');
const browse = async () => { const p = await invoke(IPC.vaultBrowse, undefined as never); if (p) setVal(p); };
diff --git a/apps/desktop/src/renderer/features/shell/TitleBar.tsx b/apps/desktop/src/renderer/features/shell/TitleBar.tsx
index 5ea378f..28bae1e 100644
--- a/apps/desktop/src/renderer/features/shell/TitleBar.tsx
+++ b/apps/desktop/src/renderer/features/shell/TitleBar.tsx
@@ -7,7 +7,7 @@ const drag = { WebkitAppRegion: 'drag', flex: 1, height: '100%' } as React.CSSPr
const noDrag = { WebkitAppRegion: 'no-drag' } as React.CSSProperties;
export function TitleBar({ onToggleView, view, onSettings, onAppearance, onToggleSidebar, onToggleMinimal, onToggleTabbar, minimal, tabbarVisible }: { onToggleView: () => void; view: 'editor' | 'graph'; onSettings: () => void; onAppearance: () => void; onToggleSidebar: () => void; onToggleMinimal: () => void; onToggleTabbar: () => void; minimal: boolean; tabbarVisible: boolean }) {
- const { loadTree } = useStore();
+ const loadTree = useStore(s => s.loadTree);
const [maximized, setMaximized] = useState(false);
useEffect(() => { invoke(IPC.winIsMaximized, undefined as never).then(setMaximized).catch(() => {}); }, []);
return (
diff --git a/apps/desktop/src/renderer/features/tabs/TabBar.tsx b/apps/desktop/src/renderer/features/tabs/TabBar.tsx
index 570cbe5..1e9fec6 100644
--- a/apps/desktop/src/renderer/features/tabs/TabBar.tsx
+++ b/apps/desktop/src/renderer/features/tabs/TabBar.tsx
@@ -4,11 +4,12 @@ import { useEffect, useState } from 'react';
export function TabBar() {
const { tabs, activeId, setActive, closeTab, togglePin } = useTabs();
- const { openDoc, currentPath } = useStore();
+ const openDoc = useStore(s => s.openDoc);
+ const currentPath = useStore(s => s.currentPath);
+ const resetEditor = useStore(s => s.resetEditor);
const [hoveredTab, setHoveredTab] = useState(null);
// 当 activeId 变化,加载对应标签的文档;标签全部关闭时清空编辑器
- const resetEditor = useStore(s => s.resetEditor);
useEffect(() => {
const tab = tabs.find(t => t.id === activeId);
if (tab && tab.path !== currentPath) openDoc(tab.path);