vue 仿deepseek前端开发一个对话界面

news/2025/3/15 14:08:31/

后端:调用deepseek的api,所以返回数据格式和deepseek相同

javascript">{"model": "DeepSeek-R1-Distill-Qwen-1.5B", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ",有什么", "tool_calls": null}, "finish_reason": null, "logprobs": null}], "usage": {"prompt_tokens": 5, "completion_tokens": 11, "total_tokens": 16}, "id": "chatcmpl-203a3024a36e4c02b02200ca47d8901e", "object": "chat.completion.chunk", "created": 1741766893}

在这里插入图片描述
前端开发的几个注意点:

  1. 将后端返回的文本转换为html展示在页面上,注意调整样式
  2. 模拟打字形式,页面随之滚动
  3. 撑开的输入框和对话内容部分样式调整
  4. 测试多种文本输入,例如含有html标签的
  5. 记录思考时间,思考内容模仿deepseek做收起

这只是初步的项目,仅支持文本输入
懒得分步写了,直接贴完整代码吧

有些地方可能写得比较繁琐
比如判断思考内容那段,只能通过think标签判断吗?
请各位多多指点,欢迎交流!!



javascript"><template><div class="talk-window"><div class="talk-title"><p>{{ answerTitle }}</p><el-input v-model="choicedTasks.name" placeholder="请选择任务" class="sangedianBtn" readonly><template #append><el-button class="ec-font icon-sangedian" @click="isShowTask = true" /></template></el-input></div><div class="talk-container"><div class="talk-welcome" v-if="contentList.length == 0"><h1>{{ welcome.title }}</h1><p>{{ welcome.desc }}</p></div><div class="talk-box" v-else :style="{ height: answerContHeight }"><div ref="logContainer" class="talk-content"><el-row v-for="(item, i) in contentList" :key="i" class="chat-assistant"><transition name="fade"><div :class="['answer-cont', item.type === 'send' ? 'end' : 'start']"><img v-if="item.type == 'answer'" :src="welcome.icon" /><div :class="item.type === 'send' ? 'send-item' : 'answer-item'"><div v-if="item.type == 'answer'" class="hashrate-markdown" v-html="item.message" /><div v-else>{{ item.message }}</div></div><!-- 增加复制 --><!-- <div v-if="item.type == 'answer' && !isTalking"><el-tooltip centent="复制"><i class="ec-font icon-ticket" @click="copyMsg(item.message)" /></el-tooltip></div> --></div></transition></el-row></div><div style="text-align: center; margin-top: 10px"><el-button class="chat-add" @click="newChat"><i class="ec-font icon-tianjia1" />新建对话</el-button></div></div><div class="talk-send"><textarea@keydown.enter="enterMessage"ref="input"v-model="inputMessage"@input="adjustInputHeight"placeholder="输入消息...":rows="2" /><!-- <el-inputv-model="inputMessage":autosize="{ minRows: 2, maxRows: 5 }"type="textarea"@keyup.enter="enterMessage"placeholder="Please input" /> --><div class="talk-btn-cont" style="text-align: right"><img @click="sendMessage" :src="iconImg" /></div></div></div><copyrightContent :systemNameOption="systemNameOption" /><taskDialog v-if="isShowTask" :choicedTasks="choicedTasks" v-model:isShow="isShowTask" @confirm="confirmTask" /></div>
</template><script>
import taskDialog from '@/views/chat/task/taskDialog.vue'
import copyrightContent from '@/views/chat/talking/components/copyright.vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import MarkdownIt from 'markdown-it'
import { VSConfig } from '/config/envConfig'window.hiddenThink = function (index) {// 隐藏思考内容if (document.getElementById(`think_content_${index}`).style.display == 'none') {document.getElementById(`think_content_${index}`).style.display = 'block'document.getElementById(`think_icon_${index}`).classList.replace('icon-a-xiangshang3', 'icon-a-xiangxia3')} else {document.getElementById(`think_content_${index}`).style.display = 'none'document.getElementById(`think_icon_${index}`).classList.replace('icon-a-xiangxia3', 'icon-a-xiangshang3')}
}export default {props: {isCollapsed: {type: Boolean,default: false},activeChat: String,activeTitle: String,chat_style_setting: {type: Object,default: function () {return {}}},systemNameOption: {type: Object,default: function () {return {}}}},components: { taskDialog, copyrightContent },data() {return {inputMessage: '',messages: [],choicedTasks: {uuid: '',name: ''},isShowTask: false,contentList: [],eventSourceChat: null,markdownIt: {},startAnwer: false,startTime: null,endTime: null,thinkTime: null,answerTitle: '',talkUUID: '',refreshHistoryFlag: false, // 是否已经生成了对话uuid,生成了的话就刷新历史列表msgHight: null,isTalking: false, //是否在对话中,处于对话中则展示休止按钮welcome: {title: '很高兴见到你!',desc: '我可以帮你写代码、读文件、写作各种创意内容,请把你的任务交给我吧~',icon: require('@/assets/image/AI.png')},store: {}}},watch: {activeTitle(val) {// 重命名了对话this.answerTitle = val},activeChat(val) {this.answerTitle = ''this.talkUUID = val || ''this.inputMessage = ''this.refreshHistoryFlag = falsethis.isTalking = falseif (val) {// 滚动回到顶部const logContainer = this.$refs.logContainerif (logContainer) {logContainer.scrollTop = 0}this.getTalkDetail()} else {this.contentList = []}this.eventSourceChat && this.eventSourceChat.close()},chat_style_setting(val) {this.welcome.title = val.welcome_speech_style || this.welcome.titlethis.welcome.desc = val.description_style || this.welcome.descthis.welcome.icon = val.icon_image || this.welcome.icon}},computed: {iconImg() {// 对话中可能有输入if (this.isTalking) {return require('/src/assets/image/chat/stop.png')} else {if (!this.inputMessage ||this.inputMessage.trim() === '' ||this.inputMessage.split(/\r?\n/).every((line) => line.trim() === '')) {return require('/src/assets/image/chat/unsend.png')} else {return require('/src/assets/image/chat/send.png')}}},answerContHeight() {// 回到初始值return this.msgHight == '56px' ? 'calc(100% - 140px)' : `calc(100% - 140px - ${this.msgHight} + 56px)`}},mounted() {this.store = mainStore()this.markdownIt = MarkdownIt({html: true,linkify: true,highlight: function (str, lang) {if (lang && hljs.getLanguage(lang)) {try {return hljs.highlight(str, { language: lang }).value} catch (__) {}}return '' // use external default escaping}})},methods: {getTalkDetail() {chat.historyDetail({uuid: this.activeChat}).then((res) => {this.contentList = []if (res.code == 0) {res.info.history_meta &&res.info.history_meta.forEach((item, index) => {if (item.conversation_type == 'Answer') {// 增加一个历史思考记录收起吧item.context = item.context.replace(/<think>\n\n<\/think>/g, '').replaceAll('<think>',`<div class="think-time">历史思考<i id="think_icon_${index}" onclick="hiddenThink(${index})" class="ec-font icon-a-xiangxia3"></i></div><section id="think_content_${index}">`).replaceAll('</think>', '</section>')item.context = this.markdownIt.render(item.context)}this.contentList.push({type: item.conversation_type == 'Question' ? 'send' : 'answer',message: item.context})})this.answerTitle = res.info.name.substring(0, 20)}})},enterMessage(event) {if (event.key === 'Enter' && !event.shiftKey) {event.preventDefault()this.sendMessage()}},sendMessage() {// 终止当前对话if (this.isTalking) {this.eventSourceChat && this.eventSourceChat.close()// 关闭后,处理正在对话的"思考中"let curAnswer = this.contentList[this.contentList.length - 1].messagecurAnswer = curAnswer.replaceAll('<div class="think-time">思考中……</div>', '对话中止')this.contentList[this.contentList.length - 1].message = curAnswerthis.isTalking = false// 再获取一下历史记录// 暂时不获取历史记录,因为中止对话时,历史UUID可能还没返回// this.$emit('getHistoryList')return}if (!this.inputMessage ||this.inputMessage.trim() === '' ||this.inputMessage.split(/\r?\n/).every((line) => line.trim() === '')) {this.inputMessage = ''return false}if (!this.choicedTasks.name) {this.$message({ message: '请选择推理任务', type: 'warning' })return false}// 回到初始高度const textarea = this.$refs.inputtextarea.style.height = '56px'this.msgHight = '56px'this.eventSourceChat && this.eventSourceChat.close()// let markedText = this.markdownIt.render(this.inputMessage)this.contentList.push({ type: 'send', message: this.inputMessage })this.contentList.push({ type: 'answer', message: `<div class="think-time">思考中……</div>` })this.answerTitle = this.answerTitle || this.contentList[0].message.substring(0, 20)this.scrollToBottom()this.initSSEChat()},initSSEChat() {const url = `${VSConfig.isHttps ? 'https' : 'http'}://${this.store.leader}/v1/intelligent_computing/task/chat/stream?uuid=${this.choicedTasks.uuid}&message=${encodeURIComponent(this.inputMessage)}&token=${this.store.token}&conversation_uuid=${this.talkUUID}`this.inputMessage = ''this.eventSourceChat = new EventSource(url)let buffer = ''this.startTime = nullthis.endTime = nullthis.thinkTime = nulllet len = this.contentList.lengthlet index = len % 2 === 0 ? len - 1 : lenthis.isTalking = truethis.eventSourceChat.onmessage = async (event) => {await this.sleep(10)if (event.data == '[DONE]') {return false}// 接收 Delta 数据// 最后一条是UUID,第二次发对话的时候要传参try {var { choices, created } = JSON.parse(event.data)} catch (e) {// 新对话在历史列表补充数据this.talkUUID = event.dataif (!this.refreshHistoryFlag) {this.refreshHistoryFlag = event.datathis.$emit('refreshHistory', this.refreshHistoryFlag)}}// const { choices, created } = JSON.parse(event.data)if (choices && choices[0].delta?.content) {buffer += choices[0].delta.content// think标签内是思考内容,单独记录思考时间if (choices[0].delta.content.includes('<think>')) {choices[0].delta.content = `<div class="think-time">思考中……</div><section id="think_content_${index}">`buffer = buffer.replaceAll('<think>', choices[0].delta.content)this.startTime = Math.floor(new Date().getTime() / 1000)}if (choices[0].delta.content.includes('</think>')) {// console.log("结束时间赋值的判断")choices[0].delta.content = `</section>`this.endTime = Math.floor(new Date().getTime() / 1000)// 获取到结束时间后,直接展示收起按钮this.thinkTime = this.endTime - this.startTimebuffer = buffer.replaceAll('<div class="think-time">思考中……</div>',`<div class="think-time">已深度思考(${this.thinkTime}S)<i id="think_icon_${index}" onclick="hiddenThink(${index})" class="ec-font icon-a-xiangxia3"></i></div>`).replaceAll('</think>', choices[0].delta.content).replaceAll(`<section id="think_content_${index}"></section>`, '')}let markedText = this.markdownIt.render(buffer)this.contentList[index] = { type: 'answer', message: markedText }this.scrollToBottomIfAtBottom()}}this.eventSourceChat.onerror = (event) => {console.log('错误触发===》', event)this.contentList[index] = { type: 'answer', message: `<div class="think-time">对话服务连接失败</div>` }this.eventSourceChat.close()this.isTalking = false}this.eventSourceChat.onclose = (event) => {// 关闭事件console.log('关闭事件--->')this.isTalking = false}},sleep(ms) {return new Promise((resolve) => setTimeout(resolve, ms))},scrollToBottomIfAtBottom() {this.$nextTick(() => {const logContainer = this.$refs.logContainerif (logContainer) {const threshold = 100const distanceToBottom = logContainer.scrollHeight - logContainer.scrollTop - logContainer.clientHeightif (distanceToBottom <= threshold) logContainer.scrollTop = logContainer.scrollHeight}})},scrollToBottom() {this.$nextTick(() => {const logContainer = this.$refs.logContainerif (logContainer) {logContainer.scrollTop = logContainer.scrollHeight}})},confirmTask(val) {this.choicedTasks = val},clearWindow() {this.eventSourceChat && this.eventSourceChat.close()this.contentList = []this.answerTitle = ''this.talkUUID = ''this.inputMessage = ''this.refreshHistoryFlag = falsethis.isTalking = falseconst textarea = this.$refs.inputtextarea.style.height = '56px'this.msgHight = '56px'},newChat() {this.clearWindow()this.$emit('clearChat')},adjustInputHeight(event) {// enter键盘按下的换行赋值为空if (event.key === 'Enter' && !event.shiftKey) {this.inputMessage = ''event.preventDefault()return}this.$nextTick(() => {const textarea = this.$refs.inputtextarea.style.height = 'auto'// 最高200pxtextarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'this.msgHight = textarea.style.height})},copyMsg(txt) {// 复制功能// 创建一个临时的 textarea 元素const textarea = document.createElement('textarea')textarea.value = txt.replace(/<[^>]+>/g, '') // 去掉html标签textarea.style.position = 'fixed'document.body.appendChild(textarea)textarea.select() // 选中文本try {document.execCommand('copy') // 执行复制ElMessage({message: '复制成功',type: 'success'})} catch (err) {ElMessage({message: '复制失败',type: 'error'})} finally {document.body.removeChild(textarea) // 移除临时元素}}},beforeDestroy() {this.eventSourceChat && this.eventSourceChat.close()}
}
</script><style scoped lang="scss">
.talk-window {height: 100%;transition: margin 0.2s ease;position: relative;
}
.talk-container {height: calc(100% - 58px);position: relative;
}
.talk-welcome {text-align: center;// margin-bottom: 25px;padding: 10% 20% 25px;box-sizing: border-box;h1 {margin-bottom: 30px;font-size: 21px;}p {color: #8f9aad;}
}
.messages {padding: 20px;overflow-y: auto;
}.message {display: flex;margin: 12px 0;
}.message.user {justify-content: flex-end;
}.bubble {max-width: 70%;padding: 12px 16px;border-radius: 12px;background: #fff;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}.message.user .bubble {background: #e8f4ff;
}.time {font-size: 12px;color: #666;margin-top: 4px;
}.talk-send {background: #f1f2f7;border-radius: 10px;border: 1px solid #e9e9eb;padding: 5px 10px;margin: 0px 20%;img {cursor: pointer;}textarea {width: 100%;padding: 10px;resize: none;overflow: auto;// min-height: 48px;max-height: 200px;line-height: 1.5;box-sizing: border-box;font-family: inherit;border: 0px;background: #f1f2f7;}textarea:focus {outline: none !important;}
}input {flex: 1;padding: 12px;border: 1px solid #ddd;border-radius: 8px;
}.talk-title {height: 56px;line-height: 56px;p {color: #000000;font-size: 15px;font-weight: 550;text-align: center;}.sangedianBtn {width: 225px;height: 32px;position: absolute;top: 15px;right: 30px;}
}.send-item {max-width: 60%;word-break: break-all;padding: 10px;background: #eef6ff;border-radius: 10px;color: #000000;white-space: pre-wrap;font-size: 13px;
}
.msg-row {margin-bottom: 10px;
}
.talk-box {height: calc(100% - 140px);.talk-content {background-color: #fff;color: #324659;overflow-y: auto;height: calc(100% - 50px);box-sizing: border-box;padding: 0px 20%;// &:hover {//   overflow-y: auto;// }.chat-assistant {display: flex;margin-bottom: 10px;.answer-item {line-height: 30px;color: #324659;}}.answer-cont {position: relative;display: flex;width: 100%;> img {width: 30px;height: 30px;margin-right: 10px;}&.end {justify-content: flex-end;}&.start {justify-content: flex-start;}}}.chat-sse {min-height: 100px;max-height: 460px;}.chat-message {height: calc(100vh - 276px);}.thinking-bubble {height: calc(100vh - 296px);}
}
.chat-add {width: 111px;height: 33px;background: #dbeafe;border-radius: 6px !important;font-size: 14px !important;border: 0px;color: #516ffe !important;&:hover {background: #ebf0f7;}.icon-tianjia1 {margin-right: 10px;font-size: 14px;}
}
.talk-btn-cont {text-align: right;height: 30px;margin-top: 5px;
}
</style><style lang="scss">
@use './markdown.scss';
</style>

最终页面:
在这里插入图片描述


http://www.ppmy.cn/news/1579325.html

相关文章

69.Harmonyos NEXT图片预览组件应用实践(二):电商、内容与办公场景

温馨提示&#xff1a;本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦&#xff01; Harmonyos NEXT图片预览组件应用实践&#xff08;二&#xff09;&#xff1a;电商、内容与办公场景 文章目录 Harmonyos NEXT图片预览组件应用实践…

根据Excel快速生成表的创建以及新增数据记录的sql

目录 前言一、下载Excel二、使用步骤(以自增版为例)1.生成建表sql1.1.在"table结构表"创建表结构1.2.确认区域1.3.获取建表sql 2.生成新增数据记录sql2.1.维护新增的数据2.2.处理新增的数据2.3.获取sql语句 总结 前言 在Excel软件中&#xff0c;根据维护的表结构与数…

HTTP 协议中常见的错误状态码(详细介绍)

以下是 HTTP 协议中常见的错误状态码及其原因的总结&#xff0c;按错误类型分类整理&#xff1a; 4xx 客户端错误 400 Bad Request 原因&#xff1a;请求格式错误&#xff0c;服务器无法解析。常见场景&#xff1a; 请求头或请求体语法错误&#xff08;如 JSON/XML 格式错误…

Linux 部署 Spring Boot 项目, Web项目(2025版)

Linux 部署 Spring Boot 项目&#xff0c;Web项目&#xff08;2025版&#xff09; 一、简洁版1.1 Linux 环境配置1.2 将Spring Boot 项目部署到 Linux 中 二、详细版2.1 Linux 环境配置2.2 Spring Boot 项目搭建2.3 mysql 配置2.4 测试项目2.5 将Spring Boot 项目部署到 Linux …

Maven | 站在初学者的角度配置

目录 Maven 是什么 概述 常见错误 创建错误代码示例 正确代码示例 Maven 的下载 Maven 依赖源 Maven 环境 环境变量 CMD测试 Maven 文件配置 本地仓库 远程仓库 Maven 工程创建 IDEA配置Maven IDEA Maven插件 Maven 是什么 概述 Maven是一个项目管理和构建自…

linux 时间同步(阿里云ntp服务器)

1、安装ntp服务 rootlocalhost ~]# yum -y install ntp 已加载插件&#xff1a;fastestmirror, langpacks Loading mirror speeds from cached hostfile* base: mirrors.nju.edu.cn* centos-sclo-rh: mirrors.nju.edu.cn* centos-sclo-sclo: mirrors.huaweicloud.com* epel: m…

Apifox使用总结

将登录获取的token 赋值到接口中 登陆接口设置后置操作&#xff0c;代码如下 var data JSON.parse(responseBody) pm.environment.set(token, data.token)在需要配置登录的文件夹 选择 auth 选择 Bear Token 设置变量值 {{token}}

python编写的一个打砖块小游戏

游戏介绍 打砖块是一款经典的街机游戏&#xff0c;玩家控制底部的挡板&#xff0c;使球反弹以击碎上方的砖块。当球击中砖块时&#xff0c;砖块消失&#xff0c;球反弹&#xff1b;若球碰到挡板&#xff0c;则改变方向继续运动&#xff1b;若球掉出屏幕底部&#xff0c;玩家失…