Vue.js前端项目调用SmallThinker-3B-Preview API实现智能对话界面
Vue.js前端项目调用SmallThinker-3B-Preview API实现智能对话界面最近在做一个内部工具需要集成一个智能对话助手。后端同事已经用SmallThinker-3B-Preview模型搭好了服务接下来就是我的任务了在前端用Vue.js做一个好看又好用的聊天界面。这活儿听起来简单不就是发个请求、收个回复、显示出来嘛。但真做起来你会发现要处理不少细节怎么让对话历史有条理怎么让AI的回复像真人打字一样逐字显示怎么处理网络请求的各种状态今天我就把整个实现过程梳理一下如果你也在做类似的功能希望能给你一些参考。1. 项目准备与环境搭建首先我们得把项目架子搭起来。我习惯用Vue 3它的Composition API写起来更灵活。如果你还在用Vue 2大部分思路也是相通的只是语法上有些区别。1.1 创建Vue项目打开终端用Vue CLI或者Vite创建一个新项目。我更喜欢Vite速度快配置简单。npm create vuelatest创建过程中按需选择需要的功能。对于这个聊天界面我们至少需要Vue Router如果应用有多个页面Pinia状态管理管理对话历史很方便TypeScript可选但用了之后代码提示更友好项目创建好后安装我们需要的几个核心依赖npm install axiosAxios是用来发HTTP请求的比原生的fetch用起来顺手一些拦截器、错误处理这些功能都封装好了。1.2 理解后端API接口在动手写代码之前得先和后端同事确认好API怎么调用。一般来说像SmallThinker-3B-Preview这样的模型服务会提供一个对话接口。假设后端给的接口文档是这样的接口地址http://your-api-server/v1/chat/completions请求方法POST请求头需要包含Content-Type: application/json可能还需要认证的Authorization头请求体一个JSON对象至少包含messages数组每个消息对象有roleuser或assistant和content属性。响应通常是一个JSON包含AI的回复。如果支持流式输出响应可能是一个text/event-stream流。关键是要确认API是否支持流式响应。如果支持我们就能实现那种逐字打印的效果体验会好很多。如果不支持那就一次性返回完整回复显示起来简单但少了点交互感。2. 构建核心聊天组件项目架子搭好API也清楚了接下来就是写核心的聊天界面了。我把它拆成了几个部分显示消息的列表、输入消息的框、以及发送按钮。2.1 设计消息列表组件消息列表要能清晰区分用户和AI的发言并且能流畅地展示AI的流式回复。我们先来定义一下消息的数据结构。在src/types/chat.ts如果用TypeScript或者直接在一个JS文件里// 定义单条消息的类型 export interface ChatMessage { id: string | number // 消息唯一标识 role: user | assistant // 发送者角色 content: string // 消息内容 timestamp?: number // 时间戳可选 isLoading?: boolean // 是否正在加载用于AI回复时 } // 定义整个对话的状态 export interface ChatState { messages: ChatMessage[] currentInput: string isWaitingForResponse: boolean }然后我们创建一个MessageList.vue组件来展示这些消息template div classmessage-list div v-formessage in messages :keymessage.id :class[message-item, message-${message.role}] !-- 消息头像 -- div classmessage-avatar img v-ifmessage.role user src/assets/user-avatar.png alt用户 div v-else classai-avatarAI/div /div !-- 消息内容区域 -- div classmessage-content-wrapper !-- 发送者名称 -- div classmessage-sender {{ message.role user ? 我 : SmallThinker }} /div !-- 消息内容 -- div classmessage-content !-- 如果是AI消息且正在加载显示加载动画 -- template v-ifmessage.role assistant message.isLoading div classtyping-indicator span/spanspan/spanspan/span /div /template template v-else !-- 这里用v-html是为了简单处理换行实际生产环境要注意XSS防护 -- div v-htmlformatMessageContent(message.content)/div /template /div !-- 消息时间 -- div v-ifmessage.timestamp classmessage-time {{ formatTime(message.timestamp) }} /div /div /div /div /template script setup langts import { defineProps } from vue import type { ChatMessage } from /types/chat const props defineProps{ messages: ChatMessage[] }() // 格式化消息内容将换行符转换为br const formatMessageContent (content: string) { return content.replace(/\n/g, br) } // 格式化时间显示 const formatTime (timestamp: number) { return new Date(timestamp).toLocaleTimeString([], { hour: 2-digit, minute: 2-digit }) } /script style scoped .message-list { padding: 20px; flex: 1; overflow-y: auto; } .message-item { display: flex; margin-bottom: 20px; animation: fadeIn 0.3s ease; } .message-user { flex-direction: row-reverse; } .message-avatar { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; margin: 0 12px; } .message-user .message-avatar { margin-left: 12px; margin-right: 0; } .ai-avatar { width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; } .message-content-wrapper { max-width: 70%; } .message-user .message-content-wrapper { align-items: flex-end; } .message-sender { font-size: 0.85rem; color: #666; margin-bottom: 4px; } .message-content { padding: 12px 16px; border-radius: 18px; line-height: 1.5; } .message-user .message-content { background-color: #007aff; color: white; border-bottom-right-radius: 4px; } .message-assistant .message-content { background-color: #f0f0f0; color: #333; border-bottom-left-radius: 4px; } /* 打字动画 */ .typing-indicator { display: flex; padding: 12px 16px; } .typing-indicator span { height: 8px; width: 8px; background: #999; border-radius: 50%; margin: 0 2px; animation: bounce 1.4s infinite ease-in-out; } .typing-indicator span:nth-child(1) { animation-delay: -0.32s; } .typing-indicator span:nth-child(2) { animation-delay: -0.16s; } keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /style这个组件负责把消息列表漂亮地展示出来用户消息在右AI消息在左还有加载动画体验就比较完整了。2.2 实现输入框与发送组件用户输入消息的组件要处理文本输入、发送消息还要考虑一些用户体验细节比如按Enter发送、CtrlEnter换行。创建MessageInput.vuetemplate div classmessage-input-container div classinput-wrapper textarea reftextareaRef v-modelinputText :placeholderplaceholder keydown.enter.exact.preventhandleSend keydown.ctrl.enter.exacthandleNewLine inputhandleInput rows1 classmessage-textarea /textarea button clickhandleSend :disabled!canSend classsend-button svg v-ifisLoading classloading-icon viewBox0 0 24 24 circle cx12 cy12 r10 strokecurrentColor fillnone stroke-width4/ /svg span v-else发送/span /button /div !-- 一些操作提示 -- div classinput-hints span classhint-text按 Enter 发送Ctrl Enter 换行/span /div /div /template script setup langts import { ref, computed, nextTick } from vue const props defineProps{ isLoading?: boolean placeholder?: string }() const emit defineEmits{ send: [message: string] }() const inputText ref() const textareaRef refHTMLTextAreaElement() // 计算是否可以发送消息 const canSend computed(() { return inputText.value.trim().length 0 !props.isLoading }) // 处理发送 const handleSend () { if (!canSend.value) return const message inputText.value.trim() emit(send, message) inputText.value // 发送后自动调整输入框高度 nextTick(() { adjustTextareaHeight() }) } // 处理CtrlEnter换行 const handleNewLine () { const textarea textareaRef.value if (!textarea) return const cursorPos textarea.selectionStart const textBefore inputText.value.substring(0, cursorPos) const textAfter inputText.value.substring(cursorPos) inputText.value textBefore \n textAfter // 移动光标到新位置 nextTick(() { textarea.selectionStart textarea.selectionEnd cursorPos 1 adjustTextareaHeight() }) } // 自动调整输入框高度 const adjustTextareaHeight () { const textarea textareaRef.value if (!textarea) return // 重置高度然后根据内容调整 textarea.style.height auto const newHeight Math.min(textarea.scrollHeight, 120) // 最大高度120px textarea.style.height newHeight px } // 输入时调整高度 const handleInput () { adjustTextareaHeight() } /script style scoped .message-input-container { border-top: 1px solid #e0e0e0; padding: 16px; background: white; } .input-wrapper { display: flex; gap: 12px; align-items: flex-end; } .message-textarea { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 24px; resize: none; font-family: inherit; font-size: 16px; line-height: 1.5; max-height: 120px; transition: border-color 0.2s; } .message-textarea:focus { outline: none; border-color: #007aff; } .send-button { padding: 12px 24px; background: #007aff; color: white; border: none; border-radius: 24px; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; min-width: 80px; height: 48px; } .send-button:hover:not(:disabled) { background: #0056cc; } .send-button:disabled { background: #ccc; cursor: not-allowed; } .loading-icon { width: 20px; height: 20px; animation: rotate 1s linear infinite; } keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .input-hints { margin-top: 8px; text-align: center; } .hint-text { font-size: 12px; color: #999; } /style这个输入组件考虑了用户体验的细节自适应高度的文本域、快捷键支持、发送按钮的状态反馈。用户用起来会感觉比较顺手。3. 集成API与状态管理组件准备好了接下来要把它们连接起来处理真正的AI对话逻辑。这里涉及到发送请求、接收响应、管理对话状态。3.1 创建API服务层我们先创建一个专门处理API调用的服务文件这样业务逻辑和API调用就分开了以后要改接口也方便。在src/services/chatService.ts中import axios, { AxiosInstance, AxiosResponse } from axios import type { ChatMessage } from /types/chat // 创建axios实例 const apiClient: AxiosInstance axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || http://localhost:3000/api, timeout: 30000, // 30秒超时 headers: { Content-Type: application/json, }, }) // 请求拦截器可以在这里添加认证token等 apiClient.interceptors.request.use( (config) { // 如果有token添加到请求头 const token localStorage.getItem(auth_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器处理通用错误 apiClient.interceptors.response.use( (response) response, (error) { console.error(API请求错误:, error) // 根据错误状态码进行统一处理 if (error.response) { switch (error.response.status) { case 401: console.error(认证失败请重新登录) break case 403: console.error(没有访问权限) break case 429: console.error(请求过于频繁请稍后再试) break case 500: console.error(服务器内部错误) break default: console.error(请求失败: ${error.response.status}) } } else if (error.request) { console.error(网络错误请检查网络连接) } else { console.error(请求配置错误:, error.message) } return Promise.reject(error) } ) // 定义API请求参数类型 interface ChatCompletionRequest { messages: Array{ role: user | assistant content: string } stream?: boolean max_tokens?: number temperature?: number } // 普通请求一次性返回完整回复 export const sendChatMessage async ( messages: ChatMessage[], options?: { max_tokens?: number temperature?: number } ): Promisestring { try { const requestData: ChatCompletionRequest { messages: messages.map(msg ({ role: msg.role, content: msg.content })), stream: false, max_tokens: options?.max_tokens || 1000, temperature: options?.temperature || 0.7, } const response: AxiosResponse{ choices: Array{ message: { content: string } } } await apiClient.post(/v1/chat/completions, requestData) if (response.data.choices response.data.choices.length 0) { return response.data.choices[0].message.content } throw new Error(API返回格式异常) } catch (error) { console.error(发送消息失败:, error) throw error } } // 流式请求逐字返回 export const sendChatMessageStream async ( messages: ChatMessage[], onChunk: (chunk: string) void, onComplete: () void, onError: (error: Error) void, options?: { max_tokens?: number temperature?: number } ): Promisevoid { try { const requestData: ChatCompletionRequest { messages: messages.map(msg ({ role: msg.role, content: msg.content })), stream: true, max_tokens: options?.max_tokens || 1000, temperature: options?.temperature || 0.7, } const response await fetch( ${apiClient.defaults.baseURL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, ...(apiClient.defaults.headers.common as Recordstring, string), }, body: JSON.stringify(requestData), } ) if (!response.ok) { throw new Error(HTTP错误: ${response.status}) } if (!response.body) { throw new Error(响应体为空) } const reader response.body.getReader() const decoder new TextDecoder() let buffer while (true) { const { done, value } await reader.read() if (done) { onComplete() break } buffer decoder.decode(value, { stream: true }) // 处理SSE格式的数据 const lines buffer.split(\n) buffer lines.pop() || // 最后一行可能不完整留到下次处理 for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6) // 去掉data: 前缀 if (data [DONE]) { onComplete() return } try { const parsed JSON.parse(data) const content parsed.choices?.[0]?.delta?.content if (content) { onChunk(content) } } catch (e) { console.warn(解析流数据失败:, e) } } } } } catch (error) { console.error(流式请求失败:, error) onError(error as Error) } }这个服务层做了几件事创建了配置好的axios实例、定义了两种请求方式普通和流式、处理了错误和响应。流式请求那里稍微复杂点需要处理Server-Sent EventsSSE格式的数据。3.2 使用Pinia管理对话状态对于聊天应用状态管理很重要。我们需要管理消息列表、当前输入、加载状态等。用Pinia来做这个事很合适。创建src/stores/chatStore.tsimport { defineStore } from pinia import { ref, computed } from vue import type { ChatMessage, ChatState } from /types/chat import { sendChatMessage, sendChatMessageStream } from /services/chatService export const useChatStore defineStore(chat, () { // 状态 const messages refChatMessage[]([]) const currentInput ref() const isWaitingForResponse ref(false) const currentStreamController refAbortController | null(null) // 计算属性 const canSend computed(() { return currentInput.value.trim().length 0 !isWaitingForResponse.value }) // 添加消息 const addMessage (message: OmitChatMessage, id) { const newMessage: ChatMessage { ...message, id: Date.now() Math.random(), // 简单生成唯一ID timestamp: message.timestamp || Date.now(), } messages.value.push(newMessage) } // 更新最后一条消息用于流式响应 const updateLastMessage (content: string) { if (messages.value.length 0) return const lastMessage messages.value[messages.value.length - 1] if (lastMessage.role assistant) { lastMessage.content content lastMessage.isLoading false } } // 设置最后一条消息加载状态 const setLastMessageLoading (isLoading: boolean) { if (messages.value.length 0) return const lastMessage messages.value[messages.value.length - 1] if (lastMessage.role assistant) { lastMessage.isLoading isLoading } } // 发送消息普通方式 const sendMessage async () { if (!canSend.value) return const userMessage currentInput.value.trim() currentInput.value // 添加用户消息 addMessage({ role: user, content: userMessage, }) // 添加AI消息占位 addMessage({ role: assistant, content: , isLoading: true, }) isWaitingForResponse.value true try { const response await sendChatMessage(messages.value) // 更新AI消息 const lastMessage messages.value[messages.value.length - 1] lastMessage.content response lastMessage.isLoading false } catch (error) { console.error(发送消息失败:, error) // 出错时显示错误信息 const lastMessage messages.value[messages.value.length - 1] lastMessage.content 抱歉我遇到了一些问题请稍后再试。 lastMessage.isLoading false } finally { isWaitingForResponse.value false } } // 发送消息流式方式 const sendMessageStream async () { if (!canSend.value) return const userMessage currentInput.value.trim() currentInput.value // 添加用户消息 addMessage({ role: user, content: userMessage, }) // 添加AI消息占位 addMessage({ role: assistant, content: , isLoading: true, }) isWaitingForResponse.value true // 创建AbortController用于取消请求 const controller new AbortController() currentStreamController.value controller try { await sendChatMessageStream( messages.value.slice(0, -1), // 不包含刚添加的AI消息 (chunk) { // 收到数据块更新最后一条消息 updateLastMessage(chunk) }, () { // 流式传输完成 setLastMessageLoading(false) isWaitingForResponse.value false currentStreamController.value null }, (error) { // 出错处理 console.error(流式请求失败:, error) const lastMessage messages.value[messages.value.length - 1] if (lastMessage.content ) { lastMessage.content 抱歉我遇到了一些问题请稍后再试。 } lastMessage.isLoading false isWaitingForResponse.value false currentStreamController.value null } ) } catch (error) { console.error(发送消息失败:, error) isWaitingForResponse.value false currentStreamController.value null } } // 取消当前请求 const cancelCurrentRequest () { if (currentStreamController.value) { currentStreamController.value.abort() currentStreamController.value null } isWaitingForResponse.value false // 如果AI消息还是空的移除它 const lastMessage messages.value[messages.value.length - 1] if (lastMessage.role assistant lastMessage.content ) { messages.value.pop() } else { lastMessage.isLoading false } } // 清空对话 const clearConversation () { messages.value [] currentInput.value isWaitingForResponse.value false if (currentStreamController.value) { currentStreamController.value.abort() currentStreamController.value null } } // 导出状态和方法 return { // 状态 messages, currentInput, isWaitingForResponse, // 计算属性 canSend, // 方法 addMessage, sendMessage, sendMessageStream, cancelCurrentRequest, clearConversation, } })这个store管理了所有的聊天状态和逻辑。它提供了两种发送方式流式和非流式还处理了取消请求、清空对话这些功能。用Pinia的好处是状态响应式在任何组件里都能方便地访问和修改。4. 整合与优化现在各个部分都准备好了我们把它们整合到主组件里再加点优化功能。4.1 创建主聊天界面创建ChatInterface.vue作为主组件template div classchat-container !-- 顶部标题栏 -- div classchat-header h1SmallThinker智能对话/h1 div classheader-actions button clickclearChat classaction-button :disabledstore.isWaitingForResponse 清空对话 /button button v-ifstore.isWaitingForResponse clickcancelRequest classaction-button cancel-button 停止生成 /button /div /div !-- 消息列表 -- div classmessages-container MessageList :messagesstore.messages / !-- 空状态提示 -- div v-ifstore.messages.length 0 classempty-state div classempty-icon/div h3开始对话吧/h3 p输入你的问题SmallThinker会为你解答/p /div /div !-- 输入区域 -- div classinput-container MessageInput v-modelstore.currentInput :is-loadingstore.isWaitingForResponse placeholder输入你的消息... sendhandleSendMessage / /div /div /template script setup langts import { onMounted, onUnmounted } from vue import MessageList from /components/MessageList.vue import MessageInput from /components/MessageInput.vue import { useChatStore } from /stores/chatStore const store useChatStore() // 发送消息 const handleSendMessage (message: string) { // 使用流式响应体验更好 store.sendMessageStream() } // 清空对话 const clearChat () { if (confirm(确定要清空对话历史吗)) { store.clearConversation() } } // 取消当前请求 const cancelRequest () { store.cancelCurrentRequest() } // 页面加载时可以加载历史记录 onMounted(() { // 这里可以添加加载历史对话的逻辑 // 比如从localStorage或后端加载 }) // 页面卸载时清理资源 onUnmounted(() { if (store.isWaitingForResponse) { store.cancelCurrentRequest() } }) /script style scoped .chat-container { display: flex; flex-direction: column; height: 100vh; max-width: 800px; margin: 0 auto; background: white; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); } .chat-header { padding: 16px 24px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; background: white; } .chat-header h1 { margin: 0; font-size: 1.5rem; color: #333; } .header-actions { display: flex; gap: 12px; } .action-button { padding: 8px 16px; border: 1px solid #ddd; border-radius: 6px; background: white; color: #666; cursor: pointer; font-size: 0.9rem; transition: all 0.2s; } .action-button:hover:not(:disabled) { background: #f5f5f5; border-color: #ccc; } .action-button:disabled { opacity: 0.5; cursor: not-allowed; } .cancel-button { border-color: #ff3b30; color: #ff3b30; } .cancel-button:hover:not(:disabled) { background: #ff3b30; color: white; } .messages-container { flex: 1; overflow-y: auto; padding: 20px; position: relative; } .empty-state { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #999; } .empty-icon { font-size: 48px; margin-bottom: 16px; } .empty-state h3 { margin: 0 0 8px 0; color: #666; } .empty-state p { margin: 0; font-size: 0.9rem; } .input-container { border-top: 1px solid #e0e0e0; } /style4.2 添加一些优化功能为了让体验更好我们可以再加几个小功能1. 对话历史持久化用户刷新页面后之前的对话还能看到。在chatStore里添加// 保存对话到localStorage const saveToLocalStorage () { const saveData { messages: messages.value, timestamp: Date.now(), } localStorage.setItem(chat_history, JSON.stringify(saveData)) } // 从localStorage加载对话 const loadFromLocalStorage () { const saved localStorage.getItem(chat_history) if (saved) { try { const data JSON.parse(saved) // 可以加个过期时间比如只加载24小时内的记录 const oneDay 24 * 60 * 60 * 1000 if (Date.now() - data.timestamp oneDay) { messages.value data.messages } } catch (e) { console.error(加载历史记录失败:, e) } } } // 在addMessage方法里自动保存 const addMessage (message: OmitChatMessage, id) { const newMessage: ChatMessage { ...message, id: Date.now() Math.random(), timestamp: message.timestamp || Date.now(), } messages.value.push(newMessage) saveToLocalStorage() // 每次添加消息都保存 }2. 自动滚动到底部新消息来了自动滚动到最新消息。在MessageList组件里script setup langts import { defineProps, ref, watch, nextTick } from vue import type { ChatMessage } from /types/chat const props defineProps{ messages: ChatMessage[] }() const messagesContainer refHTMLElement() // 监听消息变化自动滚动到底部 watch(() props.messages, () { nextTick(() { scrollToBottom() }) }, { deep: true }) const scrollToBottom () { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight } } /script template div refmessagesContainer classmessage-list !-- ... 原有内容 ... -- /div /template3. 添加消息复制功能让用户可以方便地复制AI的回复。在MessageList组件里给AI消息添加复制按钮template div classmessage-item message-assistant !-- ... 头像和内容 ... -- div classmessage-actions button clickcopyMessage(message.content) classaction-button 复制 /button /div /div /template script setup langts const copyMessage async (content: string) { try { await navigator.clipboard.writeText(content) // 可以加个提示比如Toast console.log(已复制到剪贴板) } catch (err) { console.error(复制失败:, err) } } /script5. 实际使用与调试建议功能都实现了但在实际用的时候可能会遇到一些问题。这里分享几个调试和优化的经验。5.1 处理跨域问题如果你的前端和后端不在同一个域名下会遇到跨域问题。后端需要配置CORS跨域资源共享。如果你控制后端可以这样配置以Node.js Express为例const cors require(cors) app.use(cors({ origin: http://localhost:5173, // 你的前端地址 credentials: true, methods: [GET, POST, PUT, DELETE, OPTIONS], allowedHeaders: [Content-Type, Authorization] }))如果后端不支持CORS开发时可以在Vite配置里设置代理// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://your-api-server, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })5.2 流式响应的兼容性流式响应SSE在现代浏览器上支持都不错但要注意确保后端正确设置了Content-Type: text/event-stream如果用了Nginx等反向代理可能需要额外配置来支持流式传输考虑降级方案如果流式请求失败可以自动切换到普通请求5.3 性能优化建议虚拟滚动如果消息非常多比如上千条考虑用虚拟滚动只渲染可视区域的消息。图片懒加载如果消息里有图片用懒加载。请求防抖快速连续发送消息时可以加个防抖。本地缓存除了对话历史还可以缓存一些常见的回复。5.4 错误处理与用户体验网络错误提示给用户友好的错误提示而不是控制台错误。重试机制请求失败时可以让用户一键重试。加载状态除了按钮禁用还可以在页面顶部加个加载条。超时处理设置合理的超时时间超时后给提示。6. 总结整个做下来用Vue.js集成AI对话API其实不算复杂但要把体验做好确实需要花些心思。从最基础的组件搭建到状态管理再到流式响应的处理每一步都有不少细节可以优化。流式响应那个逐字显示的效果对用户体验提升挺明显的用户能实时看到AI的思考过程感觉更像是在和真人对话。状态管理用Pinia也很合适逻辑清晰维护起来方便。实际用的时候可能会根据需求再加些功能比如对话导出、多种AI模型切换、对话模板等等。但核心的框架就是这样了在这个基础上扩展都比较容易。如果你也在做类似的功能建议先从最简单的版本开始把核心流程跑通然后再慢慢加功能、优化体验。遇到问题多看看浏览器的开发者工具网络请求、控制台错误这些信息都很有帮助。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。