vue3實(shí)現(xiàn)ai聊天對(duì)話框功能
各功能部分學(xué)習(xí)
input輸入
使用@keydown 鍵盤(pán)進(jìn)行操作,回車(chē)和點(diǎn)擊一樣進(jìn)行搜索
@keydown.enter.exact.prevent="handleSend" @keydown.enter.shift.exact="newline"
按鈕 loading 加載圖標(biāo):這里設(shè)置 template 插槽
<el-button
type="primary"
:loading="loading"
@click="handleSend"
>
<template #icon>
<el-icon><Position /></el-icon>
</template>
發(fā)送
</el-button>職責(zé)分離,子組件完成頁(yè)面搭建,需要用到哪些邏輯,再通過(guò) emit 通信到父組件由父組件完成。整個(gè)頁(yè)面涉及到一個(gè)多個(gè)組件使用的 loading 屬性,由 settings 倉(cāng)庫(kù)存儲(chǔ)。
message對(duì)話框
簡(jiǎn)單的一行代碼使用戶消息放在右側(cè)
&.message-user {
flex-direction: row-reverse;
//翻轉(zhuǎn)實(shí)現(xiàn)用戶布局在右側(cè)
.message-content {
align-items: flex-end;
}
}換行屬性white-space: pre-wrap;保留源代碼中的空白字符和換行符,否則為white-space: normal;(默認(rèn)值):合并連續(xù)的空白字符,忽略源代碼中的換行符,自動(dòng)換行
settings設(shè)置面板
- 設(shè)置屬性都設(shè)置在倉(cāng)庫(kù)里,用于全局。
- 樣式命名
w-full這總可以通俗易懂的看出是寬度占滿。 - 如何在 stlye 中修改 elementPlus 原本的樣式。
- elementPlus 設(shè)置暗黑模式
//App.vue
<template>
<div :class="{ 'dark': isDarkMode }">
<router-view />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useSettingsStore } from './stores/settings'
const settingsStore = useSettingsStore()
const isDarkMode = computed(() => settingsStore.isDarkMode)
</script>
//dark.scss
html.dark {
// Element Plus 暗黑模式變量覆蓋
--el-bg-color: var(--bg-color);
--el-bg-color-overlay: var(--bg-color-secondary);
--el-text-color-primary: var(--text-color-primary);
--el-text-color-regular: var(--text-color-regular);
--el-border-color: var(--border-color);
// Element Plus 組件暗黑模式樣式覆蓋
.el-input-number {
--el-input-number-bg-color: var(--bg-color-secondary);
--el-input-number-text-color: var(--text-color-primary);
}
.el-select-dropdown {
--el-select-dropdown-bg-color: var(--bg-color);
--el-select-dropdown-text-color: var(--text-color-primary);
}
.el-slider {
--el-slider-main-bg-color: var(--primary-color);
}
}
//main.js
import './assets/styles/dark.scss'使用 scss 設(shè)置暗黑模式:
1. 使用pinia狀態(tài)管理。
2. 在設(shè)置面板點(diǎn)擊切換,觸發(fā)toggleDarkMode動(dòng)作(在pinia中設(shè)置),切換isDarlMode狀態(tài),并更新跟元素的data-theme屬性。
3. 暗黑模式的樣式設(shè)置在scss變量中
當(dāng)刷新后樣式會(huì)變回白天模式,但是這時(shí)候settings中選中的還是黑夜模式,這是為什么?
答:雖然設(shè)置存儲(chǔ)在了 localStorage 中,但是在頁(yè)面刷新后沒(méi)有正確地初始化深色模式的樣式。我們需要在應(yīng)用啟動(dòng)時(shí)立即應(yīng)用存儲(chǔ)的主題設(shè)置。
解決辦法:給 App.vue 加上掛載時(shí)就進(jìn)行一次設(shè)置。
onMounted(() => {
// 根據(jù)存儲(chǔ)的設(shè)置初始化主題
document.documentElement.setAttribute('data-theme', settingsStore.isDarkMode ? 'dark' : 'light')
})傳輸數(shù)據(jù)
axios、 XMLHttpRequest 、fetch
以下是 Axios、XMLHttpRequest 和 Fetch 在發(fā)送 HTTP 請(qǐng)求時(shí)的對(duì)比,包括用法、性能、兼容性和適用場(chǎng)景的詳細(xì)分析:
前后端通信所用技術(shù)對(duì)比
AI 對(duì)話需要到流式響應(yīng)處理,通信技術(shù)最終采用 fetch。
| 場(chǎng)景 | Axios | XMLHttpRequest | Fetch |
|---|---|---|---|
| 快速開(kāi)發(fā)簡(jiǎn)單請(qǐng)求 | 優(yōu)秀,語(yǔ)法簡(jiǎn)潔 | 不適合,代碼繁瑣 | 良好,語(yǔ)法簡(jiǎn)潔 |
| 全局配置需求 | 支持,提供 defaults 配置 | 不支持 | 需要手動(dòng)實(shí)現(xiàn) |
| 請(qǐng)求/響應(yīng)攔截處理 | 原生支持?jǐn)r截器 | 不支持 | 需要手動(dòng)實(shí)現(xiàn) |
| 上傳/下載進(jìn)度監(jiān)聽(tīng) | 不支持 | 支持 | 不支持 |
| 兼容舊版瀏覽器 | 支持,通過(guò) polyfill | 支持 | 不支持 |
| 流式響應(yīng)處理 | 不支持 | 不支持 | 支持(結(jié)合 ReadableStream) |
| 輕量級(jí)需求 | 適合 | 不適合 | 適合 |
實(shí)現(xiàn)流式數(shù)據(jù)處理
1. 項(xiàng)目實(shí)現(xiàn)代碼
- 發(fā)送請(qǐng)求
在 src/utils/api.js 中,chatApi.sendMessage 方法負(fù)責(zé)發(fā)送請(qǐng)求。根據(jù) stream 參數(shù)的值,決定是否請(qǐng)求流式響應(yīng)。
async sendMessage(messages, stream = false) {
// ...
const response = await fetch(`${API_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
...createHeaders(),
...(stream && { 'Accept': 'text/event-stream' }) // 如果是流式響應(yīng),添加相應(yīng)的 Accept 頭
},
body: JSON.stringify(payload)
})
// ...
if (stream) {
return response // 對(duì)于流式響應(yīng),直接返回 response 對(duì)象
}
// ...
}處理流式響應(yīng)
在 src/utils/messageHandler.js 中,processStreamResponse 方法負(fù)責(zé)處理流式響應(yīng)。它使用 ReadableStream 的 getReader 方法逐步讀取數(shù)據(jù),并使用 TextDecoder 解碼數(shù)據(jù)。
- 讀取流數(shù)據(jù)
- 解碼數(shù)據(jù)塊
- 處理解碼后的數(shù)據(jù):先拆分為行(數(shù)組),再轉(zhuǎn)換為json字符串,再轉(zhuǎn)換為js對(duì)象,提取出對(duì)象中content內(nèi)容,更新message、更新token使用量
async processStreamResponse(response, { updateMessage, updateTokenCount }) {
try {
let fullResponse = '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
// 1.讀取流數(shù)據(jù)
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('流式響應(yīng)完成');
break;
}
//2.解碼數(shù)據(jù)塊
const chunk = decoder.decode(value); //這里每一個(gè)chunk是一個(gè)可能包含多個(gè)數(shù)組
//3.處理解碼后的數(shù)據(jù),先拆分為行(數(shù)組),再轉(zhuǎn)換為json字符串,再轉(zhuǎn)換為js對(duì)象,提取出對(duì)象中content內(nèi)容,更新message、更新token使用量
// 3.1 拆分為行
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.includes('data: ')) {
// 3.2 轉(zhuǎn)換為json字符串
const jsonStr = line.replace('data: ', '');
// 檢查是否結(jié)束
if (jsonStr === '[DONE]') {
console.log('流式響應(yīng)完成,讀取完畢');
continue;
}
// 3.3 轉(zhuǎn)換為js對(duì)象
try {
const jsData = JSON.parse(jsonStr);
if (jsData.choices[0].delta.content) {
const content = jsData.choices[0].delta.content;
//3.4 提取出對(duì)象中content內(nèi)容,更新message
fullResponse += content;
updateMessage(fullResponse);
}
// 3.5更新token使用量
if (jsData.usage) {
updateTokenCount(jsData.usage);
}
} catch (e) {
console.error('解析JSON失敗:', e);
}
}
}
}
} catch (error) {
console.error('流處理錯(cuò)誤:', error);
throw error;
}
},更新界面
在 src/views/ChatView.vue 中,handleSend 方法調(diào)用 processStreamResponse,并通過(guò)回調(diào)函數(shù) updateMessage 和 updateTokenCount 更新界面。
const handleSend = async (content) => {
// ...
try {
const response = await chatApi.sendMessage(
messages.value.slice(0, -1).map(m => ({
role: m.role,
content: m.content
})),
settingsStore.streamResponse
);
if (settingsStore.streamResponse) {
// 這里使用了await不會(huì)將這個(gè)變化為同步嗎,我了解到的使用await后會(huì)等之后的函數(shù)調(diào)用完再執(zhí)行之后的代碼,是這樣嗎?
await messageHandler.processStreamResponse(response, {
updateMessage: (content) => chatStore.updateLastMessage(content),
updateTokenCount: (usage) => chatStore.updateTokenCount(usage)
});
}
// ...
} catch (error) {
chatStore.updateLastMessage('抱歉,發(fā)生了錯(cuò)誤,請(qǐng)稍后重試。')
} finally {
chatStore.isLoading = false
}
}2. 知識(shí)點(diǎn)
2.1. 疑問(wèn)及解答
幾個(gè)核心概念:1.ReadableStream,2.getReader()算是ReadableStream的一個(gè)方法嗎,3.reader.read(),4.Uint8Array ,5. Streams API,6.TextDecoder
(由 fetch 返回的response.body 是ReadableStream對(duì)象引申出來(lái)的問(wèn)題) fetch 處理響應(yīng)有哪些方式?類(lèi)型又是什么 ?
2.1.1. 解答的理解
有關(guān)流式涉及到的概念
ReadableStream是 StreamsAPI 的核心對(duì)象 之一,這里涉及到是因?yàn)榫W(wǎng)絡(luò)請(qǐng)求的response.body是一個(gè)ReadableStream對(duì)象。
深入補(bǔ)充:Streams API是 Web API ,用于流方式處理數(shù)據(jù) getReader() 是 ReadableStream的一個(gè)方法,(因?yàn)?1 所以response.body也有這個(gè)方法)
這個(gè)方法會(huì)返回一個(gè) reader 對(duì)象(這里是簡(jiǎn)稱),它可以逐塊讀取流中的數(shù)據(jù),提供對(duì)流的完全控制 (注:使用 getReader 后,其他地方便無(wú)法訪問(wèn)流,意思是只有它返回的 reader 對(duì)象可以訪問(wèn)流)
reader 對(duì)象有一個(gè)方法reader.read() 異步讀取流中的數(shù)據(jù)塊 。返回值如下:
{ done: true/false, value: Uint8Array | undefined }done: 如果為true,表示流已讀取完畢。value: 當(dāng)前讀取的塊數(shù)據(jù),通常是Uint8Array, 每個(gè)元素占用 1 個(gè)字節(jié) 。
Uint8Array 是一種 類(lèi)型化數(shù)組,高效地處理原始二進(jìn)制數(shù)據(jù),如文件塊、圖像、網(wǎng)絡(luò)響應(yīng) 。
對(duì)二進(jìn)制數(shù)據(jù)的處理:
- 將
Uint8Array轉(zhuǎn)換為字符串,使用TextDecoder,編碼如utf-8、utf-16。使用它的decode方法將字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符串。 - 轉(zhuǎn)換為圖像/視頻,使用
Blob,設(shè)置類(lèi)型 image 或者 video,再將生成的 blob 對(duì)象轉(zhuǎn)換為 URL,即可使用。
總結(jié)圖示:
Streams API
├── ReadableStream(response.body類(lèi)型) → 提供流式數(shù)據(jù)塊
│ ├── getReader() → 獲取 reader
│ │ └── reader.read() → 讀取單個(gè)數(shù)據(jù)塊 {done, value}
│ │
│ └── 數(shù)據(jù)塊通常是 Uint8Array 類(lèi)型
│
└── TextDecoder → 解碼 Uint8Array 為字符串
└── Blob → 解碼 Uint8Array 為圖像視頻fetch 響應(yīng)方法極其類(lèi)型
| 方法 | 返回類(lèi)型 | 常見(jiàn)場(chǎng)景 |
|---|---|---|
response.text() | Promise<string> | 文本、HTML |
response.json() | Promise<Object> | JSON API 響應(yīng) |
response.blob() | Promise<Blob> | 圖片、視頻、文件下載 |
response.arrayBuffer() | Promise<ArrayBuffer> | 二進(jìn)制數(shù)據(jù)、文件解析 |
response.formData() | Promise<FormData> | 表單響應(yīng)(少見(jiàn)) |
response.body | ReadableStream | 實(shí)時(shí)處理、進(jìn)度跟蹤 |
注意:
fetch 的響應(yīng)體只能被讀取一次 (即:不能同時(shí)調(diào)用 response.json() 和 response.text()等 )
即使響應(yīng)有錯(cuò)誤狀態(tài)(如 404),fetch 不會(huì)拋出異常,需要手動(dòng)檢查 response.ok。如下代碼處理方式:
let response = await fetch('https://example.com');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}完整代碼見(jiàn):github
到此這篇關(guān)于vue3實(shí)現(xiàn)ai聊天對(duì)話框的文章就介紹到這了,更多相關(guān)vue3 ai聊天對(duì)話框內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于Python實(shí)現(xiàn)一個(gè)具有記憶功能的AI對(duì)話代理
- Spring AI與DeepSeek實(shí)戰(zhàn)一之快速打造智能對(duì)話應(yīng)用
- 基于SpringAI+DeepSeek實(shí)現(xiàn)流式對(duì)話功能
- SpringBoot整合DeepSeek實(shí)現(xiàn)AI對(duì)話功能
- Java使用百度AI接口實(shí)現(xiàn)智能機(jī)器人對(duì)話系統(tǒng)
- 基于ChatGPT使用AI實(shí)現(xiàn)自然對(duì)話的原理分析
- AI人工智能 Python實(shí)現(xiàn)人機(jī)對(duì)話
- AI對(duì)話中的“停止生成”與“重新回答”交互邏輯和實(shí)現(xiàn)方法
相關(guān)文章
Vue3+Uniapp實(shí)現(xiàn)環(huán)境多語(yǔ)言方案完整介紹
本文主要介紹了Vue3 Uniapp環(huán)境下的多語(yǔ)言實(shí)現(xiàn)方案,核心思路是通過(guò)人為翻譯后將內(nèi)容存儲(chǔ)在 JSON 文件中,當(dāng)用戶選擇語(yǔ)言時(shí)調(diào)用對(duì)應(yīng)文件實(shí)現(xiàn)多語(yǔ)言切換,需要的小伙伴可以了解下2025-08-08
Vue組件庫(kù)ElementUI實(shí)現(xiàn)表格列表分頁(yè)效果
這篇文章主要為大家詳細(xì)介紹了Vue組件庫(kù)ElementUI實(shí)現(xiàn)表格列表分頁(yè)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06
通過(guò)vue方式實(shí)現(xiàn)二維碼掃碼功能
這篇文章給大家介紹了通過(guò)vue的方式,實(shí)現(xiàn)掃碼功能,實(shí)現(xiàn)步驟分為兩步,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2021-11-11
VUE使用DXFParser組件解析dxf文件生成圖片的操作代碼
這篇文章主要介紹了VUE使用DXFParser組件解析dxf文件生成圖片的操作代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
vue實(shí)現(xiàn)歌手列表字母排序下拉滾動(dòng)條側(cè)欄排序?qū)崟r(shí)更新
這篇文章主要介紹了vue實(shí)現(xiàn)歌手列表字母排序,下拉滾動(dòng)條側(cè)欄排序?qū)崟r(shí)更新,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-05-05
???????基于el-table和el-pagination實(shí)現(xiàn)數(shù)據(jù)的分頁(yè)效果
本文主要介紹了???????基于el-table和el-pagination實(shí)現(xiàn)數(shù)據(jù)的分頁(yè)效果,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
uni-app中使用ECharts配置四種不同的圖表(示例詳解)
在uni-app中集成ECharts可以為我們的應(yīng)用提供強(qiáng)大的圖表功能,我們?cè)敿?xì)說(shuō)一下如何在uni-app中使用ECharts,并配置四種不同的圖表,感興趣的朋友跟隨小編一起看看吧2024-01-01
vue基于pdf.js實(shí)現(xiàn)pdf文件預(yù)覽相關(guān)功能實(shí)例
在Vue項(xiàng)目開(kāi)發(fā)中,有時(shí)需要實(shí)現(xiàn)PDF預(yù)覽功能,下面這篇文章主要介紹了vue基于pdf.js實(shí)現(xiàn)pdf文件預(yù)覽相關(guān)功能的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2026-03-03
Vue使用Vue-cropper實(shí)現(xiàn)圖片裁剪
這篇文章主要為大家詳細(xì)介紹了Vue使用Vue-cropper實(shí)現(xiàn)圖片裁剪,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

