From 3fe35ef8730ae2dc523b8c42842b25494a53415f Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Wed, 26 Aug 2026 22:34:16 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=85=A8=E5=B1=80=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E9=80=9F=E5=BA=A6=E4=BC=98=E5=8C=96=E2=80=94=E2=80=941.?= =?UTF-8?q?=E6=A0=B8=E5=BF=83:=20=E6=89=80=E6=9C=89=E7=BB=84=E4=BB=B6=20us?= =?UTF-8?q?eStore()=20=E6=94=B9=E4=B8=BA=E7=AA=84=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=20useStore(s=3D>s.field),=20=E4=B9=8B=E5=89=8D?= =?UTF-8?q?=E6=AF=8F=E6=AC=A1=E6=8C=89=E9=94=AE=20setBody=20=E6=94=B9?= =?UTF-8?q?=E5=8F=98=20store.body=20=E5=AF=BC=E8=87=B4=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E6=95=B4=E4=B8=AA=20store=20=E7=9A=84=2014=20=E4=B8=AA?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=85=A8=E9=83=A8=20re-render(TiptapEditor/E?= =?UTF-8?q?ditor/FileTree/TabBar/DocInfoPanel/Toolbar/App/SearchBox/TagFil?= =?UTF-8?q?ter/SettingsPanel/GraphView/LinkGraph/MoreActions/TitleBar);=20?= =?UTF-8?q?TiptapEditor=20=E4=B8=8D=E5=86=8D=E8=AE=A2=E9=98=85=20body(?= =?UTF-8?q?=E4=BB=85=20currentPath/setBody),=20body=20=E4=BB=8E=20getState?= =?UTF-8?q?()=20=E5=9C=A8=20effect=20=E5=86=85=E8=8E=B7=E5=8F=96;=202.Outl?= =?UTF-8?q?inePanel=20collect=20debounce=20300ms(=E4=B9=8B=E5=89=8D?= =?UTF-8?q?=E6=AF=8F=E6=AC=A1=E6=8C=89=E9=94=AE=E9=81=8D=E5=8E=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=A0=91);=203.OutlinePanel=20scroll=20handler=20rAF?= =?UTF-8?q?=20=E8=8A=82=E6=B5=81(=E4=B9=8B=E5=89=8D=E6=AF=8F=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E6=BB=9A=E5=8A=A8=E8=B0=83=20coordsAtPos);=204.?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20TabBar=20resetEditor=20=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=20+=20TiptapEditor=20content=20=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/renderer/App.tsx | 4 +- .../features/doc-info/DocInfoPanel.tsx | 10 +++- .../src/renderer/features/editor/Editor.tsx | 4 +- .../renderer/features/editor/MoreActions.tsx | 4 +- .../renderer/features/editor/TiptapEditor.tsx | 6 ++- .../renderer/features/file-tree/FileTree.tsx | 8 +++- .../src/renderer/features/graph/GraphView.tsx | 3 +- .../src/renderer/features/graph/LinkGraph.tsx | 2 +- .../features/outline/OutlinePanel.tsx | 46 +++++++++++-------- .../renderer/features/search/SearchBox.tsx | 3 +- .../renderer/features/search/TagFilter.tsx | 2 +- .../features/settings/SettingsPanel.tsx | 4 +- .../src/renderer/features/shell/TitleBar.tsx | 2 +- .../src/renderer/features/tabs/TabBar.tsx | 5 +- 14 files changed, 68 insertions(+), 35 deletions(-) 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);