fix: 实现编辑器-预览滚动同步

问题: 分屏模式下编辑器滚动时,预览面板不跟随滚动。

实现:
- Editor handleScroll 计算滚动比例,派发 editor-scroll 自定义事件
- Preview 监听 editor-scroll 事件,按比例设置 preview-panel scrollTop
- isSyncingRef 防止双向循环触发

原理: 编辑器 scrollTop / (scrollHeight - clientHeight) = 预览 scrollTop / (scrollHeight - clientHeight)
This commit is contained in:
thzxx
2026-05-27 22:30:31 +08:00
parent ba76cdab54
commit 9e3029774a
2 changed files with 26 additions and 3 deletions
+6 -1
View File
@@ -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 键支持
+20 -2
View File
@@ -6,12 +6,13 @@ export function Preview() {
const [html, setHtml] = useState('')
const previewRef = useRef<HTMLDivElement>(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')