fix: 实现编辑器-预览滚动同步
问题: 分屏模式下编辑器滚动时,预览面板不跟随滚动。 实现: - Editor handleScroll 计算滚动比例,派发 editor-scroll 自定义事件 - Preview 监听 editor-scroll 事件,按比例设置 preview-panel scrollTop - isSyncingRef 防止双向循环触发 原理: 编辑器 scrollTop / (scrollHeight - clientHeight) = 预览 scrollTop / (scrollHeight - clientHeight)
This commit is contained in:
@@ -118,13 +118,18 @@ export function Editor() {
|
|||||||
}, 150)
|
}, 150)
|
||||||
}, [getActiveTab, updateTabContent, setModified, updateLineNumbers, updateFileSizeDisplay])
|
}, [getActiveTab, updateTabContent, setModified, updateLineNumbers, updateFileSizeDisplay])
|
||||||
|
|
||||||
// 滚动同步
|
// 滚动同步:通知 Preview 跟随滚动
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current
|
||||||
if (!textarea) return
|
if (!textarea) return
|
||||||
|
// 行号跟随
|
||||||
if (lineNumbersRef.current) {
|
if (lineNumbersRef.current) {
|
||||||
lineNumbersRef.current.scrollTop = textarea.scrollTop
|
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 键支持
|
// Tab 键支持
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ export function Preview() {
|
|||||||
const [html, setHtml] = useState('')
|
const [html, setHtml] = useState('')
|
||||||
const previewRef = useRef<HTMLDivElement>(null)
|
const previewRef = useRef<HTMLDivElement>(null)
|
||||||
const requestIdRef = useRef(0)
|
const requestIdRef = useRef(0)
|
||||||
|
const isSyncingRef = useRef(false)
|
||||||
|
|
||||||
const activeTabId = useTabStore(s => s.activeTabId)
|
const activeTabId = useTabStore(s => s.activeTabId)
|
||||||
const tabs = useTabStore(s => s.tabs)
|
const tabs = useTabStore(s => s.tabs)
|
||||||
const activeTab = tabs.find(t => t.id === activeTabId) ?? null
|
const activeTab = tabs.find(t => t.id === activeTabId) ?? null
|
||||||
|
|
||||||
// B-03 + M-07: 响应式渲染(无轮询,无竞态)
|
// 响应式渲染(无轮询,无竞态)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeTab) {
|
if (!activeTab) {
|
||||||
setHtml('')
|
setHtml('')
|
||||||
@@ -20,13 +21,30 @@ export function Preview() {
|
|||||||
|
|
||||||
const requestId = ++requestIdRef.current
|
const requestId = ++requestIdRef.current
|
||||||
renderMarkdown(activeTab.content, activeTab.filePath).then(result => {
|
renderMarkdown(activeTab.content, activeTab.filePath).then(result => {
|
||||||
// M-07: 只有当 requestId 仍为最新时才更新
|
|
||||||
if (requestId === requestIdRef.current) {
|
if (requestId === requestIdRef.current) {
|
||||||
setHtml(result)
|
setHtml(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [activeTabId, activeTab?.content])
|
}, [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 handleClick = useCallback((e: React.MouseEvent) => {
|
||||||
const link = (e.target as HTMLElement).closest('a')
|
const link = (e.target as HTMLElement).closest('a')
|
||||||
|
|||||||
Reference in New Issue
Block a user