整理不易,請不要吝嗇你的贊和收藏。
1. 前言
這篇文章是 Spring AI Q&A 系統的前端實現。這篇文章將介紹如何快速搭建一個基于 vue3 + ElementPlus 的前端項目,vue3 項目的目錄結構介紹,如何在前端實現流式響應,如何高亮顯示代碼等。
效果展示:
2. 前提條件
-
后端實現:
【Spring AI】基于SpringAI+Vue3+ElementPlus的Q&A系統實現(后端)-CSDN博客文章瀏覽閱讀762次,點贊26次,收藏24次。這篇文章將介紹如何基于 RAG 技術,使用 SpringAI + Vue3 + ElementPlus 實現一個 Q&A 系統。本文使用 deepseek 的 DeepSeek-V3 作為聊天模型,使用阿里百煉的 text-embedding-v3 作為向量模型,使用 redis 作為向量庫。(PS:近期阿里百煉也上架了 DeepSeek-V3 和 DeepSeek-R1 模型供開發者調用,如果覺得 DeepSeek 官方 AP I比較慢的話,可以去試試)。https://blog.csdn.net/u013176571/article/details/145368559
-
已安裝 18.3 或更高版本的?Node.js ,未安裝進入 官網下載 ,LTS 為長期支持版,Current 為最新功能版。
3. 快速搭建 Elemenet-Plus 項目
我這里直接使用官網的快速搭建模板 element-plus-vite-starter ,其它方式參考 Element-Plus 官網 。
3.1 項目下載
# 下載模板
git clone https://github.com/element-plus/element-plus-vite-starter.git
# 進入項目,安裝依賴包
npm install
3.2 項目結構介紹
VS Code 中引入項目,項目結構如下:
項目結構介紹:
element-plus-vite-starter/
├── node_modules/ # 存放所有安裝的依賴包
├── public/ # 靜態資源文件夾
│ └── favicon.svg # 默認的站點圖標
├── src/ # 源代碼文件夾
│ ├── assets/ # 靜態資源(如圖片、樣式等)
│ ├── components/ # Vue 組件文件夾
│ ├──── layouts/
│ ├────── BaseHeader.vue # 頂部導航欄布局
│ ├────── BaseSide.vue # 側邊導航欄布局
│ ├── composables/ # Vue 3 Composition API 的邏輯復用文件夾,存放 useXXX 命名的函數
│ ├── pages/ # 頁面視圖文件夾
│ ├── styles/ # 樣式文件夾,存放全局樣式 css 類
│ ├──── element/
│ ├────── index.scss # 全局顏色主題配置文件
│ ├── App.vue # 根組件
│ ├── main.js # 項目入口文件
│ ├── components.d.ts # 組件的類型聲明文件
│ ├── env.d.ts # 環境變量的類型聲明文件
│ ├── typed-router.d.ts # 路由的類型聲明文件
│ └── types.ts # 全局類型定義文件,用于定義 TypeScript 類型
├── .gitignore
├── eslint.config.ts # ESLint 配置文件,用于代碼質量檢查
├── index.html # 項目入口 HTML 文件,Vite 會以此文件為模板進行開發和構建
├── babel.config.js # Babel 配置文件(僅在 Vue CLI 項目中)
├── package-lock.json # npm 生成的鎖定文件,確保依賴版本一致
├── package.json # 項目依賴和配置信息
├── pnpm-lock.yaml # pnpm 生成的鎖定文件,確保依賴版本一致
├── README.md # 項目說明文檔
├── tsconfig.json # TypeScript 配置文件,定義 TypeScript 編譯選項
├── uno.config.ts # UnoCSS 配置文件,UnoCSS 是一個用于生成原子 CSS 的工具
└── vite.config.ts # Vite 配置文件(僅在 Vite 項目中)
3.3 啟動項目
執行以下命令啟動:
# 啟動項目
npm run dev
瀏覽器訪問:http://localhost:5173/
4. 頁面開發
這篇文章不會提供所有前端代碼,我會在主要的卡點提供相應的代碼節選。
4.1 .vue 文件介紹
在 Vue3 中,我們通過創建一個 .vue 格式文件來創建頁面,一個 .vue 文件由以下幾個部分組成:
-
template:組件的模板部分,用于定義組件的 HTML 結構。Vue 會將模板編譯為渲染函數,用于生成最終的 DOM。
-
script:組件的邏輯部分,用于定義組件的數據、方法、生命周期鉤子等。
-
style:組件的樣式部分,用于定義組件的 CSS 樣式,支持 CSS、SCSS、Sass、Less 等。
樣例:
<template><div class = "m-container" ><h1 class = "m-title" >{{ title }}</h1><button @click = "handleClick" >@click 用來綁定點擊事件</button></div>
</template>// lang="ts" 表示標簽中的代碼是用 TypeScript 編寫的
<script lang="ts" setup>
import { onMounted, reactive, toRefs } from "vue";// 定義變量
const state = reactive({title: "頁面1",
});
// 用于將響應式對象中的屬性轉換為響應式引用(ref)
const { title } = toRefs(state)/*** 頁面加載事件*/
onMounted(() => {
});/*** 綁定事件*/
const handleClick = () => {// 變量賦值title.value = "頁面2"
}
</script>// lang="less" 表示使用 less 語法,scoped 用來限制作用域,只對當前組件模板生效
<style lang="less" scoped>
.m-container{.m-title{ }
}
</style>
4.2 網絡請求工具類
在 src 目錄下創建一個 utils 文件夾,然后創建一個 api.ts 文件,用于發起網絡請求。
4.2.1 代碼
import axios from 'axios';
import { ElMessage } from 'element-plus';// 創建一個 axios 實例
const baseURL = 'http://127.0.0.1:8082/your_service_name';
const apiClient = axios.create({baseURL: baseURL,timeout: 200000, // 請求超時時間headers: {'Content-Type': 'application/json',},
});const err = (error) => {console.log('error', error)if (axios.isCancel(error)) {return}if (!error.response) {ElMessage.error({message: '請求超時請檢查網絡鏈接!',offset: 80,})return Promise.reject(error)}const data = error.response.dataif (!data || data.code !== 200) {ElMessage.error(data.message || "發生未知錯誤,請稍后再試!")}return Promise.reject(error)
}// 請求攔截器
apiClient.interceptors.request.use((config) => {// 在發送請求之前做些什么,例如添加 tokenconst token = localStorage.getItem('token'); if (token) {config.headers['Authorization'] = `Bearer ${token}`;}return config;},err
);// 響應攔截器
apiClient.interceptors.response.use((response) => {// 對響應數據做點什么return response.data;},err
);// 定義 API 請求方法
const api = {getBaseUrl() {return baseURL;},get(url, params) {return apiClient.get(url, { params });},post(url, params, config) {return apiClient.post(url, params, config);},
};
export default api;
4.2.2 如何引用?
在需要引用的頁面的 script 標簽中 或 .ts 文件下鍵入:
import api from '@/utils/api';
4.2.3 如何調用?
const param = {};
const config = {headers: {'Content-Type': 'multipart/form-data',},
};
api.post("/ai/chat/fileUploadWithRag",param).then((response) => {console.log("Response received:", response);}).catch((error) => {// 處理錯誤console.error("請求失敗:", error);});
4.3 如何接收 SSE 響應式消息
SSE (Server-Sent Events)是一種允許服務器向客戶端推送數據的技術,屬于 HTML5 的一部分。它支持服務器向客戶端的單向通信,客戶端通過一次長連接持續接收服務器推送的數據。響應式編程(Reactive Programming)非常適合實現 SSE,因為它允許以非阻塞的方式持續推送數據,不會阻塞服務器資源。在 SpringBoot 中可以使用 Spring WebFlux 框架來實現 SSE。在 Web 端實現 SSE 通常使用 EventSource?對象。
4.3.1 代碼
同樣在 utils 目錄下,創建一個 sse.ts 的工具類。
class SSE {private eventSource: EventSource | null = null;/*** 連接 SSE* @param url* @param onMessage * @param onError */public connect(url: string, onMessage: (event: MessageEvent) => void, onError?: (event: Event) => void): void {this.eventSource = new EventSource(url);// 監聽消息事件this.eventSource.onmessage = onMessage;// 監聽錯誤事件if (onError) {this.eventSource.onerror = onError;}}/*** 關閉 SSE 連接*/public close(): void {if (this.eventSource) {this.eventSource.close();this.eventSource = null;}}}export default SSE;
4.3.2 如何引用?
import SSE from "@/utils/sse";
4.3.3 如何調用
需要注意的是,SSE 僅支持 GET 調用。
const state = reactive({sse: new SSE(),
});
const { sse } = toRefs(state);// 接收消息的回調函數
const handleMessage = (event: MessageEvent) => {eventMessage = eventMessage.concat(event.data);
};
// 錯誤處理的回調函數
const handleError = (event: Event) => {stopGenerate();console.log('SSE 連接錯誤:', event);
};
const url = "接口地址?param1=param1¶m2=param2"
sse.value.connect(url, handleMessage, handleError);
4.4 對話組件
template 中主要代碼節選,通過 message.sender 的值加載用戶和AI消息的樣式。
<el-scrollbar ref="scrollbar" class="message-list" always @scroll="handleScroll"><div v-for="message in messages" :key="message.id" class="message-wrapper" :class="{'user-message': message.sender === 'user','bot-message': message.sender === 'bot',}"><div class="message-bubble"><div class="message-content" v-html="message.content"></div></div></div>
</el-scrollbar>
script 中主要代碼節選:
// 定義一個 Message 接口
interface Message {id: number;content: string;sender: "user" | "bot";timestamp: number;
}// 定義變量
const state = reactive({messages: [] as Message[],
});
const { messages } = toRefs(state);
4.5 如何加載 Markdown 內容
由于大模型返回的流一般為 Markdown 格式,我們使用第三方庫 markdown-it 來加載內容。
4.5.1 安裝 markdown-it
由于我需要支持代碼高亮、數學公式、流程圖等,所以我安裝了額外的插件來擴展 MarkdownIt 的功能。
npm install markdown-it highlight.js katex mermaid markdown-it-sub markdown-it-sup markdown-it-emoji markdown-it-task-lists markdown-it-footnote markdown-it-deflist markdown-it-abbr markdown-it-ins markdown-it-mark
4.5.2 如何使用
template 中代碼節選:
<template><div class="message-content" v-html="message.content"></div>
</template>
script 中代碼節選:
// 引入 markdown-it
import MarkdownIt from "markdown-it";
// 定義MarkdownIt對象
const md = new MarkdownIt();// SSE 接收消息的回調函數
const handleMessage = (event: MessageEvent) => {eventMessage = eventMessage.concat(event.data);botMessage.content = computed(() => {return md.value.render(eventMessage);});// 設置nextTick(() => {scrollToBottom();});
};
4.6 其它
4.6.1 上傳組件
使用 ElementPlus 的 upload 組件。
4.6.2 使用 Alt + Enter 鍵換行
Input 默認的換行快捷鍵為 Shift + Enter,不符合我們平時的使用習慣。
const handleKeyDown = (event: KeyboardEvent) => {if (event.key === "Enter") {// 按下 Alt + Enter 鍵時,插入換行符if (event.altKey) {const cursorPosition = event.target.selectionStart;const textBeforeCursor = inputMessage.value.slice(0, cursorPosition);const textAfterCursor = inputMessage.value.slice(cursorPosition);inputMessage.value = textBeforeCursor + '\n' + textAfterCursor;event.target.selectionStart = cursorPosition + 1;event.target.selectionEnd = cursorPosition + 1;return;}event.preventDefault();sendMessage();}
};
4.6.3 消息自動滾動到最下方
代碼節選:
// 定義變量
const state = reactive({autoScroll: true,scrollbar: {} as any,
});
const { autoScroll, scrollbar } = toRefs(state);/*** 消息滾動事件監聽*/
const handleScroll = ({ scrollTop }: { scrollTop: number }) => {const scrollHeight = scrollbar.value.wrapRef?.scrollHeight || 0;const clientHeight = scrollbar.value.wrapRef?.clientHeight || 0;autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10;
};/*** 滾動到最下方*/
const scrollToBottom = () => {if (autoScroll.value) {nextTick(() => {scrollbar.value?.setScrollTop(scrollbar.value.wrapRef?.scrollHeight);});}
};
5. 參考文檔
- Vue3 文檔
- Element Plus 文檔
- markdown-it 文檔