From d60808923f2df97c847f1809fdb66adcfbfb43f5 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 8 Jun 2026 02:08:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A0=86=E5=8F=A0?= =?UTF-8?q?=E5=9D=97=E5=9C=A8=E6=8A=98=E5=8F=A0=E7=8A=B6=E6=80=81=E4=B8=8B?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E9=AC=BC=E7=95=9C=E4=B8=8E=E5=AE=B9=E5=99=A8?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E4=B8=8D=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(双重修复): 1. 容器高度计算改为精确目标高度公式 overhead + contentTarget,替代受CSS过渡 影响的实时高度测量,消除 JS测量↔CSS过渡 的循环反馈 2. 给容器添加260ms CSS过渡(与块内容max-height过渡同duration/easing), 用CSS统一负责所有动画,替代之前逐帧rAF追踪的视觉不同步 3. scrollListener中检测到scrollHeight变化时强制延长suppressUserIntentUntil 至400ms,防止stick-to-bottom spring的自动追底被误判为用户滚动而中断 配套清理:移除调试日志代码和临时/api/debug/stacked-log端点 --- server/app.py | 4 + static/src/components/chat/ChatArea.vue | 25 ++++- static/src/components/chat/StackedBlocks.vue | 106 +++++++++++++----- .../styles/components/chat/_chat-area.scss | 15 +++ 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/server/app.py b/server/app.py index 71e68a5..f723ee6 100644 --- a/server/app.py +++ b/server/app.py @@ -1,5 +1,9 @@ """统一入口:复用 app_legacy,便于逐步替换/收敛。""" import argparse +import os +import json +from datetime import datetime +from flask import request from .app_legacy import ( app, diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index 895d187..0eea756 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -805,6 +805,7 @@ let lastUserWheelUpTs = 0; let lastProgrammaticHintTs = 0; let lastProgrammaticHintSource = ''; let suppressUserIntentUntil = 0; +let lastLayoutChangeTs = 0; let scrollListener: ((event: Event) => void) | null = null; let wheelListener: ((event: WheelEvent) => void) | null = null; let traceAttachLogged = false; @@ -924,6 +925,21 @@ function attachBounceListener() { const trusted = (event as any).isTrusted === true; const closeToProgrammaticHint = now - lastProgrammaticHintTs <= 140; const likelyLayoutDriven = Math.abs(heightDelta) > 2; + // 记录最近一次布局驱动的滚动,用于在 CSS transition 结束 / runSync 收尾 + // 后继续抑制一小段时间,防止 stick-to-bottom 的 spring 收尾被误判为用户手动滚动 + if (Math.abs(heightDelta) > 1) { + lastLayoutChangeTs = now; + } + const recentlyLayoutDriven = now - lastLayoutChangeTs <= 500; + // ── 鬼畜根因修复 ── + // 当 scrollHeight 发生任何变化(即使只是 1-2px 的 sub-pixel 差异),说明即将或正在 + // 发生由内容高度变化驱动的自动追底滚动(stick-to-bottom spring)。如果不延长抑制窗口, + // 该滚动事件的 delta 可能较小(<5px)且 heightDelta 不足以触发 likelyLayoutDriven, + // 导致被误判为用户手动滚动 → stopScroll() → spring 中断 → 再次追底 → 振荡鬼畜。 + // 每次检测到高度变化时,将抑制窗口延长至 400ms,完整覆盖 spring 动画周期。 + if (Math.abs(heightDelta) > 0.5) { + suppressUserIntentUntil = Math.max(suppressUserIntentUntil, now + 400); + } const manualUpByWheel = delta < -3 && now - lastUserWheelUpTs <= 240; const strongManualUp = delta < -18 && now > suppressUserIntentUntil && !closeToProgrammaticHint; const shouldTreatAsUserIntent = @@ -931,7 +947,10 @@ function attachBounceListener() { Math.abs(delta) > 5 && now > suppressUserIntentUntil && (!closeToProgrammaticHint || strongManualUp) && - (delta < 0 ? strongManualUp || manualUpByWheel || !likelyLayoutDriven : !likelyLayoutDriven); + (delta < 0 + ? strongManualUp || manualUpByWheel || !likelyLayoutDriven + : !likelyLayoutDriven && !recentlyLayoutDriven); + if (shouldTreatAsUserIntent) { // 用户手动滚动时,立即中断 stick 引擎中可能仍在运行的动画滚动 stopScroll(); @@ -1079,7 +1098,9 @@ async function stickScrollToBottom( // 点击“滚动到底部”按钮时会触发 smooth 动画滚动;期间会产生 trusted scroll 事件, // 若不短暂抑制会被误判为“用户手动滚动”从而中断动画,表现为只滚动一点点。 if (options.force || options.behavior === 'smooth') { - suppressUserIntentUntil = Date.now() + 900; + suppressUserIntentUntil = Date.now() + 1200; + } else { + suppressUserIntentUntil = Date.now() + 600; } return await scrollToBottom({ animation: options.behavior === 'smooth' ? 'smooth' : 'auto', diff --git a/static/src/components/chat/StackedBlocks.vue b/static/src/components/chat/StackedBlocks.vue index 8912eb7..4202e7e 100644 --- a/static/src/components/chat/StackedBlocks.vue +++ b/static/src/components/chat/StackedBlocks.vue @@ -2,6 +2,7 @@