Vue3 对接 DeepSeek
1. API 密钥获取
- 注册 DeepSeek 账号并创建 API Key(官网入口)
- 建议将密钥存储在环境变量中(如
.env.local
):
VITE_DEEPSEEK_API_KEY=sk-your-api-key
VITE_DEEPSEEK_BASE_URL=https://api.deepseek.com
2. Vue3 项目集成
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const apiKey = import.meta.env.VITE_DEEPSEEK_API_KEY
const baseURL = import.meta.env.VITE_DEEPSEEK_BASE_URL
const messages = ref([{ role: 'system', content: '你是一个专业的助手' }])
const userInput = ref('')
const isLoading = ref(false)
const sendToDeepSeek = async () => {
try {
isLoading.value = true
const response = await axios.post(
`${baseURL}/chat/completions`,
{
model: 'deepseek-chat',
messages: [...messages.value, { role: 'user', content: userInput.value }],
temperature: 0.7,
stream: false // 流式传输需特殊处理
},
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}
)
messages.value.push(response.data.choices[0].message)
} catch (error) {
console.error('API调用失败:', error.response?.data || error.message)
} finally {
isLoading.value = false
}
}
</script>
3.示例 1(普通示例)
<template>
<div class="chat-container">
<div class="chat-history" ref="historyContainer">
<div
v-for="(msg, index) in messages.filter(m => m.role !== 'system')"
:key="index"
:class="['message', msg.role]"
>
<div class="message-content">
<div class="role-tag">{{ msg.role === 'user' ? '我' : 'AI' }}</div>
<div class="content">{{ msg.content }}</div>
</div>
</div>
<div v-if="isLoading" class="loading-indicator">
<div class="dot-flashing"></div>
</div>
</div>
<div class="input-area">
<textarea
v-model="userInput"
placeholder="输入你的问题..."
@keydown.enter.exact.prevent="handleSend"
:disabled="isLoading"
></textarea>
<button
@click="handleSend"
:disabled="!userInput.trim() || isLoading"
>
{{ isLoading ? '发送中...' : '发送' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
import axios from 'axios'
const apiKey = import.meta.env.VITE_DEEPSEEK_API_KEY
const baseURL = import.meta.env.VITE_DEEPSEEK_BASE_URL
const messages = ref([{ role: 'system', content: '你是一个专业的助手' }])
const userInput = ref('')
const isLoading = ref(false)
const historyContainer = ref(null)
const scrollToBottom = () => {
nextTick(() => {
if (historyContainer.value) {
historyContainer.value.scrollTop = historyContainer.value.scrollHeight
}
})
}
const handleSend = async () => {
if (!userInput.value.trim() || isLoading.value) return
const userMessage = { role: 'user', content: userInput.value.trim() }
messages.value.push(userMessage)
userInput.value = ''
try {
isLoading.value = true
const response = await axios.post(
`${baseURL}/chat/completions`,
{
model: 'deepseek-chat',
messages: messages.value,
temperature: 0.7,
stream: false
},
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}
)
const aiMessage = response.data.choices[0].message
messages.value.push(aiMessage)
} catch (error) {
console.error('API调用失败:', error.response?.data || error.message)
messages.value.push({
role: 'assistant',
content: '抱歉,请求处理过程中出现了问题,请稍后再试。'
})
} finally {
isLoading.value = false
scrollToBottom()
}
}
watch(messages, scrollToBottom, { deep: true })
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 20px auto;
height: 90vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.chat-history {
flex: 1;
padding: 20px;
overflow-y: auto;
background: white;
}
.message {
margin: 12px 0;
display: flex;
}
.message.user {
justify-content: flex-end;
}
.message.assistant {
justify-content: flex-start;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
position: relative;
}
.user .message-content {
background: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.assistant .message-content {
background: #f0f0f0;
color: #333;
border-bottom-left-radius: 4px;
}
.role-tag {
font-size: 0.8em;
color: #666;
margin-bottom: 4px;
}
.user .role-tag {
color: rgba(255,255,255,0.8);
}
.input-area {
padding: 20px;
background: white;
border-top: 1px solid #eee;
display: flex;
gap: 12px;
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: none;
min-height: 48px;
max-height: 150px;
}
button {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.loading-indicator {
padding: 12px;
text-align: center;
color: #666;
}
.dot-flashing {
display: inline-block;
position: relative;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #999;
animation: dot-flashing 1s infinite linear;
}
@keyframes dot-flashing {
0% { opacity: 0.2; transform: translateY(0); }
50% { opacity: 1; transform: translateY(-3px); }
100% { opacity: 0.2; transform: translateY(0); }
}
</style>
4.示例 2(生成markdown格式)
安装依赖库
npm install marked highlight.js
代码
<template>
<div class="container">
<!-- ai助手 -->
<div class="chat-container">
<!-- 聊天框 -->
<div class="chat-history" ref="historyContainer">
<!-- 消息循环 -->
<div v-for="(msg, index) in messages.filter(m => m.role !== 'system')" :key="index"
:class="['message', msg.role]">
<div class="message-content">
<div class="role-tag">{{ msg.role === 'user' ? '我' : 'AI' }}</div>
<!-- 添加Markdown渲染 -->
<div class="content markdown-body" v-html="renderMarkdown(msg.content)"
v-if="msg.role === 'assistant'"></div>
<div class="content" v-else>{{ msg.content }}</div>
</div>
</div>
<!-- 加载指示器 -->
<div v-if="isLoading" class="loading-indicator">
<div class="dot-bounce">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
</div>
<!-- 输入框 -->
<div class="input-area">
<textarea v-model="userInput" placeholder="输入你的问题..." @keydown.enter.exact.prevent="handleSend"
:disabled="isLoading"></textarea>
<button @click="handleSend" :disabled="!userInput.trim() || isLoading">
{{ isLoading ? '发送中...' : '发送' }}
</button>
</div>
</div>
<!-- 背景栏 -->
<div class="bg-bar"></div>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
import { marked } from 'marked'; // 引入markdown解析库
import hljs from 'highlight.js' // 引入代码高亮库
import axios from 'axios'
// 环境变量
const apiKey = import.meta.env.VITE_DEEPSEEK_API_KEY
const baseURL = import.meta.env.VITE_DEEPSEEK_BASE_URL
const messages = ref([{ role: 'system', content: '你是一个专业的助手' }])
const userInput = ref('')
const isLoading = ref(false)
const historyContainer = ref(null)
// 配置marked
marked.setOptions({
highlight: function (code, language) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(validLanguage, code).value
},
breaks: true
})
// Markdown渲染方法
const renderMarkdown = (content) => {
return marked(content)
}
// 自动滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (historyContainer.value) {
historyContainer.value.scrollTop = historyContainer.value.scrollHeight
}
})
}
// 消息发送处理
const handleSend = async () => {
if (!userInput.value.trim() || isLoading.value) return
// 添加用户消息
const userMessage = { role: 'user', content: userInput.value.trim() }
messages.value.push(userMessage)
userInput.value = ''
try {
isLoading.value = true
// API调用
const response = await axios.post(
`${baseURL}/chat/completions`,
{
model: 'deepseek-chat',
messages: messages.value,
temperature: 0.7,
stream: false
},
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}
)
// 添加AI响应
const aiMessage = response.data.choices[0].message
messages.value.push(aiMessage)
} catch (error) {
console.error('API调用失败:', error.response?.data || error.message)
messages.value.push({
role: 'assistant',
content: '抱歉,请求处理过程中出现了问题,请稍后再试。'
})
} finally {
isLoading.value = false
scrollToBottom()
}
}
// 监听消息变化触发滚动
watch(messages, scrollToBottom, { deep: true })
</script>
<style scoped>
* {
font-family: 'Pixel-Font';
}
.container {
overflow: hidden;
width: 100%;
height: 100%;
background-color: #DCD9D4;
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.50) 0%, rgba(0, 0, 0, 0.50) 100%), radial-gradient(at 50% 0%, rgba(255, 255, 255, 0.10) 0%, rgba(0, 0, 0, 0.50) 50%);
background-blend-mode: soft-light, screen;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.chat-container {
z-index: 1;
width: 100%;
margin: 30px 20% 10px;
height: 80vh;
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.chat-history {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #fff;
}
.message {
margin: 12px 0;
display: flex;
}
.message.user {
justify-content: flex-end;
}
.message.assistant {
justify-content: flex-start;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
position: relative;
}
.user .message-content {
background: #666;
color: white;
border-bottom-right-radius: 4px;
}
.assistant .message-content {
background: #f0f0f0;
color: #333;
border-bottom-left-radius: 4px;
}
.role-tag {
font-size: 0.8em;
color: #666;
margin-bottom: 4px;
}
.user .role-tag {
color: rgba(255, 255, 255, 0.8);
}
.input-area {
padding: 20px;
background: #eee;
border-top: 1px solid #eee;
display: flex;
gap: 12px;
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: none;
min-height: 48px;
max-height: 150px;
}
button {
padding: 12px 24px;
background: #666;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.loading-indicator {
padding: 12px;
text-align: center;
color: #666;
}
.loading-indicator {
padding: 12px;
text-align: center;
min-height: 40px;
}
.dot-bounce {
display: inline-flex;
align-items: center;
gap: 8px;
height: 24px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #666;
animation: dot-bouncing 1.4s infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes dot-bouncing {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.5;
}
40% {
transform: translateY(-10px);
opacity: 1;
}
}
.bg-bar {
z-index: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.15) 100%), radial-gradient(at top center, rgba(255, 255, 255, 0.40) 0%, rgba(0, 0, 0, 0.40) 120%) #989898;
background-blend-mode: multiply, multiply;
clip-path: polygon(0% 0%, 100% 0%, 90% 20%, 85% 30%, 80% 35%, 60% 50%, 45% 85%, 40% 90%, 20% 80%, 5% 90%, 0% 100%);
animation: heightAnimation 2s ease-in-out forwards;
}
@keyframes heightAnimation {
0% {
height: 0%;
}
100% {
height: 100%;
}
}
/* 添加Markdown样式 */
.markdown-body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.6;
}
/* 代码块样式 */
.markdown-body pre {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 6px;
padding: 16px;
overflow-x: auto;
}
.markdown-body code {
padding: 0.2em 0.4em;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
/* 表格样式 */
.markdown-body table {
border-collapse: collapse;
margin: 1em 0;
}
.markdown-body td,
.markdown-body th {
border: 1px solid #dfe2e5;
padding: 0.6em 1em;
}
/* 其他元素样式 */
.markdown-body blockquote {
border-left: 4px solid #dfe2e5;
margin: 0;
padding: 0 1em;
color: #6a737d;
}
.markdown-body ul {
padding-left: 2em;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
</style>
评论 (0)