From 9e3029774a3e231117ce5c9a06491167dedb483d Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 27 May 2026 22:30:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=9E=E7=8E=B0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8-=E9=A2=84=E8=A7=88=E6=BB=9A=E5=8A=A8=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 分屏模式下编辑器滚动时,预览面板不跟随滚动。 实现: - Editor handleScroll 计算滚动比例,派发 editor-scroll 自定义事件 - Preview 监听 editor-scroll 事件,按比例设置 preview-panel scrollTop - isSyncingRef 防止双向循环触发 原理: 编辑器 scrollTop / (scrollHeight - clientHeight) = 预览 scrollTop / (scrollHeight - clientHeight) --- src/renderer/components/Editor/Editor.tsx | 7 ++++++- src/renderer/components/Preview/Preview.tsx | 22 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 58bc829..2b01910 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -118,13 +118,18 @@ export function Editor() { }, 150) }, [getActiveTab, updateTabContent, setModified, updateLineNumbers, updateFileSizeDisplay]) - // 滚动同步 + // 滚动同步:通知 Preview 跟随滚动 const handleScroll = useCallback(() => { const textarea = textareaRef.current if (!textarea) return + // 行号跟随 if (lineNumbersRef.current) { lineNumbersRef.current.scrollTop = textarea.scrollTop } + // 计算滚动比例,派发给 Preview + const maxScroll = textarea.scrollHeight - textarea.clientHeight + const ratio = maxScroll > 0 ? textarea.scrollTop / maxScroll : 0 + window.dispatchEvent(new CustomEvent('editor-scroll', { detail: { ratio } })) }, []) // Tab 键支持 diff --git a/src/renderer/components/Preview/Preview.tsx b/src/renderer/components/Preview/Preview.tsx index 70ebd20..1d44e4a 100644 --- a/src/renderer/components/Preview/Preview.tsx +++ b/src/renderer/components/Preview/Preview.tsx @@ -6,12 +6,13 @@ export function Preview() { const [html, setHtml] = useState('') const previewRef = useRef(null) const requestIdRef = useRef(0) + const isSyncingRef = useRef(false) const activeTabId = useTabStore(s => s.activeTabId) const tabs = useTabStore(s => s.tabs) const activeTab = tabs.find(t => t.id === activeTabId) ?? null - // B-03 + M-07: 响应式渲染(无轮询,无竞态) + // 响应式渲染(无轮询,无竞态) useEffect(() => { if (!activeTab) { setHtml('') @@ -20,13 +21,30 @@ export function Preview() { const requestId = ++requestIdRef.current renderMarkdown(activeTab.content, activeTab.filePath).then(result => { - // M-07: 只有当 requestId 仍为最新时才更新 if (requestId === requestIdRef.current) { setHtml(result) } }) }, [activeTabId, activeTab?.content]) + // 滚动同步:监听编辑器滚动事件 + useEffect(() => { + const handleEditorScroll = (e: Event) => { + const ratio = (e as CustomEvent).detail?.ratio ?? 0 + const panel = previewRef.current?.parentElement + if (!panel || isSyncingRef.current) return + + isSyncingRef.current = true + const maxScroll = panel.scrollHeight - panel.clientHeight + panel.scrollTop = ratio * maxScroll + // 下一帧重置标志 + requestAnimationFrame(() => { isSyncingRef.current = false }) + } + + window.addEventListener('editor-scroll', handleEditorScroll) + return () => window.removeEventListener('editor-scroll', handleEditorScroll) + }, []) + // 拦截链接点击 const handleClick = useCallback((e: React.MouseEvent) => { const link = (e.target as HTMLElement).closest('a')