此为系列教程,需先完成
会话列表按更新时间倒序加载

src/db.ts
ts
db.version(1).stores({
// 主键为id,且自增
// 新增updatedAt字段,用于排序
conversations: "++id, updatedAt",
});
src/stores/conversation.ts
ts
// 从本地存储中查询出会话列表
async fetchConversations() {
const items = await db.conversations.orderBy('updatedAt') // 按更新日期排序
.reverse() // 倒序排列
.toArray(); // 转换为数组
this.items = items;
},
新创建的会话,在会话列表顶部
src/stores/conversation
ts
// pinia 中新增会话
this.items.unshift({
id: newCId,
...createdData,
});
会话更新时,同步存储到本地
src/views/Conversation.vue
ts
// 本次回答结束后
if (data.is_end) {
// 清空流式消息的内容
streamContent = "";
// 更新会话时,需要移除 id 字段,否则会报错
let temp_convsersation = JSON.parse(
JSON.stringify(convsersation.value)
);
delete temp_convsersation.id;
await conversationStore.updateConversation(
convsersation.value!.id,
temp_convsersation
);
}
src/stores/conversation.ts
ts
async updateConversation(id: number, newData: Omit<ConversationProps, "id">) {
// 本地存储中更新会话
await db.conversations.update(id, newData);
// pinia 中更新会话
const index = this.items.findIndex((item) => item.id === id);
if (index > -1) {
this.items[index] = { id, ...newData };
}
},
聊天区自动滚动到底部
src/components/MessageList.vue
需对外暴露 ref
html
<div class="message-list" ref="_ref">
ts
const _ref = ref<HTMLDivElement>();
defineExpose({
ref: _ref,
});
src/views/Conversation.vue
html
<MessageList :messages="convsersation!.msgList" ref="messageListRef" />
ts
const messageListRef = ref<{ ref: HTMLDivElement }>();
const messageScrollToBottom = async (behavior?: string) => {
await nextTick();
if (messageListRef.value) {
// 获取到自定义组件内的真实 ref 调用 scrollIntoView
messageListRef.value.ref.scrollIntoView({
block: "end",
behavior: behavior as ScrollBehavior, // "auto" | "instant" | "smooth"
});
}
};
会话页初次加载时
在 onMounted 末尾添加
ts
await messageScrollToBottom();
切换当前会话时

ts
watch(
() => route.params.id,
async (newId: string) => {
conversationId.value = parseInt(newId);
// 切换当前会话时,聊天区自动滚动到底部
await messageScrollToBottom();
}
);
AI 流式回答问题时
onUpdateMessage 内
ts
// 根据消息id, 获取到 loading 状态的消息
let msg = convsersation.value!.msgList[messageId];
// 将 AI 回答的流式消息替换掉 loading 状态的消息
msg.content = streamContent;
// 根据 AI 的返回,更新消息的状态
msg.status = getMessageStatus(data);
// 用 dayjs 得到格式化的当前时间字符串
msg.updatedAt = dayjs().format("YYYY-MM-DD HH:mm:ss");
// 顺滑滚动到底部
await messageScrollToBottom("smooth");
滚动性能优化 -- 仅当 AI 回答超过一行时才触发滚动
ts
let currentMessageListHeight = 0;
const checkAndScrollToBottom = async () => {
if (messageListRef.value) {
const newHeight = messageListRef.value.ref.clientHeight;
if (newHeight > currentMessageListHeight) {
currentMessageListHeight = newHeight;
await messageScrollToBottom("smooth");
}
}
};
onUpdateMessage 内改为
ts
// 顺滑滚动到底部
await nextTick()
await checkAndScrollToBottom()
切换会话时记得重置 currentMessageListHeight
ts
watch(
() => route.params.id,
async (newId: string) => {
conversationId.value = parseInt(newId);
// 切换当前会话时,聊天区自动滚动到底部
await messageScrollToBottom();
// 切换当前会话时,将当前会话的消息列表的高度重置为0
currentMessageListHeight = 0;
}
);
恢复默认样式
Tailwind CSS 默认移除了几乎所有的默认样式,导致无法渲染带格式的富文本,通过插件 @tailwindcss/typography 来重置一套比较合理的默认样式
ts
npm install -D @tailwindcss/typography
src/index.css 中添加
css
@plugin "@tailwindcss/typography";
给目标内容加上类名 prose
即可
html
<div class="prose">要恢复样式的内容</div>
自定义默认样式
css
// 给 p 标签添加 my-1 的 Tailwind CSS 样式
prose-p:my-1
更多自定义样式的方法见
https://github.com/tailwindlabs/tailwindcss-typography?tab=readme-ov-file#element-modifiers
渲染 markdown 的内容(支持代码高亮)
AI 模型返回的都是 markdown 语法的文本,需要按 markdown 进行格式化渲染
ts
npm install vue-markdown-render markdown-it-highlightjs --save
src/components/MessageList.vue 中使用
ts
import VueMarkdown from "vue-markdown-render";
import markdownItHighlightjs from "markdown-it-highlightjs";
const plugins = [markdownItHighlightjs];
要渲染的内容,改用 vue-markdown 组件,通过 source 传入
html
<div
v-else
class="prose prose-slate prose-hr:my-0 prose-li:my-0 prose-ul:my-3 prose-p:my-1 prose-pre:p-0"
>
<vue-markdown :source="message.content" :plugins="plugins" />
</div>