diff --git a/app/src/layout/dock/Outline.ts b/app/src/layout/dock/Outline.ts index 6e627f8f0ae..c6c1910873e 100644 --- a/app/src/layout/dock/Outline.ts +++ b/app/src/layout/dock/Outline.ts @@ -31,6 +31,9 @@ export class Outline extends Model { public blockId: string; public isPreview: boolean; private preFilterExpandIds: string[] | null = null; + private scrollAnimationId: number | null = null; + private scrollLastFrameTime: number = 0; + private scrollCurrentFPS: number = 60; constructor(options: { app: App, @@ -380,11 +383,68 @@ export class Outline extends Model { }); return; } + // 检查是否在滚动边界区域 if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) { - this.element.scroll({ - top: this.element.scrollTop + (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP), - behavior: "smooth" - }); + // 如果还没有开始滚动,则开始持续滚动 + if (!this.scrollAnimationId) { + const scrollDirection = moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -1 : 1; + this.scrollLastFrameTime = performance.now(); + let scrollFrameCount = 0; + + const scrollAnimation = (currentTime: number) => { + if (!this.scrollAnimationId) { + return; + } + + // 每隔 20 帧重新计算一次帧率 + if (scrollFrameCount % 20 === 0) { + const deltaTime = currentTime - this.scrollLastFrameTime; + this.scrollLastFrameTime = currentTime; + // 计算过去 20 帧的平均帧率 + this.scrollCurrentFPS = deltaTime > 0 ? (20 * 1000) / deltaTime : 60; + } + scrollFrameCount++; + + // 基于当前帧率计算滚动步长,确保等效于 60fps 时的 16px/帧 + const baseScrollStep = 16; + const targetFPS = 60; + const scrollStep = baseScrollStep * (targetFPS / this.scrollCurrentFPS); + + this.element.scroll({ + top: this.element.scrollTop + scrollStep * scrollDirection + }); + + // 使用 requestAnimationFrame 继续动画 + this.scrollAnimationId = requestAnimationFrame(scrollAnimation); + }; + + // 检查浏览器是否支持 requestAnimationFrame + if (typeof requestAnimationFrame !== "undefined") { + this.scrollAnimationId = requestAnimationFrame(scrollAnimation); + } else { + // 回退到 setTimeout 方法 + const scrollInterval = 16; // 约 60fps + const scrollStep = 16; // 每次滚动的距离 + + const scrollAnimationFallback = () => { + this.element.scroll({ + top: this.element.scrollTop + scrollStep * scrollDirection + }); + this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval); + }; + this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval); + } + } + } else { + // 离开滚动区域时停止滚动 + if (this.scrollAnimationId) { + if (typeof cancelAnimationFrame !== "undefined") { + cancelAnimationFrame(this.scrollAnimationId); + } else { + clearTimeout(this.scrollAnimationId); + } + this.scrollAnimationId = null; + } } selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement; if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") { @@ -416,6 +476,15 @@ export class Outline extends Model { documentSelf.onselect = null; ghostElement?.remove(); item.style.opacity = ""; + // 清理滚动动画 + if (this.scrollAnimationId) { + if (typeof cancelAnimationFrame !== "undefined") { + cancelAnimationFrame(this.scrollAnimationId); + } else { + clearTimeout(this.scrollAnimationId); + } + this.scrollAnimationId = null; + } if (!selectItem) { selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover"); }