From 4807c0b9f10b87511de6bd566b36397a69a1286a Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 9 Jun 2026 10:05:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E9=87=8D=E6=9E=84=E5=A0=86?= =?UTF-8?q?=E5=8F=A0=E5=9D=97=E9=AB=98=E5=BA=A6=E5=BC=95=E6=93=8E=EF=BC=8C?= =?UTF-8?q?=E6=A0=B9=E6=B2=BB=E9=AC=BC=E7=95=9C=E6=8A=96=E5=8A=A8=E5=B9=B6?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StackedBlocks 旧实现每帧用 getBoundingClientRect 读正在过渡的块高度, 叠加 360ms runSync 轮询 + ResizeObserver + animateMoreToggle 三个驱动源 互相触发,形成不收敛的测量-回写反馈环 = 高频上下抽搐;且新块出现/展开 收起无外框动画(瞬间赋值且无 transition)。 高度引擎重构: - 改测真实 DOM 稳定值(header.offsetHeight + content-inner.offsetHeight + border)算每块高度。content-inner 自身高度不受父级 collapsible-content 的 max-height 过渡/overflow 裁切影响,是稳定目标态值,杜绝读动画中间帧。 - 删除 runSync 轮询、animateMoreToggle JS lerp、逐帧 getBoundingClientRect。 measureAndCompute 仅在状态变化时测一次。 - ResizeObserver 只观察 content-inner(流式增长源),不观察被 transition 的 元素,断开写→RO→写回环。 - shell/viewport/inner 三者同一条 height/transform 260ms transition 同步过渡, 外框增高、传送带上移、展开收起、收起裁切全平滑。 单块路径统一(修 1→2 块重建鬼畜): - splitActionGroups 阈值 >=2 改 >=1,单个 thinking/tool 也走 StackedBlocks, 消除 single↔stack 路径整体切换导致的组件重新挂载(表现为外框瞬间塌扁再展开)。 - 单块用 isSingle + .stacked-shell--single 还原原 single 外观(12px 圆角+透明底)。 首次/后续进场动画区分: - ChatArea 传 conversation-running + is-latest-message。 - animated(运行中+最新) 控 TransitionGroup :appear/:css(块进场动画); ready(首次 measure 后一次性) 控 shell/viewport/inner/collapsible-content 的 transition:初始挂载/历史批量加载高度直接到位不跳动,之后启用使展开/ 收起在历史对话里也平滑。 Co-Authored-By: Claude Opus 4.8 (1M context) --- static/src/components/chat/ChatArea.vue | 15 +- static/src/components/chat/StackedBlocks.vue | 287 ++++++++++--------- 2 files changed, 156 insertions(+), 146 deletions(-) diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index 895d187..f0281ad 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -117,6 +117,8 @@ class="stacked-blocks-wrapper" :actions="group.actions" :expanded-blocks="expandedBlocks" + :conversation-running="streamingMessage" + :is-latest-message="index === latestMessageIndex" :icon-style="iconStyleSafe" :toggle-block="toggleBlock" :register-thinking-ref="registerThinkingRef" @@ -1535,20 +1537,15 @@ const splitActionGroups = (actions: any[] = [], messageIndex = 0) => { } const flushBuffer = () => { - if (buffer.length >= 2) { + // 单个可堆叠块(thinking/tool)也走 stack 路径,避免 1↔2 块时在 single/stack + // 两套渲染路径之间整体切换导致 StackedBlocks 重新挂载(表现为外框瞬间塌成最扁再展开)。 + // 单块时 StackedBlocks 内部 moreVisible=false,不显示「更多」头,外观由 CSS 对齐 single。 + if (buffer.length >= 1) { result.push({ kind: 'stack', actions: buffer.slice(), key: `stack-${messageIndex}-${result.length}` }); - } else if (buffer.length === 1) { - const single = buffer[0]; - result.push({ - kind: 'single', - action: single, - actionIndex: actions.indexOf(single), - key: single.id || `single-${messageIndex}-${result.length}` - }); } buffer = []; }; diff --git a/static/src/components/chat/StackedBlocks.vue b/static/src/components/chat/StackedBlocks.vue index 8912eb7..8d04f29 100644 --- a/static/src/components/chat/StackedBlocks.vue +++ b/static/src/components/chat/StackedBlocks.vue @@ -1,6 +1,7 @@