此為系列教程,需先完成
- Electron Forge【實戰】桌面應用 —— AI聊天(上)
- Electron Forge【實戰】桌面應用 —— AI聊天(中)
會話列表按更新時間倒序加載
src/db.ts
db.version(1).stores({// 主鍵為id,且自增// 新增updatedAt字段,用于排序conversations: "++id, updatedAt",
});
src/stores/conversation.ts
// 從本地存儲中查詢出會話列表async fetchConversations() {const items = await db.conversations.orderBy('updatedAt') // 按更新日期排序.reverse() // 倒序排列.toArray(); // 轉換為數組this.items = items;},
新創建的會話,在會話列表頂部
src/stores/conversation
// pinia 中新增會話this.items.unshift({id: newCId,...createdData,});
會話更新時,同步存儲到本地
src/views/Conversation.vue
// 本次回答結束后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
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
<div class="message-list" ref="_ref">
const _ref = ref<HTMLDivElement>();defineExpose({ref: _ref,
});
src/views/Conversation.vue
<MessageList :messages="convsersation!.msgList" ref="messageListRef" />
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 末尾添加
await messageScrollToBottom();
切換當前會話時
watch(() => route.params.id,async (newId: string) => {conversationId.value = parseInt(newId);// 切換當前會話時,聊天區自動滾動到底部await messageScrollToBottom();}
);
AI 流式回答問題時
onUpdateMessage 內
// 根據消息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 回答超過一行時才觸發滾動
let currentMessageListHeight = 0;
const checkAndScrollToBottom = async () => {if (messageListRef.value) {const newHeight = messageListRef.value.ref.clientHeight;if (newHeight > currentMessageListHeight) {currentMessageListHeight = newHeight;await messageScrollToBottom("smooth");}}
};
onUpdateMessage 內改為
// 順滑滾動到底部await nextTick()await checkAndScrollToBottom()
切換會話時記得重置 currentMessageListHeight
watch(() => route.params.id,async (newId: string) => {conversationId.value = parseInt(newId);// 切換當前會話時,聊天區自動滾動到底部await messageScrollToBottom();// 切換當前會話時,將當前會話的消息列表的高度重置為0currentMessageListHeight = 0;}
);
恢復默認樣式
Tailwind CSS 默認移除了幾乎所有的默認樣式,導致無法渲染帶格式的富文本,通過插件 @tailwindcss/typography 來重置一套比較合理的默認樣式
npm install -D @tailwindcss/typography
src/index.css 中添加
@plugin "@tailwindcss/typography";
給目標內容加上類名 prose
即可
<div class="prose">要恢復樣式的內容</div>
自定義默認樣式
// 給 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 進行格式化渲染
npm install vue-markdown-render markdown-it-highlightjs --save
src/components/MessageList.vue 中使用
import VueMarkdown from "vue-markdown-render";
import markdownItHighlightjs from "markdown-it-highlightjs";const plugins = [markdownItHighlightjs];
要渲染的內容,改用 vue-markdown 組件,通過 source 傳入
<divv-elseclass="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>