使用 Element Plus X + DeepSeek API 搭建 AI 对话应用
如果你正在寻找一种快速、优雅的方式来构建一个类似 ChatGPT 的 AI 对话界面,那么 Element Plus X 绝对值得一试。它是基于 Vue 3 和 Element Plus 的扩展组件库,专门为 AI 对话场景设计,提供了开箱即用的消息列表(BubbleList)、智能输入框(Sender)、会话管理、流式响应支持等组件。
本教程将带你一步步使用 Element Plus X 搭建一个完整的 AI 对话应用,并直接调用 DeepSeek 的开放 API(无需自己写后端)。你将在半小时内拥有一个功能完善的 AI 聊天界面,并体验流式逐字输出、Markdown 渲染等高级特性。
特别提醒:本教程为了快速演示,采用前端直接调用 DeepSeek API 的方式。这会暴露你的 API Key,存在安全风险,请勿在生产环境中使用! 生产环境建议通过后端代理转发请求。后文会给出安全最佳实践。
1. 环境准备
确保你的开发环境满足以下要求:
- Node.js 16.0 或更高版本
- 包管理工具(推荐 pnpm,也可以用 npm 或 yarn)
使用 Vite 创建一个新的 Vue 3 项目(如果你已经有现有项目,可以跳过此步):
pnpm create vite ai-chat-demo --template vue
cd ai-chat-demo
pnpm install
2. 安装 Element Plus X
Element Plus X 需要 element-plus 作为对等依赖,所以需要同时安装两者:
pnpm add element-plus vue-element-plus-x
在 main.js 中引入 Element Plus 的样式(Element Plus X 的组件已内置样式,无需单独引入):
import { createApp } from 'vue'
import App from './App.vue'
import 'element-plus/dist/index.css' // 引入 Element Plus 基础样式
createApp(App).mount('#app')
3. 基础对话界面搭建
我们将创建一个名为 ChatView.vue 的组件,它包含两个核心部分:
BubbleList:用于展示对话气泡列表Sender:用于输入和发送消息
3.1 组件结构
创建 src/components/ChatView.vue,写入以下基础模板:
<template>
<div class="chat-container">
<!-- 消息列表 -->
<BubbleList
ref="bubbleListRef"
:list="messageList"
:max-height="600"
:virtual-scroll="messageList.length > 100"
:item-size="80"
>
<template #loading>
<Thinking :loading="isLoading" :error="error" />
</template>
</BubbleList>
<!-- 输入区域 -->
<Sender
v-model="inputText"
@send="handleSend"
:allow-speech="true"
placeholder="输入消息,Enter 发送,Shift + Enter 换行"
:disabled="isLoading"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { BubbleList, Sender, Thinking } from 'vue-element-plus-x'
// 消息列表
const messageList = ref([
{
key: 1,
role: 'ai',
content: '你好!我是 DeepSeek AI 助手,有什么可以帮你的吗?',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
isMarkdown: true,
typing: true,
},
])
const inputText = ref('')
const bubbleListRef = ref(null)
const isLoading = ref(false) // 加载状态
const error = ref(null) // 错误信息
// 当前正在接收的流消息 ID
let currentAiMessageKey = null
// 处理发送消息
const handleSend = async () => {
const content = inputText.value.trim()
if (!content || isLoading.value) return
// 添加用户消息
const userMessage = {
key: Date.now(),
role: 'user',
content: content,
avatar: 'https://avatars.githubusercontent.com/u/76239030?v=4',
}
messageList.value.push(userMessage)
inputText.value = ''
// 创建一条空的 AI 消息,准备接收流式内容
currentAiMessageKey = Date.now() + 1
messageList.value.push({
key: currentAiMessageKey,
role: 'ai',
content: '',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
isMarkdown: true,
typing: true,
})
scrollToBottom()
// 开始调用 DeepSeek API
isLoading.value = true
error.value = null
try {
// 构建请求参数
const messages = messageList.value
.filter(msg => msg.key !== currentAiMessageKey) // 排除当前正在生成的这条
.map(msg => ({
role: msg.role === 'ai' ? 'assistant' : 'user',
content: msg.content
}))
// 调用 DeepSeek API(流式模式)
await fetchDeepSeekStream(messages)
} catch (err) {
console.error('API 调用失败:', err)
error.value = err.message
// 更新错误消息
const aiMessage = messageList.value.find(msg => msg.key === currentAiMessageKey)
if (aiMessage) {
aiMessage.content = `❌ 请求失败:${err.message}`
aiMessage.typing = false
}
} finally {
isLoading.value = false
}
}
// 调用 DeepSeek 流式 API
const fetchDeepSeekStream = async (messages) => {
// ⚠️ 重要提示:在生产环境中,API Key 不应直接暴露在前端
// 建议通过后端代理转发请求,或使用环境变量(但仍有风险)
const API_KEY = import.meta.env.VITE_DEEPSEEK_API_KEY || '你的API密钥'
const API_URL = 'https://api.deepseek.com/v1/chat/completions'
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'deepseek-chat', // 使用 deepseek 模型
messages: messages,
stream: true, // 启用流式输出
temperature: 0.7, // 控制随机性
max_tokens: 2000, // 最大生成长度
})
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.error?.message || `HTTP ${response.status}`)
}
// 处理流式响应
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = '' // 用于拼接不完整的 chunk
try {
while (true) {
const { value, done } = await reader.read()
if (done) break
// 解码当前 chunk
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
// 按行分割,处理 SSE 格式的数据
const lines = buffer.split('\n')
buffer = lines.pop() || '' // 保留可能不完整的最后一行
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine || !trimmedLine.startsWith('data:')) continue
const data = trimmedLine.slice(5).trim() // 去掉 "data:" 前缀
// 检查是否是结束标记
if (data === '[DONE]') {
// 流结束,关闭打字机效果
const aiMessage = messageList.value.find(msg => msg.key === currentAiMessageKey)
if (aiMessage) {
aiMessage.typing = false
}
currentAiMessageKey = null
continue
}
try {
const parsed = JSON.parse(data)
const delta = parsed.choices?.[0]?.delta?.content
if (delta) {
// 找到当前 AI 消息并追加内容
const aiMessage = messageList.value.find(msg => msg.key === currentAiMessageKey)
if (aiMessage) {
aiMessage.content += delta
scrollToBottom()
}
}
} catch (e) {
// 忽略解析错误,可能是部分数据
console.warn('解析 chunk 失败:', data)
}
}
}
} finally {
reader.releaseLock()
}
}
// 滚动到底部
const scrollToBottom = () => {
setTimeout(() => {
bubbleListRef.value?.scrollToBottom()
}, 50)
}
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
</style>
3.2 在 App.vue 中使用
修改 App.vue 让聊天组件占满视口:
<template>
<div class="app">
<ChatView />
</div>
</template>
<script setup>
import ChatView from './components/ChatView.vue'
</script>
<style>
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
}
.app {
height: 100%;
padding: 20px;
box-sizing: border-box;
}
</style>
现在运行 pnpm run dev,你应该能看到一个基本的聊天界面,可以发送消息并收到模拟回复。但我们还没有对接真实的 API,所以消息发送后会创建空白的 AI 消息并等待流式返回。接下来我们配置 DeepSeek API。
4. 获取 DeepSeek API 密钥
- 访问 DeepSeek 开放平台
- 注册/登录后,在控制台找到 API Keys 页面
- 点击 创建 API Key,复制生成的密钥(注意:密钥只在创建时显示一次,请妥善保存)
5. 配置环境变量(开发环境)
在项目根目录创建 .env 文件(或 .env.local),写入你的 API Key:
VITE_DEEPSEEK_API_KEY=sk-你的API密钥
然后在代码中通过 import.meta.env.VITE_DEEPSEEK_API_KEY 获取。注意: 以 VITE_ 开头的变量会被 Vite 注入到客户端代码中,因此仍然暴露在浏览器中。这只是为了开发方便,不要提交到公开仓库。
6. 测试对话
重新启动开发服务器,输入一条消息并发送,你应该会看到 AI 逐字输出回复,并且 Markdown 格式会被正确渲染。整个过程无需后端,完全在前端完成。
⚠️ 重要安全提醒:为什么不能在前端直接调用?
虽然上述代码可以工作,但存在严重的安全隐患:
- 所有前端代码(包括环境变量中的 API Key)都对用户可见。用户可以通过浏览器开发者工具轻松查看你的 API Key。
- 恶意用户可能盗用你的 Key 调用 DeepSeek API,造成不必要的费用消耗甚至账户风险。
因此,本教程的方法仅适用于本地开发或个人学习,绝对不要部署到公开网络!
🛡️ 生产环境最佳实践:后端代理转发
正确的做法是在你的服务器上搭建一个轻量后端(或使用无服务器函数),由后端持有 API Key 并转发请求。前端调用自己的接口,后端再调用 DeepSeek API。这样 API Key 永远不会暴露给客户端。
示例:使用 Vercel Serverless Functions 作为代理
- 在项目根目录创建
api/chat.js:
export default async function handler(req, res) {
// 只允许 POST 请求
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
const API_KEY = process.env.DEEPSEEK_API_KEY // 在 Vercel 后台设置环境变量
const API_URL = 'https://api.deepseek.com/v1/chat/completions'
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify(req.body)
})
// 设置响应头以支持流式输出
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
// 将 DeepSeek 的流式响应直接 pipe 给前端
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
res.write(chunk)
}
res.end()
} catch (error) {
res.status(500).json({ error: error.message })
}
}
-
在前端将请求地址改为
/api/chat而不是直接调用 DeepSeek 的地址。 -
在 Vercel 项目设置中添加环境变量
DEEPSEEK_API_KEY。
这样,API Key 安全地存储在服务器端,前端只需调用同域接口,安全又简单。其他平台(Netlify Functions、Cloudflare Workers 等)也有类似方案。
7. 更多增强功能
7.1 Markdown 与代码高亮
BubbleList 的 isMarkdown 属性已开启 Markdown 渲染。如果需要代码高亮,可以引入 highlight.js 或 prismjs,并在全局配置。例如:
pnpm add highlight.js
然后在 main.js 中:
import 'highlight.js/styles/github.css'
7.2 附件上传
Sender 组件支持附件上传,通过 attachments 属性开启:
<Sender
v-model="inputText"
@send="handleSend"
:attachments="true"
@attachments-change="handleAttachments"
/>
在 handleAttachments 中你可以将文件上传到自己的服务器,获取 URL 后作为消息内容的一部分发送给 AI(如果你的模型支持多模态)。
7.3 长对话性能优化
当对话历史非常长时,启用虚拟滚动可以大幅提升性能:
<BubbleList
:list="messageList"
:virtual-scroll="true"
:threshold="100"
:item-size="80"
/>
item-size 是每个消息项的预估高度,用于计算滚动条。如果你的消息高度变化较大,可能需要使用动态高度模式。
8. 部署你的 AI 对话应用(安全版)
按照上述后端代理方案,你可以将项目部署到 Vercel、Netlify 等平台。构建命令仍然是:
pnpm run build
部署时记得设置环境变量 DEEPSEEK_API_KEY。
9. 总结
通过本教程,你学会了:
- 如何使用 Element Plus X 快速搭建 AI 对话界面
- 如何调用 DeepSeek API 实现流式对话
- 为什么不能在前端暴露 API Key,以及如何通过后端代理安全地调用
Element Plus X 还提供了更多高级组件(会话管理、提示词、思维链等),你可以根据需求进一步扩展。希望这篇教程能帮助你快速上手 AI 对话应用的开发!
如果你在实践中遇到问题,欢迎在评论区留言讨论。Happy coding! 🚀
本文来自博客园,作者:战立标,转载请注明原文链接:https://www.cnblogs.com/zhanlibiao/p/19703439

浙公网安备 33010602011771号