MessageScroller 会话滚动容器
会话滚动容器:一套「滚动意图模型」——读者在底部时跟随新内容(AI 式)、IM 式始终钉底可切、跳到底按钮、上翻加载历史不跳动(含异步资源保位)、轮次锚定(AI「回到最后提问」)、首屏防跳。组件不拥有消息(只提供滚动行为),宿主接任意后端。
基础用法
会话流(default-position=end 默认打开在底部)
默认 default-position="end":打开即定位到底部。viewport 为 role="region" + 可读名称(缺省走 locale「消息列表」,可用 label 属性覆盖)+ tabindex="0"(Tab 聚焦后方向键滚动);内容容器 role="log" + aria-relevant="additions"(屏幕阅读器只播报新增行)。
首屏防跳(data-pending-scroll)
end / last-anchor 打开前的未定位帧不闪现顶部
default-position="end"(及 last-anchor)需要真实定位:连接时组件即在宿主上反射 data-pending-scroll,此刻 viewport visibility: hidden(保留布局供定位测量);首帧定位与属性移除在同一帧完成——读者看不到「闪现顶部再跳到底」的未定位帧。default-position="start" 天然定位(scrollTop=0 就是初始值),不设该反射。读者若在定位前已滚动(让位守卫生效),反射同样移除,viewport 不会被永久隐藏。
AI 式底部跟随(auto-scroll)
auto-scroll:读者在底部才跟随流式输出
auto-scroll 是 AI 助手的默认安全模式:仅当读者在底部(near-bottom ≤ edge-threshold,默认 8px)时跟随流式增长;上翻阅读历史时不抢位置——「绝不逆着读者意图移动」。内容高度变化由 slotchange + ResizeObserver 双通道感知(纯文本增长无 slotchange 信号时仍能跟随)。
IM 式始终钉底(pin-to-bottom)
pin-to-bottom:无论在哪都拉回底部
pin-to-bottom 在场即 IM 模式:新内容总是滚到底,读者上翻也会被拉回。裁决优先级:pin-to-bottom > turn-anchor > auto-scroll。
跳底按钮与状态事件
跳底按钮 + oas-scroll-state 反馈
占位内容 1——滚动容器内容超过视口高度时,离开底部会出现右下角跳底按钮。
占位内容 2
占位内容 3
占位内容 4
占位内容 5
占位内容 6(最底部)
离开底部且可向下滚时显示内置跳底按钮(part="button",可读名称走 locale;也可 slot="button" 自定义);点击滚到底并消失。滚动位置跨过判定线时派发 oas-scroll-state(detail { atBottom, atTop, canScrollStart, canScrollEnd }),并反射 data-scrollable="start end"(供边缘渐隐等 CSS 判定)。scrollToEnd()/scrollToStart() 方法可编程定位(behavior 可传 'smooth')。
上翻加载历史(异步资源保位)
滚到顶加载更早消息,阅读位置不跳
历史内图片延迟加载撑高,锚行位置不动
顶部插入历史时,preserve-scroll-on-prepend(缺省开启,显式 ="false" 关闭)按「插入前首节点的内容坐标位移」精确补偿 scrollTop——同一帧既 prepend 历史又 append 新消息时,只结算顶部插入量(底部追加不参与补偿,不会多补)。补偿后组件锁定保位锚(插入前首节点):历史内的图片等异步资源随后加载撑高上方内容时(ResizeObserver 通道,无 slotchange 信号),按锚点视口位移持续补偿,阅读位置在资源加载完成前不跳。给消息行声明 stable message-id 时保位锚优先按 id 重查(宿主重建行后仍能找回);无 id 用元素引用。
打开位置 last-anchor 与 scrollToMessage
default-position=last-anchor:打开即回到最后提问位置
宿主在消息行上标 anchor 属性即轮次锚点(AI 场景通常标在用户提问行);default-position="last-anchor" 打开时定位到最后一个锚点的顶部——长会话回到「最后提问位置」继续阅读,而不是落底或回顶;锚点靠底(下方内容不足一屏)时贴底显示(滚动上限 clamp,与「最后提问在屏幕底部」的产品直觉一致)。无锚点时回退 end。scrollToMessage(id, opts?) 按 message-id 精确滚到某条消息顶部(命中返回 true,未知 id 返回 false 且不滚动)。
轮次锚定(turn-anchor)
turn-anchor:回答流入时提问钉在视口顶(prev-peek 露上一条语境)
turn-anchor 在场把「一次提问 + 回答」作为锚定单元:跟随态把最后 anchor 标记的轮次锚点钉在视口顶 prev-peek px 处(默认 64,露出上一条尾部作语境),回答在锚下方流入。位置即意图——锚点贴视口顶(或处于 peek 对齐窗口)= 锚顶跟随;读者在底部 = 贴底跟随(新轮次从底部流入视野);两者皆离 = 读者接管,静止不拉回。跟随模式按当前位置一次性定型,贴底跟随中锚点穿越顶部窗口不会被误拉回顶部。
可见性通道(track-visible)
当前可见消息清单 + 当前锚点
track-visible 在场开启可见性通道(pay-for-use,未开启零计算):可见集变化时派发 oas-visible-change(detail { visibleMessageIds, currentAnchorId }),并始终提供 visibleMessageIds / currentAnchorId getter(按需计算)。visibleMessageIds 是当前与视口相交的消息 id 清单(未声明 message-id 的行不计入);currentAnchorId 是「读者阅读位置所在的轮次」——最后一个视口顶附近的 anchor 标记行。供大纲菜单、搜索定位、已读追踪等使用。
长会话虚拟化(组合 oas-virtual-list)
scroller 负责常规会话的锚定/跟随;数千轮超长会话把消息数据交给 oas-virtual-list 做视口窗口渲染(只渲染可见行 + 缓冲),消息行仍用 oas-message-row / oas-bubble 渲染——虚拟化归 virtual-list,滚动意图归 scroller,职责互不重写。scrollToIndex(index, { align: 'end' }) 打开到最新、追加后跟随。
2000 条消息的虚拟化会话(追加一条试试)
边界
- 可见性通道:
track-visible未开启时不计算、不派发(pay-for-use);开启后按视口相交计算(长会话逐行 rect 的代价由宿主开启该属性时承担)。未声明message-id的行不进可见集。 - 虚拟化与渲染降耗:离屏行跳过渲染(
content-visibility等)随虚拟化一起提供(slotted 元素上单独启用会卡占位高度不展开,实测撤回);数千轮长会话接虚拟列表由宿主组合。 - 空态:无消息时容器照常渲染(空 log),宿主放空态插槽内容即可。
- prepend 保位边界:保位锚在读者真实滚动时释放(读者位置优先);
pin-to-bottom/ 贴底跟随时补偿无意义(钉底后发覆盖)。同帧混合插入的结算依赖插入前首元素仍在 DOM(宿主删除该行时该帧不补偿)。 - 首屏防跳边界:
data-pending-scroll只覆盖「连接 → 首帧定位」这一段;宿主在挂载后异步拉取首批消息的场景由auto-scroll/pin-to-bottom的装载行为接管。pending 期间 viewport 不可聚焦(毫秒级窗口)。 label覆盖 viewport 可读名称后被组件吸收移除(原生全局属性吸收惯例)。anchor/message-id是宿主写在 slotted 行上的协作标记(scroller 只读);Vue 宿主下两属性为普通透传属性,不会被框架剥离。
API
oas-message-scroller
属性
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
auto-scroll | AI 式底部跟随(布尔在场,默认关):仅当读者在底部(near-bottom ≤ edge-threshold)时跟随流式增长,上翻阅读不打扰 | boolean | — |
default-position | 打开位置:start(顶部)/ end(底部,默认;非法值回退 end)/ last-anchor(定位到最后一个 anchor 标记消息顶部——AI 场景「回到最后提问位置」;无锚点回退 end) | — | — |
edge-threshold | 底部判定阈值 px(默认 8)——距底 ≤ 阈值即视为在底部(near-bottom 检测) | — | — |
label | viewport 可读名称覆盖(缺省走 locale「消息列表」;读入渲染后从宿主移除——原生全局属性吸收惯例) | — | — |
pin-to-bottom | IM 式始终钉底(布尔在场):新内容总是滚到底、读者上翻也拉回;与 auto-scroll 同时在场时优先 | boolean | — |
preserve-scroll-on-prepend | 顶部插入历史时补偿 scrollTop 保持阅读位置(缺省开启;显式 ="false" 关闭)——按插入前首节点位移精确结算(同帧 append+prepend 混合只结算顶部插入量),补偿后锁定保位锚:历史内异步资源(图片等)撑高上方内容时按锚点视口位移持续补偿(message-id 优先重查) | — | — |
prev-peek | 轮次锚定对齐时锚顶上方露出的语境 px(默认 64;非法/负值回退 64) | — | — |
track-visible | 布尔在场:开启可见性通道(oas-visible-change 事件 + visibleMessageIds / currentAnchorId getter);未开启零计算(pay-for-use) | boolean | — |
turn-anchor | 轮次锚定(布尔在场):把一次提问+回答作为锚定单元——锚点贴视口顶时把最后 anchor 标记消息钉在视口顶 prev-peek 处,回答在锚下方流入;读者在底部延续贴底跟随,上翻即静止(绝不逆着读者意图移动) | boolean | — |
事件
| 事件 | 说明 |
|---|---|
oas-scroll-state | 滚动位置跨过判定线时派发,detail { atBottom, atTop, canScrollStart, canScrollEnd };首帧(初始定位后)广播一次初始态——宿主监听晚于 upgrade 也能收到 |
oas-visible-change | track-visible 开启时可见集变化派发,detail { visibleMessageIds, currentAnchorId }(供大纲/搜索定位等) |
插槽
| 名称 | 说明 |
|---|---|
| 默认 | 消息行列表(oas-message-row / oas-marker 等;行上的 anchor / message-id 属性为 scroller 协作标记) |
button | 自定义跳底按钮内容(替换内置箭头图形;显隐与点击仍由组件裁决——缺省为内置按钮 part=button) |
CSS 变量
| CSS 变量 | 默认值 |
|---|---|
--oas-message-gap | var(--oas-space-4) |
方法
| 方法 | 说明 |
|---|---|
scrollToEnd(opts?) | 滚到底部;opts.behavior 可传 'smooth'(缺省瞬时) |
scrollToStart(opts?) | 滚到顶部(参数同上) |
scrollToMessage(id, opts?) | 按 message-id 滚到指定消息行顶部;命中返回 true,未知 id 返回 false 且不滚动(参数同上) |
状态反射:data-scrollable("start" / "end" / "start end" 空格分隔,无可滚为空);data-pending-scroll(首屏防跳——初始定位应用前在场,定位/读者让位后移除)。ARIA:viewport role="region" + 可读名称 + tabindex="0";内容容器 role="log" + aria-relevant="additions"。CSS 变量:--oas-message-gap(行间距,默认 var(--oas-space-4))。部件:::part(scroller) / ::part(viewport) / ::part(content) / ::part(button)。