ChatMessageList 消息列表组件:基于 BubbleList 搭建 AI 对话展示区
前言
在 AI 工作台里,中间区域通常是用户停留时间最长的地方。
它承担的是最核心的对话体验:
text
用户输入需求
AI 流式回复
消息自动向下滚动
不同角色展示不同气泡样式
在当前项目中,这一块被拆成了 ChatMessageList 组件。
它不负责发送请求,也不负责处理 SSE 流式接口,而是专注于一件事:
把当前会话里的消息,以接近真实 AI 产品的方式展示出来。
这篇文章主要介绍:
ChatMessageList在工作台里的位置;- 它的整体结构;
- 使用了哪些组件;
- 如何区分用户消息和 AI 消息;
- 当前实现了哪些展示效果。
这里不会过多展开 BubbleList 的源码细节。后面会单独写一篇文章,分析 BubbleList、typing、autoScroll 这些关键能力的内部实现。
组件位置
当前组件文件位于:
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 |
消息角色,当前有 user 和 assistant |
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 对话体验
目前这篇只介绍了组件结构和页面效果。
后续会深入结合源码分析各个部分如何实现,比如打字机等效果。