Skip to content

会话历史

一个长期使用的 AI 对话,历史消息很容易累积到几万条。这一节讲三件事: 往上翻历史、翻历史期间收到新消息、以及在会话之间切换。

基础的 loadMore 模型见 分页与无限加载, 这里只讲对话场景特有的部分。

下面这个示例就带着向上加载:滚到顶部会拉取更早的对话, 生成过程中向上翻历史时收到的新消息会记成未读数。

微应用尚未挂载。

只实现向上加载

对话的「未来」是用户自己产生的,不需要从服务端拉取,所以只实现 top 方向, 并关掉底部方向(hasMoreBottomfalse)。头部插入后视口内容会留在原处, 不需要任何手动的位移补偿。

向上的加载不会自动触发:偏移量为 0 是初始常态而不是用户意图, 若自动触发,任何配了 loadMore 的对话在挂载瞬间就会开始无限向上拉取。 要更早的消息,必须由「主动向上滚到顶」这个动作来表达。

翻历史期间来了新消息

用户正在向上读历史时收到新消息(自己在别的设备上发的、或者协作场景里对方发的), 视口不该被拽到底部。stickyBottom 只在原本就贴底时才跟随, 其余情况把新增量记在 loadState.pendingNew 里,用它渲染「N 条新消息」角标。

用户滚回底部后 pendingNew 自动归零,不需要自己清理。

切换会话

切换到另一个会话时,不只是数据换了——尺寸缓存、滚动位置、头部快照 都属于旧会话,必须一并丢弃:先 reset(),再灌新数据,再 scrollToBottom()

只换数据是不够的:旧会话的实测尺寸会被当成新数据的尺寸用,首屏位置就会偏。

跳到某一条消息

引用跳转、搜索结果定位都用 scrollToIndex(index, { align: 'start' })。 目标项此刻多半还没渲染、只能按预估尺寸落位;渲染出来量到真实尺寸后位置会自动修正, 所以不定高的消息列表里也不需要自己算偏移量。

未渲染的项搜不到

浏览器的页内搜索(Ctrl+F)只能找到当前渲染的那几十项。会话内搜索需要在数据里 匹配,再跳过去并高亮,不能依赖浏览器的查找功能——具体做法见搜索与引用跳转

Vanilla 写法

js
const vl = new VirtList(
  container,
  {
    list,
    itemKey: 'id',
    estimatedSize: 80,
    initialPosition: 'bottom',
    stickyBottom: true,
    hasMoreBottom: false,
    loadMore: async (direction) => {
      if (direction !== 'top') return false;
      const older = await fetchHistory(cursor);
      list = older.concat(list);
      vl.setList(list);
      // 返回该方向是否还有更多;返回 false 后不再触发
      return older.length > 0;
    },
  },
  {
    loadStateChange: (state) => {
      jumpBtn.textContent =
        state.pendingNew > 0 ? `${state.pendingNew} 条新消息` : '回到底部';
    },
  },
);

function switchConversation(messages) {
  vl.reset();            // 清空尺寸缓存与滚动状态
  vl.setList(messages);
  vl.scrollToBottom();   // 新会话同样从最新消息开始看
}