ChatMessageList 消息列表组件:基于 BubbleList 搭建 AI 对话展示区

ChatMessageList 消息列表组件:基于 BubbleList 搭建 AI 对话展示区

前言

在 AI 工作台里,中间区域通常是用户停留时间最长的地方。

它承担的是最核心的对话体验:

text 复制代码
用户输入需求
AI 流式回复
消息自动向下滚动
不同角色展示不同气泡样式

在当前项目中,这一块被拆成了 ChatMessageList 组件。

它不负责发送请求,也不负责处理 SSE 流式接口,而是专注于一件事:

把当前会话里的消息,以接近真实 AI 产品的方式展示出来。

这篇文章主要介绍:

  • ChatMessageList 在工作台里的位置;
  • 它的整体结构;
  • 使用了哪些组件;
  • 如何区分用户消息和 AI 消息;
  • 当前实现了哪些展示效果。

这里不会过多展开 BubbleList 的源码细节。后面会单独写一篇文章,分析 BubbleListtypingautoScroll 这些关键能力的内部实现。

组件位置

当前组件文件位于:

text 复制代码
src/features/workbench/components/chat/ChatMessageList.vue

它被 ChatWorkspace 使用:

text 复制代码
src/features/workbench/components/chat/ChatWorkspace.vue

整体页面结构大概是:

text 复制代码
WorkbenchPage
  ├── HistoryPanel       左侧历史对话
  ├── ChatWorkspace      中间聊天工作区
  │   ├── ChatHeader     当前会话标题
  │   ├── ChatMessageList 消息列表
  │   └── ChatComposer   底部输入框
  └── InspectorPanel     右侧辅助面板

ChatMessageList 处在 ChatWorkspace 中间,是整个工作台的消息展示区域。

当前组件做什么

ChatMessageList 当前主要做四件事:

text 复制代码
渲染当前会话的消息列表
区分用户消息和 AI 消息
展示用户头像和 AI 头像
给 AI 消息增加顶部信息和底部操作按钮

它接收的核心数据是:

ts 复制代码
conversation.messages

每一条消息大概长这样:

ts 复制代码
{
  key: 'xxx',
  role: 'assistant',
  content: '这里是 AI 回复内容',
  loading: false,
  typing: { step: 2, interval: 24 },
}

其中最重要的是:

字段 作用
key 消息唯一标识
role 消息角色,当前有 userassistant
content 消息正文
loading 是否处于加载状态
typing 是否启用打字机效果

ChatMessageList 不关心消息从哪里来。

它只关心:

给我一个消息数组,我负责把它展示成对话列表。

使用 BubbleList 渲染消息

当前消息列表没有从零手写,而是使用了 Ant Design X Vue 提供的 BubbleList

ts 复制代码
import { BubbleList } from 'ant-design-x-vue';

模板里核心结构是:

vue 复制代码
<BubbleList
  :items="conversation.messages"
  :roles="roles"
  :auto-scroll="true"
>
  ...
</BubbleList>

这里用到三个关键参数。

参数 作用
items 消息数据源
roles 不同角色对应的气泡配置
auto-scroll 消息变化时自动滚动到合适位置

在这个组件里,BubbleList 承担了消息列表的基础能力:

text 复制代码
消息排列
角色布局
气泡渲染
自动滚动
打字机内容更新

所以 ChatMessageList 本身不需要关心每条消息怎么布局,只需要把消息数据和角色配置传进去。

roles:区分用户和 AI

AI 对话组件最常见的需求,就是区分不同角色。

用户消息通常在右边,AI 消息通常在左边。

当前组件通过 roles 配置完成这件事:

ts 复制代码
const roles = {
  user: {
    placement: 'end',
    variant: 'filled',
    shape: 'round',
    avatar: {
      src: userAvatar,
      size: 32,
      shape: 'square',
    },
    classNames: { content: 'agent-message-user' },
  },
  assistant: {
    placement: 'start',
    variant: 'borderless',
    avatar: {
      src: aiAvatar,
      size: 32,
      shape: 'square',
    },
    classNames: { content: 'agent-message-ai' },
  },
};

这段配置可以理解成:

text 复制代码
role = user       使用用户气泡样式
role = assistant  使用 AI 气泡样式

用户消息:

text 复制代码
靠右展示
使用填充色气泡
展示用户头像

AI 消息:

text 复制代码
靠左展示
使用白色卡片气泡
展示 AI 头像

这样消息数据里只需要写:

ts 复制代码
role: 'user'

或者:

ts 复制代码
role: 'assistant'

页面就会自动使用不同的展示方式。

自定义头像

当前组件使用了两张本地图片作为头像:

ts 复制代码
import userAvatar from '@/assets/avatar/user.png';
import aiAvatar from '@/assets/avatar/ai.png';

然后在 roles 中配置:

ts 复制代码
avatar: {
  src: userAvatar,
  size: 32,
  shape: 'square',
}

这里的 avatar 使用的是组件内置头像能力。

也就是说,我们不需要自己额外写一个头像 DOM,只要传入头像配置,BubbleList 内部会帮我们把头像渲染到正确的位置。

当前效果大概是:

这种布局很接近常见 AI 聊天产品。

AI 消息顶部信息

除了头像和内容,当前组件还给 AI 消息增加了一行顶部信息:

vue 复制代码
<template #header="{ item }">
  <div v-if="item.role === 'assistant'" class="agent-message-meta">
    <span>Zero Code Agent</span>
  </div>
</template>

也就是说,只有 AI 消息会展示:

text 复制代码
Zero Code Agent

用户消息不会展示这行内容。

这样做的好处是:

text 复制代码
用户可以更清楚地知道这条消息来自当前 Agent

后续如果系统里有多个 Agent,也可以在这里展示不同的 Agent 名称。

比如:

text 复制代码
Zero Code Agent
Code Review Agent
Design Agent

当前只是先保留了一个简单的展示位。

AI 消息底部操作

当前组件还给 AI 消息增加了底部操作区:

vue 复制代码
<template #footer="{ item }">
  <div v-if="item.role === 'assistant' && !item.loading" class="agent-message-actions">
    <button type="button"><CopyOutlined />复制</button>
    <button type="button"><ReloadOutlined />重试</button>
    <button type="button"><LikeOutlined /></button>
    <button type="button"><DislikeOutlined /></button>
  </div>
</template>

展示效果大概是:

这几个按钮当前主要是 UI 展示:

操作 作用
复制 复制 AI 回复内容
重试 重新生成当前回复
点赞 反馈回复质量较好
点踩 反馈回复质量较差

后续可以继续接入真实逻辑。

比如:

text 复制代码
复制按钮接 Clipboard API
重试按钮重新发起当前消息请求
点赞点踩上报反馈数据

这里先把交互入口留出来。

自动滚动

消息列表开启了 auto-scroll

vue 复制代码
<BubbleList
  :items="conversation.messages"
  :roles="roles"
  :auto-scroll="true"
>

它的作用是:

text 复制代码
当消息增加或 AI 回复持续变化时,消息列表自动滚动到合适的位置。

这对 AI 流式输出很重要。

因为 AI 回复不是一次性展示出来,而是不断追加内容:

text 复制代码
你
你好
你好,
你好,我
你好,我可以

如果没有自动滚动,用户可能需要手动向下滑,才能看到最新回复。

当前组件把这部分交给 BubbleList 自己处理。

组件外部只负责保证消息列表区域有明确高度:

css 复制代码
.chat-message-list {
  height: 100%;
  min-height: 0;
  overflow: hidden;
}

.chat-message-list :deep(.ant-bubble-list) {
  height: 100%;
  min-height: 0;
  box-sizing: border-box;
  padding: 22px 26px;
}

这里不是自己实现滚动逻辑,而是让 BubbleList 内部的滚动容器能够正常工作。

打字机效果

AI 回复的打字机效果不是在 ChatMessageList 里直接写动画实现的。

ChatMessageList 只负责把消息交给 BubbleList

vue 复制代码
<BubbleList :items="conversation.messages" />

真正的打字机配置在消息对象上:

ts 复制代码
{
  role: 'assistant',
  content: '',
  typing: { step: 2, interval: 24 },
}

其中:

字段 作用
step 每次展示几个字符
interval 每次展示的间隔时间

配合 SSE 流式接口,最终效果就是:

text 复制代码
后端一段段返回内容
前端持续更新 message.content
BubbleList 根据 typing 做打字机展示

这也是比较常见的分层方式:

text 复制代码
SSE 负责数据传输
消息状态负责保存内容
BubbleList 负责视觉展示

当前页面效果

最终 ChatMessageList 实现的效果可以概括成:

text 复制代码
用户消息在右侧
AI 消息在左侧
双方使用不同头像
AI 消息带 Agent 名称
AI 消息底部有复制、重试、点赞、点踩操作
AI 回复支持流式展示和打字机效果
消息列表支持自动滚动

整体效果类似一个基础版 AI 对话窗口:

组件边界

当前 ChatMessageList 的边界很清晰。

它负责:

text 复制代码
展示消息
展示头像
展示气泡样式
展示 AI 消息操作区
承载 BubbleList 的 auto-scroll 和 typing 效果

它不负责:

text 复制代码
发送消息
创建应用
连接 SSE
保存历史记录
处理后端接口

这些逻辑放在更上层的 composable 中。

比如当前项目里,流式请求和消息更新主要在:

text 复制代码
src/features/workbench/composables/useWorkbenchChat.ts

这样拆分后,组件职责会比较干净:

text 复制代码
ChatMessageList 只关心展示
useWorkbenchChat 负责业务和状态
api 层负责接口请求

这也是我在这个工作台里比较想保持的结构。

小结

这篇文章简单介绍了 ChatMessageList 的整体实现。

它不是一个复杂的业务组件,而是一个典型的"展示型组件"。

核心思路是:

text 复制代码
用 BubbleList 承接消息列表能力
用 roles 区分用户和 AI
用 avatar 展示双方头像
用 header 展示 AI 名称
用 footer 放置消息操作
用 auto-scroll 和 typing 优化 AI 对话体验

目前这篇只介绍了组件结构和页面效果。

后续会深入结合源码分析各个部分如何实现,比如打字机等效果。

相关推荐
阿黎梨梨2 小时前
从零搞懂 JWT 登录鉴权:一张“电子通行证”的诞生记
前端
HjhIron2 小时前
实战 React + JWT + Zustand:从零搭建登录鉴权系统
前端
晴天162 小时前
DeepSeek Harness 全景技术解析-Day23
前端·deepseek
风月说与山鬼3 小时前
三、uni-app页面配置(pages.json)
前端·uni-app
daols883 小时前
vue 实现基于 vxe-table 构建多维度产品对比表
前端·javascript·vue.js
前端 贾公子3 小时前
第08章:中间件(5)
服务器·前端·javascript
tech_zjf3 小时前
当 AI 把 Next.js Route 越写越快:我为什么做了 next-route-kit
前端·后端
常宇佳3 小时前
vue3 @代指src路径设置
前端·typescript·vue
砚凝霜4 小时前
软考网络工程师|案例分析:Eth‑Trunk 链路聚合、iStack 堆叠、CSS 集群核心考点总结
前端·css·网络