fix(outline): 点击跳转滚动修复——1.点一下到最下面: setTextSelection+focus 在 scrollTo 之后执行, ProseMirror 的 scrollIntoView 覆盖手动 smooth scroll; 修复: 先 setTextSelection(不触发滚动) 再 scrollTo(smooth), focus 延迟到 rAF 下一帧执行, 避免覆盖; 2.暗色遗留: outline-flash-anim 和 outline-item-active 用硬编码 #63b3ed/rgba(99,179,237), 暗色主题下异常; 修复: 改用 var(--accent)/var(--control-border) 主题变量, animation fill-mode:forwards 确保结束完全透明; 3.流畅度: 两条 smooth scroll 冲突导致卡顿, 修复后仅一条 scroll
This commit is contained in:
@@ -64,30 +64,33 @@ export function OutlinePanel() {
|
|||||||
return () => { scrollEl!.removeEventListener('scroll', onScroll); };
|
return () => { scrollEl!.removeEventListener('scroll', onScroll); };
|
||||||
}, [editor, headings]);
|
}, [editor, headings]);
|
||||||
|
|
||||||
// FR-O2:平滑滚动到标题——手动计算 scrollTop,确保标题显示在编辑区最上方
|
// FR-O2:滚动到标题——先滚后选,避免 ProseMirror focus scroll 覆盖手动滚动
|
||||||
const scrollTo = useCallback((h: HeadingItem) => {
|
const scrollTo = useCallback((h: HeadingItem) => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
const el = editor.view.nodeDOM(h.pos) as HTMLElement | null;
|
const el = editor.view.nodeDOM(h.pos) as HTMLElement | null;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
// 找到编辑器的可滚动父容器(与滚动联动逻辑一致)
|
// 找到编辑器的可滚动父容器
|
||||||
let scrollEl: HTMLElement | null = el.parentElement;
|
let scrollEl: HTMLElement | null = el.parentElement;
|
||||||
while (scrollEl && getComputedStyle(scrollEl).overflowY !== 'auto' && getComputedStyle(scrollEl).overflowY !== 'scroll') {
|
while (scrollEl && getComputedStyle(scrollEl).overflowY !== 'auto' && getComputedStyle(scrollEl).overflowY !== 'scroll') {
|
||||||
scrollEl = scrollEl.parentElement;
|
scrollEl = scrollEl.parentElement;
|
||||||
}
|
}
|
||||||
if (scrollEl) {
|
if (scrollEl) {
|
||||||
// 计算让标题位于容器最上方的滚动位置
|
// 先计算目标位置(此时 rect 还未被 focus scroll 干扰)
|
||||||
const elRect = el.getBoundingClientRect();
|
const elRect = el.getBoundingClientRect();
|
||||||
const containerRect = scrollEl.getBoundingClientRect();
|
const containerRect = scrollEl.getBoundingClientRect();
|
||||||
const targetTop = elRect.top - containerRect.top + scrollEl.scrollTop;
|
const targetTop = Math.max(0, elRect.top - containerRect.top + scrollEl.scrollTop);
|
||||||
scrollEl.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
|
// 先设置光标但不 focus(不触发 ProseMirror 滚动)
|
||||||
|
editor.commands.setTextSelection(h.pos + 1);
|
||||||
|
// 平滑滚动——必须在 focus 之前执行,否则 ProseMirror 的 scrollIntoView 会覆盖
|
||||||
|
scrollEl.scrollTo({ top: targetTop, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
// 闪烁高亮
|
// 闪烁高亮
|
||||||
el.classList.remove('outline-flash');
|
el.classList.remove('outline-flash');
|
||||||
void el.offsetWidth; // 触发重排以重启动画
|
void el.offsetWidth;
|
||||||
el.classList.add('outline-flash');
|
el.classList.add('outline-flash');
|
||||||
setTimeout(() => el.classList.remove('outline-flash'), 1600);
|
setTimeout(() => el.classList.remove('outline-flash'), 1500);
|
||||||
editor.commands.setTextSelection(h.pos + 1);
|
// focus 放到下一帧,让 smooth scroll 先启动
|
||||||
editor.commands.focus();
|
requestAnimationFrame(() => editor.commands.focus());
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
// FR-O4:未打开文档时显示占位
|
// FR-O4:未打开文档时显示占位
|
||||||
|
|||||||
@@ -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 { background: rgba(255, 213, 79, 0.35); border-radius: 2px; }
|
||||||
.tiptap-wrap .ProseMirror .find-match-active { background: rgba(255, 145, 0, 0.65); }
|
.tiptap-wrap .ProseMirror .find-match-active { background: rgba(255, 145, 0, 0.65); }
|
||||||
|
|
||||||
/* FR-O2:标题闪烁高亮 */
|
/* FR-O2:标题闪烁高亮——使用主题变量,确保动画结束完全透明 */
|
||||||
@keyframes outline-flash-anim {
|
@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; }
|
100% { background: transparent; }
|
||||||
}
|
}
|
||||||
.tiptap-wrap .ProseMirror h1.outline-flash,
|
.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 h4.outline-flash,
|
||||||
.tiptap-wrap .ProseMirror h5.outline-flash,
|
.tiptap-wrap .ProseMirror h5.outline-flash,
|
||||||
.tiptap-wrap .ProseMirror h6.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;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FR-O3:大纲当前项高亮 */
|
/* FR-O3:大纲当前项高亮——主题变量适配 */
|
||||||
.outline-item-active {
|
.outline-item-active {
|
||||||
background: rgba(99, 179, 237, 0.15) !important;
|
background: var(--control-border) !important;
|
||||||
border-left: 2px solid #63b3ed !important;
|
border-left: 2px solid var(--accent) !important;
|
||||||
}
|
}
|
||||||
.tiptap-wrap .ProseMirror blockquote {
|
.tiptap-wrap .ProseMirror blockquote {
|
||||||
border-left: 3px solid var(--control-border); padding-left: 12px; color: var(--text-secondary); margin: 0.6em 0;
|
border-left: 3px solid var(--control-border); padding-left: 12px; color: var(--text-secondary); margin: 0.6em 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user