From ed6eddc355a0d3bfbd49ffe8ccb4a29807341437 Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Wed, 26 Aug 2026 21:44:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(outline):=20=E7=82=B9=E5=87=BB=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E6=BB=9A=E5=8A=A8=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94?= =?UTF-8?q?1.=E7=82=B9=E4=B8=80=E4=B8=8B=E5=88=B0=E6=9C=80=E4=B8=8B?= =?UTF-8?q?=E9=9D=A2:=20setTextSelection+focus=20=E5=9C=A8=20scrollTo=20?= =?UTF-8?q?=E4=B9=8B=E5=90=8E=E6=89=A7=E8=A1=8C,=20ProseMirror=20=E7=9A=84?= =?UTF-8?q?=20scrollIntoView=20=E8=A6=86=E7=9B=96=E6=89=8B=E5=8A=A8=20smoo?= =?UTF-8?q?th=20scroll;=20=E4=BF=AE=E5=A4=8D:=20=E5=85=88=20setTextSelecti?= =?UTF-8?q?on(=E4=B8=8D=E8=A7=A6=E5=8F=91=E6=BB=9A=E5=8A=A8)=20=E5=86=8D?= =?UTF-8?q?=20scrollTo(smooth),=20focus=20=E5=BB=B6=E8=BF=9F=E5=88=B0=20rA?= =?UTF-8?q?F=20=E4=B8=8B=E4=B8=80=E5=B8=A7=E6=89=A7=E8=A1=8C,=20=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E8=A6=86=E7=9B=96;=202.=E6=9A=97=E8=89=B2=E9=81=97?= =?UTF-8?q?=E7=95=99:=20outline-flash-anim=20=E5=92=8C=20outline-item-acti?= =?UTF-8?q?ve=20=E7=94=A8=E7=A1=AC=E7=BC=96=E7=A0=81=20#63b3ed/rgba(99,179?= =?UTF-8?q?,237),=20=E6=9A=97=E8=89=B2=E4=B8=BB=E9=A2=98=E4=B8=8B=E5=BC=82?= =?UTF-8?q?=E5=B8=B8;=20=E4=BF=AE=E5=A4=8D:=20=E6=94=B9=E7=94=A8=20var(--a?= =?UTF-8?q?ccent)/var(--control-border)=20=E4=B8=BB=E9=A2=98=E5=8F=98?= =?UTF-8?q?=E9=87=8F,=20animation=20fill-mode:forwards=20=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E7=BB=93=E6=9D=9F=E5=AE=8C=E5=85=A8=E9=80=8F=E6=98=8E?= =?UTF-8?q?;=203.=E6=B5=81=E7=95=85=E5=BA=A6:=20=E4=B8=A4=E6=9D=A1=20smoot?= =?UTF-8?q?h=20scroll=20=E5=86=B2=E7=AA=81=E5=AF=BC=E8=87=B4=E5=8D=A1?= =?UTF-8?q?=E9=A1=BF,=20=E4=BF=AE=E5=A4=8D=E5=90=8E=E4=BB=85=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=20scroll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/outline/OutlinePanel.tsx | 21 +++++++++++-------- apps/desktop/src/renderer/styles.css | 12 +++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx b/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx index efc23ff..66e29be 100644 --- a/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx +++ b/apps/desktop/src/renderer/features/outline/OutlinePanel.tsx @@ -64,30 +64,33 @@ export function OutlinePanel() { return () => { scrollEl!.removeEventListener('scroll', onScroll); }; }, [editor, headings]); - // FR-O2:平滑滚动到标题——手动计算 scrollTop,确保标题显示在编辑区最上方 + // FR-O2:滚动到标题——先滚后选,避免 ProseMirror focus scroll 覆盖手动滚动 const scrollTo = useCallback((h: HeadingItem) => { if (!editor) return; const el = editor.view.nodeDOM(h.pos) as HTMLElement | null; if (!el) return; - // 找到编辑器的可滚动父容器(与滚动联动逻辑一致) + // 找到编辑器的可滚动父容器 let scrollEl: HTMLElement | null = el.parentElement; while (scrollEl && getComputedStyle(scrollEl).overflowY !== 'auto' && getComputedStyle(scrollEl).overflowY !== 'scroll') { scrollEl = scrollEl.parentElement; } if (scrollEl) { - // 计算让标题位于容器最上方的滚动位置 + // 先计算目标位置(此时 rect 还未被 focus scroll 干扰) const elRect = el.getBoundingClientRect(); const containerRect = scrollEl.getBoundingClientRect(); - const targetTop = elRect.top - containerRect.top + scrollEl.scrollTop; - scrollEl.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' }); + 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' }); } // 闪烁高亮 el.classList.remove('outline-flash'); - void el.offsetWidth; // 触发重排以重启动画 + void el.offsetWidth; el.classList.add('outline-flash'); - setTimeout(() => el.classList.remove('outline-flash'), 1600); - editor.commands.setTextSelection(h.pos + 1); - editor.commands.focus(); + setTimeout(() => el.classList.remove('outline-flash'), 1500); + // focus 放到下一帧,让 smooth scroll 先启动 + requestAnimationFrame(() => editor.commands.focus()); }, [editor]); // FR-O4:未打开文档时显示占位 diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index b8b1710..4aee2ee 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -59,9 +59,9 @@ html, body, #root { margin: 0; height: 100%; } .tiptap-wrap .ProseMirror .find-match { background: rgba(255, 213, 79, 0.35); border-radius: 2px; } .tiptap-wrap .ProseMirror .find-match-active { background: rgba(255, 145, 0, 0.65); } -/* FR-O2:标题闪烁高亮 */ +/* FR-O2:标题闪烁高亮——使用主题变量,确保动画结束完全透明 */ @keyframes outline-flash-anim { - 0% { background: rgba(99, 179, 237, 0.4); } + 0% { background: var(--flash-bg, rgba(127,127,127,0.3)); } 100% { background: transparent; } } .tiptap-wrap .ProseMirror h1.outline-flash, @@ -70,14 +70,14 @@ html, body, #root { margin: 0; height: 100%; } .tiptap-wrap .ProseMirror h4.outline-flash, .tiptap-wrap .ProseMirror h5.outline-flash, .tiptap-wrap .ProseMirror h6.outline-flash { - animation: outline-flash-anim 1.5s ease-out; + animation: outline-flash-anim 1.2s ease-out forwards; border-radius: 4px; } -/* FR-O3:大纲当前项高亮 */ +/* FR-O3:大纲当前项高亮——主题变量适配 */ .outline-item-active { - background: rgba(99, 179, 237, 0.15) !important; - border-left: 2px solid #63b3ed !important; + background: var(--control-border) !important; + border-left: 2px solid var(--accent) !important; } .tiptap-wrap .ProseMirror blockquote { border-left: 3px solid var(--control-border); padding-left: 12px; color: var(--text-secondary); margin: 0.6em 0;