1.创建一个vue2项目,引入UI组件Element
2.安装依赖这三个依赖库 npm install markdown-it markdown-it-highlightjs dompurify
3.组件代码
可以直接复制进项目使用,在需要显示的界面引用就好。代码中的apiKey需要自己去deepseek官网获取,具体操作步骤如下:
- 访问DeepSeek官网 https://platform.deepseek.com
- 注册/登录后进入控制台
- 在「API Keys」模块点击「Create New Key」
- 复制生成的密钥字符串
- 在代码中定位以下位置进行替换:
-
// 在<script>部分找到data配置项 data() { return { // ...其他配置... apiKey: "在此替换为你的API密钥", // 👈 替换这里 }; },
<template>
<div class="chat-container">
<div class="chat-box" style="height: 1200px; overflow: auto;">
<div v-for="(message, index) in messages" :key="index" class="message-item">
<div v-if="message.role === 'user'" class="user-message">
<div class="bubble user" v-html="message.content"></div>
<!-- <div>
<el-image src="/static/BNMap/images/user.png" style="width: 45px;height:45px" alt="用户头像"></el-image>
</div> -->
</div>
<div v-else class="assistant-message">
<div style="background-color:#fff;border-radius: 50%;width:40px;height:40px;margin-right:5px">
<svg-icon icon-class="deepseek" style="font-size:46px"></svg-icon>
</div>
<div v-if="message.content" class="bubble assistant" v-html="parseMarkdown(message.content)"></div>
<!-- 添加逻辑:仅在loading状态为true且没有内容时显示加载图标 -->
<div v-if="message.loading && !message.content" style="position: absolute;left: 71px"> <i
class="el-icon-loading" style="font-size:25px"></i></div>
</div>
</div>
</div>
<div class="input-container">
<el-input type="textarea" :rows="4" placeholder="请输入您的问题..." class="message-input" v-model="content"
@keyup.enter.native="submit"></el-input>
<!-- 动态切换按钮 -->
<el-button @click="handleButtonAction" class="send-button" :style="buttonStyle">
<i v-if="!isSending" style="font-size: 20px" class="el-icon-position"></i>
<i v-else style="font-size: 20px" class="el-icon-switch-button"></i>
{{ buttonText }}
</el-button>
</div>
</div>
</template>
<script>
import MarkdownIt from 'markdown-it'
import hljs from 'markdown-it-highlightjs'
import DOMPurify from 'dompurify'
export default {
name: "ChatWindow",
data() {
return {
content: "",
messages: [],
isButtonDisabled: false,
apiKey: "your_deepseek_key", // 替换为你的API Key
md: new MarkdownIt({
html: true,
linkify: true,
typographer: true
}).use(hljs),
controller: null,
isSending: false, // 新增发送状态
buttonText: '发送', // 按钮文字
buttonStyle: { // 按钮样式
backgroundColor: '#409eff'
}
};
},
mounted() { },
methods: {
// 统一按钮处理
handleButtonAction() {
if (this.isSending) {
this.stopGenerating();
} else {
this.submit();
}
},
// 停止生成
stopGenerating() {
if (this.controller) {
this.controller.abort();
this.controller = null;
// this.updateLastAssistantMessage(this.fullResponse + "\n(已停止生成)");
this.resetButtonState();
}
},
parseMarkdown(raw) {
return DOMPurify.sanitize(this.md.render(raw))
},
async submit() {
if (!this.content.trim()) return;
// 设置按钮状态
this.isSending = true;
this.buttonText = '停止';
this.buttonStyle.backgroundColor = '#ff4d4f';
this.isButtonDisabled = false;
// 添加用户消息
this.messages.push({
role: "user",
content: this.content
});
// 添加AI消息占位符
this.messages.push({
role: "assistant",
content: "",
loading: true
});
this.isButtonDisabled = true;
this.content = "";
try {
this.controller = new AbortController();
const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: "deepseek-chat",
messages: this.formatMessages(),
stream: true,
temperature: 0.3,
max_tokens: 2048
}),
signal: this.controller.signal
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
// 添加首次响应处理
let hasReceivedData = false;
for (const line of lines) {
const message = line.replace(/^data: /, '');
if (message === "[DONE]") break;
try {
const parsed = JSON.parse(message);
if (parsed.choices[0].delta.content) {
// 首次收到数据时关闭loading
if (!hasReceivedData) {
hasReceivedData = true;
this.messages[this.messages.length - 1].loading = false;
}
fullResponse += parsed.choices[0].delta.content;
this.updateLastAssistantMessage(fullResponse);
}
} catch (e) {
console.error("Error parsing message:", e);
}
}
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error("请求错误:", error);
this.updateLastAssistantMessage("请求出错,请稍后重试");
this.messages[this.messages.length - 1].loading = false;
}
} finally {
this.isButtonDisabled = false;
this.controller = null;
this.messages[this.messages.length - 1].loading = false;
this.resetButtonState();
}
},
// 重置按钮状态
resetButtonState() {
this.isSending = false;
this.buttonText = '发送';
this.buttonStyle.backgroundColor = '#409eff';
this.isButtonDisabled = false;
// 更新最后一条消息的加载状态
const lastIndex = this.messages.length - 1;
if (lastIndex >= 0 && this.messages[lastIndex].loading) {
this.$set(this.messages, lastIndex, {
...this.messages[lastIndex],
loading: false
});
}
},
formatMessages() {
return this.messages
.filter(m => m.role !== 'assistant' || m.content)
.map(m => ({ role: m.role, content: m.content }));
},
updateLastAssistantMessage(content) {
const lastIndex = this.messages.length - 1;
if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
this.$set(this.messages, lastIndex, {
...this.messages[lastIndex],
content: content
});
}
}
},
beforeDestroy() {
if (this.controller) {
this.controller.abort();
}
}
};
</script>
<style scoped>
/* 新增停止按钮动画 */
.el-icon-switch-button {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
/* 按钮过渡效果 */
.send-button {
transition: all 0.3s ease-in-out !important;
}
/* 保持原有样式不变 */
.chat-container {
height: 700px;
display: flex;
flex-direction: column;
max-width: 1000px;
margin: 0 auto;
padding: 10px;
background-color: rgb(90, 79, 79);
border-radius: 10px;
}
.chat-box {
border-radius: 10px;
width: 800px;
background-color: #ccc;
flex: 1;
padding: 10px;
margin-bottom: 8px;
overflow-y: auto;
overflow-x: hidden;
}
.message-item {
margin-bottom: 10px;
}
.user-message {
display: flex;
justify-content: flex-end;
align-items: center;
}
.assistant-message {
display: flex;
justify-content: flex-start;
align-items: center;
}
.bubble {
font-size: 15px;
max-width: 80%;
padding: 15px;
padding-left: 25px;
border-radius: 10px;
overflow: hidden;
word-wrap: break-word;
box-sizing: border-box;
}
.user {
background-color: #e0f7fa;
margin-left: auto;
}
.assistant {
background-color: #f5f5f5;
margin-right: auto;
}
.input-container {
display: flex;
width: 800px;
gap: 10px;
}
.send-button {
padding: 0 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.loading-icon {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-top-color: #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@import '~highlight.js/styles/github.css';
</style>
3.效果界面展示