<template> <div class="chatInfor"> <div class="chatInfor-content"> <el-scrollbar height="97%" id="chatBox" ref="scrollbarRef" v-loading="loading" width="928px" element-loading-text="数据加载中。。。" :element-loading-spinner="svg" element-loading-svg-view-box="-10, -10, 50, 50" element-loading-background="rgba(122, 122, 122, 0)"> <div class="chatInfor-content-item" v-for="(item, index) in chatList" :key="index"> <div class="chatInfor-content-item-time" v-if="item.role == 'user'">{{ item.created_at }}</div> <div class="chatInfor-content-item-users" v-if="item.role == 'user'"> <p> <div v-for="(items, index) in item.content"> <div v-if="items.type == 'text'"> {{ items.text }}</div> <div style="display: flex;margin-left: -16px;" v-show="items.imgList"> <div v-for="(itemimage, index) in items.imgList" style="margin-left:16px;margin-top:8px"> <el-image :src="itemimage" :zoom-rate="1.2" :max-scale="7" class="chatInfor-input-button-imglist-item-img" :min-scale="0.2" :preview-src-list="items.imgList" :initial-index="0" fit="cover" style="height:72px;" /> </div> </div> <!-- <img :src="items.text" v-if="items.imgList.length > 0" /> --> </div> </p> <img :src="icon ? proxy.$loginUrl + icon : hqdInforImg"> <!-- <img src="@/assets/function/chat/person-image.png" /> --> </div> <div class="chatInfor-content-item-chats" v-if="item.role == 'assistant' && item.progress == false"> <img src="@/assets/function/chat/hqd-image.png" /> <p v-if="!item.content[0].text"> <div class="loader"> <div class="loader__circle"></div> <div class="loader__circle"></div> <div class="loader__circle"></div> <div class="loader__circle"></div> <div class="loader__circle"></div> </div> </p> <p v-else> <span v-html="md.render(item.content[0].text)" :id="`id${index}`"></span> <div class="chatInfor-content-item-chats-button" v-if="item.isRecepting == false"> <div class="ebutton" @click="reBuild(index)"> <SvgIcon name="chat-reset" class="chat-btn" /> <span>重新生成</span> </div> <div class="ebutton" @click="tranlate(item, index)"> <SvgIcon name="chat-trans" class="chat-btn" /> <span>翻译</span> </div> <div class="ebutton" @click="onCopy(item)"> <SvgIcon name="chat-copy" class="chat-btn" /> <span>复制</span> </div> </div> </p> <!-- <div>111</div> --> </div> <div class="chatInfor-content-item-chats" v-if="item.role == 'assistant' && item.progress == true"> <img src="@/assets/function/chat/hqd-image.png" /> <p v-if="!respContent"> <div class="loader"> <div class="loader__circle"></div> <div class="loader__circle"></div> <div class="loader__circle"></div> <div class="loader__circle"></div> <div class="loader__circle"></div> </div> </p> <p v-html="md.render(respContent)" v-if="respContent"></p> </div> </div> </el-scrollbar> </div> <div class="chatInfor-input"> <el-input type="textarea" class="el-text" v-model="infor" resize="none" ref="textInput" @keyup.ctrl.enter.native="lineBreak" @keydown.enter.exact.native="sendMessage" autosize placeholder="请输入你想要咨询的内容"> </el-input> <div class="chatInfor-input-button"> <div class="chatInfor-input-button-imglist"> <div class="chatInfor-input-button-imglist-item" v-for="(item, index) in imgList" :key="index"> <el-image :src="item" :zoom-rate="1.2" :max-scale="7" class="chatInfor-input-button-imglist-item-img" :min-scale="0.2" :preview-src-list="imgList" :initial-index="0" fit="cover" style="height:32px;" /> <SvgIcon name="chat-close" class="chat-close" @click="deleteImage(item, index)" /> <!-- <img :src="proxy.$loginUrl + item" class="chatInfor-input-button-imglist-item-img"> --> </div> </div> <div style="display: flex;align-items: center;justify-content: center;"> <el-upload v-model:file-list="fileList" class="upload-demo" :http-request="handleImageUpload" :before-upload="beforeUpload" :on-remove="handleRemove" list-type="picture" :show-file-list="false"> <SvgIcon name="chat-upload" class="chat-robot" v-if="!isRecepting" /> </el-upload> <SvgIcon name="chat-send" class="chat-robots" @click="sendMessage" v-if="!isRecepting" /> </div> <div class="loader-3" v-if="isRecepting"> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> <div class="circle"></div> </div> </div> </div> </div> </template> <script setup lang="ts"> import moment from "moment"; import { ref, getCurrentInstance, nextTick,onActivated } from 'vue' const { proxy } = getCurrentInstance() as any; import SvgIcon from '@/components/index.vue'; import { getChatDetails, uploadFile } from '@/api/chat' import MarkdownIt from 'markdown-it' import { ElScrollbar, ElMessage } from 'element-plus' import type { UploadProps, UploadUserFile } from 'element-plus' import useClipboard from 'vue-clipboard3'; import hqdInforImg from "@/assets/function/chat/person-image.png"; const md = new MarkdownIt() const loading = ref(false); const { toClipboard } = useClipboard() const disabled = ref(false); const fileList = ref<UploadUserFile[]>([]); const chatId = ref(''); const imgList: any = ref([]); const imgUrl: any = ref([]); const respContent = ref(''); const sendfileType = ref(1); const icon = localStorage.getItem('icon'); onActivated(()=>{ scrollToBottom() }) const textInput = ref(); const isRecepting = ref(false); //判断是否已完成发送 const infor = ref('') const svg = ` <path class="path" d=" M 30 15 L 28 17 M 25.61 25.61 A 15 15, 0, 0, 1, 15 30 A 15 15, 0, 1, 1, 27.99 7.5 L 15 15 " style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/> ` const chatList = ref<any>([]) const currentPost = ref('') const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>() const getData = async (val: any) => { loading.value = true; chatId.value = val; const res: any = await getChatDetails({ s_id: val, s_size: 99 }); if (res.status == 1) { console.log(res.json_data.data_list) res.json_data.data_list.map((item: any) => { item.progress = false; item.isRecepting = false; if (item.role == 'user') { item.content[0].imgList = []; item.content.map((items: any) => { if (items.type == 'image_url') { item.content[0].imgList.push(items.text) } }) } }) chatList.value = res.json_data.data_list; console.log(chatList.value) nextTick(() => { scrollToBottom() loading.value = false; }) } else { ElMessage({ message: '系统好像出了点问题呢', type: 'error', }) loading.value = false; } } //处理换行逻辑,ctrl+enter换行,enter发送消息 const lineBreak = () => { const textarea = textInput.value.textarea; const start = textarea.selectionStart; const end = textarea.selectionEnd; const value = textarea.value; const newValue = value.substring(0, start) + "\n" + value.substring(end); textarea.value = newValue; textarea.selectionStart = textarea.selectionEnd = start + 1; } //滚动条置底 const scrollToBottom = () => { const element: any = document.getElementById("chatBox"); scrollbarRef.value!.setScrollTop(element.clientHeight) }; //发送消息 const sendMessage = (e: any) => { e.preventDefault(); openWebSocketInfor(); } //翻译 const tranlate = (val: any, vals: any) => { isRecepting.value = true; val.isRecepting = true; const element: any = document.getElementById(`id${vals}`); const text = element.innerText; let ws = new WebSocket(`wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value }&s_type=1&s_chat=${text.replace(/#/g, "")}&s_other=`); val.content[0].text = ""; element.scrollIntoView(); ws.onmessage = e => { const response = JSON.parse(e.data); if (response.message === "success") { const data = response.data; if (data.is_stream) { // 开始接收信息 val.content[0].text += data.text_stream; isRecepting.value = true; } else { // val.content[0].text = data.text_stream; // resetData() val.isRecepting = false; val.progress = false; isRecepting.value = false; disabled.value = false; } } }; ws.onerror = handleError; } const updateChatList = (val1: any, val2: any) => { // if(val1) // const websocketurl = `wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value}&s_type=0&s_chat=${infor.value}&s_other=`; let contentList: any = []; contentList = val2; console.log(val2); // const contentList = [{ text: infor.value, type: 'text' }]; chatList.value.push({ role: 'user', created_at: moment(new Date()).format("YYYY-MM-DD HH:mm:ss"), role_info: { img: localStorage.getItem("icon") }, content: contentList, }) } const openWebSocketInfor = () => { console.log(infor) if (isRecepting.value == true) { ElMessage({ message: '请等待回答结束后再发起新的提问哦', type: 'warning', }) } else { let websocketurl = `wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value}&s_type=0`; respContent.value = ''; console.log(imgList.value) if (infor.value == '' && imgList.value.length == 0) { ElMessage({ message: '警告,发送的信息不能为空', type: 'warning', }) } else { let contentList = []; if (sendfileType.value == 2) { } else { if (imgList.value.length) { // contentList = imgList.value.map((item: any) => { // return { text: item, type: "text", imgList: } // }) contentList.push({ text: infor.value, type: 'text', imgList: imgList.value }) } else { contentList.push({ text: infor.value, type: 'text' }); } updateChatList("text", contentList); websocketurl += `&s_chat=${infor.value}&s_other=${imgUrl.value.join(',')}`; } nextTick(() => { scrollToBottom() // loading.value = false; }) sendMsgEvent(websocketurl) } } } const sendMsgEvent = (val: any) => { isRecepting.value = true; respContent.value = ''; let ws = new WebSocket(val); const currentItem = { isRecepting: true, role: "assistant", created_at: moment(new Date()).format("YYYY-MM-DD HH:mm:ss"), content: [{ text: "", annotations: [], type: "text" }], progress: true, }; chatList.value.push(currentItem) // const count = 1; ws.onopen = () => { console.log("连接成功"); }; ws.onmessage = e => { const response = JSON.parse(e.data); if (response.message == 'success') { const data = response.data; if (data.is_stream) { respContent.value += data.text_stream; isRecepting.value = true; infor.value = ''; } else { console.log(currentItem) // respContent.value = ''; currentItem.content[0].text = respContent.value; resetData() currentItem.isRecepting = false; currentItem.progress = false; isRecepting.value = false; disabled.value = false; } // chatList.value.push(currentItem) scrollToBottom() } else { isRecepting.value = false; ElMessage({ message: response.message, type: 'error', }) respContent.value = ''; } } ws.onerror = handleError } const handleError = (e: any) => { console.log(e); if (e.type == 'error') { ElMessage({ message: '系统出了点问题哦,请重新发起问题提问', type: 'error', }) } isRecepting.value = false; }; //重置输入框 const resetData = () => { infor.value = ''; imgList.value = []; imgUrl.value = []; } const handleRemove = () => { } //处理文件上传前的逻辑 const beforeUpload = (file: any) => { const isJPG = ["image/jpeg", "image/png"].includes(file.type); if (!isJPG) { ElMessage({ message: '上传文件封面只能是JPG/png格式!', type: 'error', }) } return isJPG; }; //上传文件 const handleImageUpload = async (file: any) => { const formData = new FormData(); formData.append("s_file", file.file); const data: any = await uploadFile(formData); imgUrl.value.push(data.file_path) imgList.value.push(proxy.$loginUrl + data.file_path); console.log(imgList.value) } //删除图片 const deleteImage = (val1: any, val2: any) => { console.log(val1, val2) imgList.value.splice(val2, 1) imgUrl.value.splice(val2, 1) } //重新发送 const reBuild = (val: any) => { console.log(1) console.log(val) let websocketurl = `wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value}&s_type=0`; if (isRecepting.value) { ElMessage({ message: '请等待回答结束后再发起新的提问哦', type: 'warning', }) return false; } // let resetItem = ''; let resetItem = findIndexAsk(val); chatList.value.push(resetItem); console.log(resetItem) // console.log(resetItem, '????') let imgList: any = []; const chaTtext = resetItem.content[0].text; if (resetItem.content[0].imgList) { const arr = resetItem.content[0].imgList.map(getUrlAfterStatic) imgList.value = arr; websocketurl += `&s_chat=${chaTtext}&s_other=${imgList.value.join(",")}`; } else { websocketurl += `&s_chat=${chaTtext}&s_other=`; } sendMsgEvent(websocketurl); nextTick(() => { scrollToBottom() // loading.value = false; }) } //截取字符 const getUrlAfterStatic = (url: string) => { const startIndex = url.indexOf('/static'); if (startIndex === -1) { return null; // 或者抛出一个错误,表示没有找到 /static } return url.substring(startIndex); } //根据Index查找提问的问题 const findIndexAsk = (index: any) => { let resetItem; const currentItem = chatList.value[index]; scrollToBottom(); if (currentItem.role !== "user") { for (let i = index; i > -1; i--) { const item = chatList.value[i]; if (item.role === "user") { resetItem = item; break; } } console.log(resetItem, 'gggg') } else { resetItem = currentItem; } return resetItem } //复制功能(注:复制内容只能是String类型) const onCopy = async (msg: any) => { console.log(msg.content[0]) try { // 复制 await toClipboard(msg.content[0].text) ElMessage({ message: '复制成功', type: 'success', }) console.log(1) // 复制成功 } catch (e) { // 复制失败 } } defineExpose({ getData }) </script> <style lang="scss"> .el-text { width: 100%; max-width: 928px; // min-width: 800px; // min-height: 24px; // margin-left: 64px; border: none !important; --el-input-border-color: none !important; --el-input-focus-border: none !important; --el-input-border: none !important; --el-input-focus-border-color: none !important; --el-input-hover-border-color: none !important; --el-input-clear-hover-color: none !important; border-radius: 0px 0px 8px 8px; .el-textarea__inner { // height: auto !important; // min-height: 24px !important; font-size: 16px; padding: 16px 16px 16px 16px; min-height: 48px !important; max-height: 80px !important; line-height: 24px; // line-height: 56px !important; border: none !important; border-radius: 8px 8px 0px 0px; // border-radius: 8px; } } </style> <style lang="scss" scoped> .upload-demo { display: flex; justify-content: center; align-items: center } .chatInfor { max-width: 1048px; position: absolute; left: 50%; transform: translateX(-50%); // min-width: 920px; overflow: auto; position: relative; // padding: 0px 14%; height: 100%; text-align: left; &-content { height: 85%; max-width: 1048px; // overflow-y: scroll; width: 100%; &-item { height: 100%; width: 100%; padding: 0px 60px; font-size: 15px; line-height: 24px; letter-spacing: .5px; &-time { color: #A5A5A5; font-size: 12px; line-height: 20px; ; text-align: center; margin-top: 32px; } &-users { margin-top: 16px; display: flex; justify-content: right; width: 100%; // align-items: right; img { width: 48px; height: 48px; border-radius: 100px; } // p { width: fit-content; background-color: var(--vt-c-green); padding: 12px 16px; border-radius: 24px 0px 24px 24px; color: var(--vt-c-selected); margin-right: 16px; max-width: calc(100% - 128px); img { height: 72px; width: 100%; } } } &-chats { margin-top: 16px; display: flex; justify-content: left; position: relative; img { width: 48px; height: 48px; } p { margin-left: 16px; width: 100%; background-color: var(--vt-c-white); padding: 12px 16px; max-width: calc(100% - 128px); position: relative; border-radius: 0px 16px 16px 16px; color: var(--vt-c-selected); overflow-x: auto; } &-button { float: right; // position: absolute; margin-top: 16px; // right: 0px; margin-right: 5px; height: 16px; // margin-bottom:02px; display: flex; align-items: center; cursor: pointer; .ebutton { display: flex; align-items: center; color: #4E5969; .chat-btn { width: 20px; height: 20px; margin-left: 24px; } span { margin-left: 4px; font-size: 14px; } } .ebutton:hover { color: #85E822; } } } } } .active { background-color: var(--el-fill-color-light); &-input { &-button { background-color: var(--el-fill-color-light); } } } &-input { // height: 85%; // max-width: 928px; // min-width: 928px; // min-width: 800px; position: relative; width: calc(100% - 248px); margin-left: 124px; // border-radius: 8px; &-button { // position: absolute; // right: 75px; // bottom: 10px; width: 100%; max-width: 928px; // min-width: 800px; height: 36px; z-index: 100; // background-color: #fff; display: flex; align-items: center; // position: absolute; // flex-direction: row-reverse; justify-content: space-between; // text-align: right; // margin-left: 64px; background-color: #fff; border-radius: 0px 0px 8px 8px; &-imglist { width: 80%; height: 100%; display: flex; align-items: center; &-item { margin-left: 16px; position: relative; // width:12px; img { height: 32px; width: auto; } } } // flex-direction: row-reverse; .chat-robots { width: 24px; height: 24px; margin-right: 16px; cursor: pointer; } .chat-robot { width: 20px; height: 20px; margin-right: 16px; cursor: pointer; } .chat-close { position: absolute; width: 14px; height: 14px; right: -7px; top: -5px; cursor: pointer; } } } &-inputs { width: 800px; } // &-input:hover { // // margin-left:64PX; // border: 1px solid var(--el-color-primary); // max-width: 928px; // border-radius: 8px; // min-width: 800px; // position: relative; // width: calc(100% - 128px); // margin-left: 64px; // } // overflow-x: auto; // overflow-y: scroll; } /* From Uiverse.io by G4b413l */ /* From Uiverse.io by mobinkakei */ .loader-3 { width: 9em; display: flex; justify-content: space-evenly; } .circle { width: 8px; height: 8px; border-radius: 50%; position: relative; } .circle:nth-child(1) { background-color: #90be6d; } .circle:nth-child(2) { background-color: #f9c74f; } .circle:nth-child(3) { background-color: #f8961e; } .circle:nth-child(4) { background-color: #f3722c; } .circle:nth-child(5) { background-color: #f94346; } .circle::before { content: ""; width: 100%; height: 100%; position: absolute; border-radius: 50%; opacity: 0.5; animation: animateLoader38 2s ease-out infinite; } .circle:nth-child(1)::before { background-color: #90be6d; } .circle:nth-child(2)::before { background-color: #f9c74f; animation-delay: 0.2s; } .circle:nth-child(3)::before { background-color: #f8961e; animation-delay: 0.4s; } .circle:nth-child(4)::before { background-color: #f3722c; animation-delay: 0.6s; } .circle:nth-child(5)::before { background-color: #f94346; animation-delay: 0.8s; } @keyframes animateLoader38 { 0% { transform: scale(1); } 50%, 75% { transform: scale(2.5); } 80%, 100% { opacity: 0; } } .text { color: black; font-weight: bolder; } .loader { position: relative; display: flex; align-items: center; gap: 0.3em; margin-top: 3px; // overflow-y: auto } .loader::before { content: ""; position: absolute; left: 0; top: 10px; width: 100%; // height: 2em; filter: blur(45px); background-color: #e299ff; background-image: radial-gradient(at 52% 57%, hsla(11, 83%, 72%, 1) 0px, transparent 50%), radial-gradient(at 37% 57%, hsla(175, 78%, 66%, 1) 0px, transparent 50%); } .loader__circle { --size__loader: 0.6em; width: var(--size__loader); height: var(--size__loader); border-radius: 50%; animation: loader__circle__jumping 2s infinite; background-color: #b499ff; } .loader__circle:nth-child(2n) { animation-delay: 300ms; background-color: #e499ff; } .loader__circle:nth-child(3n) { animation-delay: 600ms; } @keyframes loader__circle__jumping { 0%, 100% { transform: translateY(0px); } 25% { transform: translateY(-15px) scale(0.5); } 50% { transform: translateY(0px); } 75% { transform: translateY(5px) scale(0.9); } } </style>