From a18982659bf1cf8ee735762573cbad39c767e188 Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Wed, 26 Aug 2026 21:51:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(outline):=20Typora=20=E5=BC=8F=E7=9E=AC?= =?UTF-8?q?=E6=97=B6=E8=B7=B3=E8=BD=AC=E2=80=94=E2=80=94smooth=20scroll=20?= =?UTF-8?q?=E5=9C=A8=E5=AF=BC=E8=88=AA=E5=9C=BA=E6=99=AF=E6=98=AF=E5=8F=8D?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F(=E6=85=A2+=E4=B8=8E=20ProseMirror=20scrollIn?= =?UTF-8?q?toView=20=E5=86=B2=E7=AA=81).=20=E5=8F=82=E8=80=83=20Typora=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1:=201.=E7=9E=AC=E6=97=B6=E8=AE=BE=E7=BD=AE=20?= =?UTF-8?q?scrollEl.scrollTop=3DtargetTop,=20=E4=B8=8D=E7=94=A8=20behavior?= =?UTF-8?q?:smooth;=202.setTextSelection=20=E8=AE=BE=E5=85=89=E6=A0=87(?= =?UTF-8?q?=E4=B8=8D=E8=A7=A6=E5=8F=91=E6=BB=9A=E5=8A=A8)=20+=20view.focus?= =?UTF-8?q?()=20=E7=9B=B4=E6=8E=A5=E8=81=9A=E7=84=A6(=E8=B7=B3=E8=BF=87=20?= =?UTF-8?q?editor.commands.focus=20=E7=9A=84=20scrollIntoView);=203.?= =?UTF-8?q?=E9=97=AA=E7=83=81=E9=AB=98=E4=BA=AE=E6=94=BE=E6=9C=80=E5=90=8E?= =?UTF-8?q?.=20=E4=B8=80=E5=87=BB=E5=8D=B3=E8=BE=BE,=20=E6=97=A0=E9=9C=80?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E7=82=B9=E5=87=BB,=20=E6=97=A0=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/outline/OutlinePanel.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx b/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx index 66e29be..183976c 100644 --- a/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx +++ b/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx @@ -64,7 +64,8 @@ export function OutlinePanel() { return () => { scrollEl!.removeEventListener('scroll', onScroll); }; }, [editor, headings]); - // FR-O2:滚动到标题——先滚后选,避免 ProseMirror focus scroll 覆盖手动滚动 + // FR-O2:Typora 式大纲跳转——瞬时定位 + 光标设置 + 闪烁高亮 + // 不用 smooth scroll(导航场景应瞬时跳转),不用 editor.commands.focus(会触发 scrollIntoView) const scrollTo = useCallback((h: HeadingItem) => { if (!editor) return; const el = editor.view.nodeDOM(h.pos) as HTMLElement | null; @@ -75,22 +76,22 @@ export function OutlinePanel() { scrollEl = scrollEl.parentElement; } if (scrollEl) { - // 先计算目标位置(此时 rect 还未被 focus scroll 干扰) + // 计算目标位置——标题紧贴容器顶部 const elRect = el.getBoundingClientRect(); const containerRect = scrollEl.getBoundingClientRect(); const targetTop = Math.max(0, elRect.top - containerRect.top + scrollEl.scrollTop); - // 先设置光标但不 focus(不触发 ProseMirror 滚动) - editor.commands.setTextSelection(h.pos + 1); - // 平滑滚动——必须在 focus 之前执行,否则 ProseMirror 的 scrollIntoView 会覆盖 - scrollEl.scrollTo({ top: targetTop, behavior: 'smooth' }); + // 瞬时跳转(不用 smooth scroll——导航操作应该立即到位) + scrollEl.scrollTop = targetTop; } - // 闪烁高亮 + // 设置光标到标题位置——setTextSelection 不触发滚动 + editor.commands.setTextSelection(h.pos + 1); + // focus 但不触发 ProseMirror scrollIntoView——直接调 view.focus() + editor.view.focus(); + // 闪烁高亮(放在最后,确保滚动已完成) el.classList.remove('outline-flash'); void el.offsetWidth; el.classList.add('outline-flash'); setTimeout(() => el.classList.remove('outline-flash'), 1500); - // focus 放到下一帧,让 smooth scroll 先启动 - requestAnimationFrame(() => editor.commands.focus()); }, [editor]); // FR-O4:未打开文档时显示占位