diff --git a/src/composables/ai/useAiVirtualScroll.ts b/src/composables/ai/useAiVirtualScroll.ts index 54f3601..1610d62 100644 --- a/src/composables/ai/useAiVirtualScroll.ts +++ b/src/composables/ai/useAiVirtualScroll.ts @@ -69,9 +69,14 @@ export function useAiVirtualScroll(options: AiVirtualScrollOptions) { // 卸载前:把实测高度锁到 sentinel 的 minHeight,防内容移除后 sentinel 塌成 0 // → scrollHeight 不变 → isNearBottom/scrollToBottom/回到底部计算零感知。 // 内容重新挂载时 RO 会把真实高度回填,registerSentinel 复挂时清 minHeight 由真实高度接管。 + // 防御(P0):RO 首次回调前 item 已被 IO 判不可见时 meta.height===0,直接不设 minHeight + // → sentinel 塌 0 高 → 下条消息上浮重叠。此处兜底:height>0 用实测值, + // height===0 用 el.offsetWidth 量得的实际渲染高度(此时 el 仍在 DOM),仍<=0 则用固定 40px。 const meta = itemMeta.get(key) - if (meta && meta.height > 0 && meta.el.isConnected) { - meta.el.style.minHeight = `${meta.height}px` + if (meta && meta.el.isConnected) { + const measured = meta.height > 0 ? meta.height : (meta.el.offsetHeight || 0) + const placeholder = measured > 0 ? measured : 40 + meta.el.style.minHeight = `${placeholder}px` } next.delete(key); changed = true } @@ -98,9 +103,12 @@ export function useAiVirtualScroll(options: AiVirtualScrollOptions) { const prev = itemMeta.get(key) if (el instanceof HTMLElement) { el.dataset.vscrollKey = key - // 复用上次实测高度作为初始 inline 占位(再次挂载时先占位,内容挂载后 RO 再校准) + // 复用上次实测高度作为初始 inline 占位(再次挂载时先占位,内容挂载后 RO 再校准)。 + // 防御(P0):首次挂载(无 prev / RO 未回填)h<=0 时若不设占位,sentinel 首帧塌 0 高 → + // 列表首次渲染时下条上浮重叠。此处用保守默认 40px 占位,RO 回调后用真实高度覆盖。 const h = prev?.height ?? 0 - if (h > 0) el.style.minHeight = `${h}px` + const placeholder = h > 0 ? h : 40 + el.style.minHeight = `${placeholder}px` itemMeta.set(key, { el, height: h }) ensureObserver() io?.observe(el)