Files
gangyan/chat_web_front/src/views/chat/index.vue
liuguancen 108022cebd feat: 品牌升级(知冶→战知) + 应用工具广场重构 + 新增工具集成
品牌升级:
- 全站品牌从"知冶"更名为"战知"
- 更换 favicon、侧边栏 logo、登录页 logo
- 更新登录页标语和首页欢迎语

应用广场重构:
- 从后端数据库驱动改为前端静态配置,按分类 tab 展示
- 新增工具卡片 UI,支持 logo 图片和 emoji 图标

新增工具部署:
- Stirling PDF (端口18080) - PDF 处理工具箱
- Excalidraw (端口18081) - 手绘风格白板,集成 AI 绘图
- TrWebOCR (端口18083) - 中文离线 OCR
- LibreTranslate (端口18084) - 中英翻译引擎
- PPTist (端口18085) - 在线 PPT 编辑器
- PPTist AI 后端 (端口18086) - 对接 deepseek-v3 生成大纲/PPT/写作
- Excalidraw AI 代理 (端口18082) - 对接 deepseek-v3 生成 Mermaid 图

其他:
- 智能场景仅保留"选题推荐"
- vite 代理配置增加 /pdf/ 和 /draw/ 路由

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:24:51 +08:00

1567 lines
41 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="chat">
<div class="scenarioBtn" @click="showScenario">
<img
src="../../assets/images/chat/scenarioImg.png"
class="scenarioImg"
alt=""
/>
<span class="scenarioText">场景</span>
</div>
<ChatHistory
ref="chatHistory"
@showHistory="showHistory"
v-show="historyStatus"
:status="historyStatus"
@newChat="newChat"
@viewHistory="viewHistory"
@updateHistoryTitle="updateHistoryTitle"
></ChatHistory>
<div class="chatAll" ref="chatDiv">
<div class="content" v-if="!chatStatus">
<div class="title">
<span>聚尖端之力创多维平台</span>
</div>
<div class="titleInfo">
聚合科技动能扩展创新疆界引领行业跃迁升级
</div>
<div class="operate">
<div class="scenario">
<div
class="scenarioItem"
v-for="(item, index) in fetchSmartPromptListBack"
:key="index"
@click="selectScenarioItem(item)"
>
<div class="scenarioItemContainer">
<div class="scenarioItemTitle">{{ item.title }}</div>
<div class="scenarioItemInfo" :title="item.desc">
{{ item.desc }}
</div>
</div>
<img src="../../assets/images/chat/jump.png" alt=""/>
</div>
</div>
<div class="carousel">
<el-carousel arrow="never" pause-on-hover height="292px">
<el-carousel-item>
<router-link to="/writing">
<img
class="carouselImg"
src="../../assets/images/chat/carouselWriting.png"
alt=""
/>
</router-link>
</el-carousel-item>
<el-carousel-item>
<router-link to="/knowledgeBase">
<img
class="carouselImg"
src="../../assets/images/chat/carouselStudy.png"
alt=""
/>
</router-link>
</el-carousel-item>
<!-- <el-carousel-item>
<img
class="carouselImg"
src="../../assets/images/chat/carouselTranslation.png"
alt=""
/>
</el-carousel-item>-->
</el-carousel>
</div>
</div>
</div>
<div
class="dialogue"
:style="{ width: `${widthChat}px` }"
v-if="chatStatus"
>
<div class="fixedTop">
<span class="chatTitle" :title="chatTitle" v-show="!changeTitleStatus">{{
chatTitle
}}</span>
<el-input
v-model="chatTitle"
@blur="changeTtile"
@keydown.enter="changeTtile"
v-if="changeTitleStatus"
/>
<el-dropdown v-show="!changeTitleStatus" trigger="click">
<img
src="../../assets/images/chat/select.png"
class="selectImg"
alt=""
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@click="
() => {
changeTitleStatus = true;
}
"
>修改名称
</el-dropdown-item
>
<el-dropdown-item @click="delChat"
><span style="color: red">删除</span></el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="blurryWrap" v-if="scrollTop != 0">
<div class="blurry"></div>
</div>
<div class="dialogueView" ref="scrollableDiv" @scroll="onScroll">
<div class="chatDialogue">
<div class="chatContent">
<Message
v-for="(item, index) in chatInfo"
:key="index"
:chatInfo="item"
@setTextarea="setTextarea"
@restSend="restSend"
@delChatInfo="delChatInfo(item, index)"
@send="send"
/>
</div>
</div>
</div>
</div>
<div class="blurryWrap">
<div class="blurry" v-if="scrollBottom > 1 && scrollTop != 0"></div>
</div>
<div>
<div class="interlocution">
<div class="btns">
<div class="btn" @click="newChat">
<div class="btnImg newChatImg"/>
<span>新建对话</span>
</div>
<div
class="btn"
:class="{ btned: historyStatus }"
style="margin-left: 16px"
@click="showHistory()"
>
<div class="btnImg historyImg" style="width: 24px;height: 24px"/>
<span>历史对话</span>
</div>
</div>
<div class="chatTextarea">
<div class="chatScenario" v-show="scenaried != ''">
<span class="chatScenarioText">{{ scenarioTitle }}</span>
<span class="exitChatScenario" @click="exitChatScenario"
>退出场景</span
>
</div>
<el-input
v-model="textarea"
:rows="1"
:class="{ noPlaceholder: noPlaceholder }"
:autosize="{ maxRows: 6 }"
type="textarea"
:placeholder="
placeholder != ''
? placeholder
: '请输入你想提的问题字数不能超过1000字'
"
@keydown.enter="keyDown"
/>
<div class="chatOperates">
<div class="chatOperatesLeft">
<div :class="isDeepThink?'deep-think deep-think-active':'deep-think'" @click="changeDeepThinkStatus()">
<img src="@/assets/images/chat/deepThink.png" v-show="!isDeepThink" alt="">
<img src="@/assets/images/chat/deepThinkActive.png" v-show="isDeepThink" alt="">
<span>DeepSeek-R1</span>
</div>
</div>
<div class="chatOperatesRight">
<el-upload
v-model:file-list="fileList"
class="upload-demo"
:action="`${baseURL}/gpt/file/upload/temp_knowledgeBase`"
:before-upload="beforeAvatarUpload"
:headers= generateAuthInfo()
accept=".xlsx, .xls, .doc, .pdf, .md, .docx,.png,.jpg"
name="files"
:data="{ chatNumber }"
:on-success="handleAvatarSuccess"
:on-error="handleAvatarError"
:show-file-list="false"
>
<img
src="../../assets/images/chat/file.png"
class="fileImg"
alt=""
/>
</el-upload>
<div
class="sendBtn"
:class="{ notAllowed: !notAllowed }"
@click="debounce(() => { send(); })"
v-show="!sendStatus"
>
<img
src="../../assets/images/chat/send.png"
class="sendImg"
alt=""
v-show="!notAllowed"
/>
<img
src="../../assets/images/chat/sendWhite.png"
class="sendImg"
alt=""
v-show="notAllowed"
/>
<span>发送</span>
</div>
<div class="stopBtn" @click="handleStop" v-show="sendStatus">
<img
src="../../assets/images/chat/stopChat.png"
class="stopImg"
alt=""
/>
<span>停止响应</span>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
以上内容均由AI生成, 仅供参考和借鉴©{{
currentYear
}}{{ PROJECT_PROPERTIES.projectName }}_浪潮软件科技有限公司
<!-- <a
target="_blank"
style="color: #0969da; text-decoration: none"
>用户协议</a
>
|
<span class="clicked">隐私政策</span>-->
</div>
</div>
</div>
<Scenario
@showScenario="showScenario"
ref="scenarioRef"
v-show="scenarioStatus"
:fetchSmartPromptList="fetchSmartPromptList"
:scenaried="scenaried"
@selectScenarioItem="selectScenarioItem"
@getFetchSmartPrompt="getFetchSmartPrompt"
:chatType="chatType"
:isDeepThink="isDeepThink"
@changeDeepThinkStatus="changeDeepThinkStatus"
/>
</div>
</template>
<script setup lang="ts">
import {nextTick, onMounted, onUnmounted, reactive, ref, watch,} from "vue";
import {
deleteChatMessageById,
delHistoryChatById,
editHistoryChatTitleById,
fetchChatAPI,
fetchConversationId,
fetchSmartPrompt,
interrupt,
getTitle
} from "@/api";
import Message from "@/components/Message.vue";
import ChatHistory from "@/components/ChatHistory.vue";
import Scenario from "./Scenario.vue";
import type {UploadProps, UploadUserFile} from "element-plus";
import {ElMessage} from "element-plus";
import {useAuthStore} from "@/store";
import {PROJECT_PROPERTIES} from "@/config/properties";
import {debounce, streamResultVerify} from "@/utils";
import {generateAuthInfo} from "@/utils/request/headerUtils";
import {useRoute} from "vue-router";
const route = useRoute();
// 对话输入框
const textarea = ref<string>("");
//是否在对话中
const chatStatus = ref<boolean>(false);
//历史记录展示
const historyStatus = ref<boolean>(false);
//场景模块展示
const scenarioStatus = ref<boolean>(false);
//当前对话是否为第一条消息
const firstChat = ref(true);
// 对话类型0普通对话 1文件对话
const chatType = ref(0);
const chatNumber = ref("");
const conversationId = ref("");
// 对话内容
const chatInfo = ref([]);
// 上传文件时禁止发送
const notAllowed = ref(false);
const noPlaceholder = ref(false);
const changePlaceholder = (val) => {
noPlaceholder.value = val;
};
// text提示语
const placeholder = ref("请输入你想提的问题字数不能超过1000字");
// 发送,停止响应切换
const sendStatus = ref<boolean>(false);
const baseURL = import.meta.env.VITE_GLOB_API_CTX;
// 选择的场景key
const scenaried = ref("");
// 对话标题
const chatTitle = ref("未命名");
// 场景value
const scenarioTitle = ref("");
// 对话id
const chatId = ref(-1);
// 文件内容
const fileContext = ref("");
// 标题是否在修改中
const changeTitleStatus = ref(false);
const knowledgeId = ref("");
// 文件名
const fileName = ref("");
// 当前年份
const currentYear = ref("");
let controller = null;
// 历史记录
const chatHistory = ref(null);
// 删除某条对话
const delChatInfo = async (item, index) => {
try {
await deleteChatMessageById(item.user.conversationId);
chatInfo.value.splice(index, 1);
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
};
// 查看历史对话
const viewHistory = (data) => {
chatInfo.value = [];
firstChat.value = false;
chatStatus.value = true;
chatType.value = data.chatType;
chatTitle.value = data.chatTitle;
scenaried.value = data.scenaried;
placeholder.value = data.placeholder;
chatId.value = data.chatId;
scenarioTitle.value = data.scenarioTitle;
chatNumber.value = data.chatNumber;
let chatInfoBack = [...data.chatInfo];
let info = {};
if (data.chatType === 1) {
fileName.value = data.fileName;
knowledgeId.value = data.knowledgeId;
fileContext.value = data.fileContext;
const suffix = data.fileName.match(/[^.]+$/)[0];
info = {
user: {content: data.fileName, suffix},
};
chatInfo.value.push(info);
chatInfoBack.shift();
}
for (let i = 0; i < chatInfoBack.length;) {
let index = chatInfoBack[i + 1].content.indexOf("**参考文献:**");
let aiStrDoc = "";
let aisStr = "";
if (
chatInfoBack[i + 1].content &&
chatInfoBack[i + 1].content.indexOf("**参考文献:**") != -1
) {
aiStrDoc = chatInfoBack[i + 1].content.substring(index + 9);
aisStr =
chatInfoBack[i + 1].content.substring(0, index + 9) +
`<div class="docs">\n${aiStrDoc}</div>`;
} else {
aisStr = chatInfoBack[i + 1].content;
}
info = {
user: {
content: chatInfoBack[i].content,
conversationId: chatInfoBack[i].messageId,
},
ai: { content: aisStr, thinkContent: chatInfoBack[i + 1].think},
};
chatInfo.value.push(info);
i = i + 2;
}
};
// 更新历史对话名称
const updateHistoryTitle = (data) => {
if (chatId.value === data.id) {
chatTitle.value = data.prompt;
}
};
// 重新发送
const restSend = (text) => {
textarea.value = text;
send();
};
// 修改对话标题
const changeTtile = async () => {
if (chatTitle.value.trim() === "") {
ElMessage.warning("对话名称不可以为空!");
return;
}
try {
let res = await editHistoryChatTitleById({
id: chatId.value,
prompt: chatTitle.value,
});
if (res && res.code === 200) {
ElMessage.success("修改成功");
if (historyStatus.value) {
chatHistory.value.getHistoryList();
}
changeTitleStatus.value = false;
return;
}
ElMessage.error("修改失败");
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
};
// 设置输入框的值
const setTextarea = (text) => {
textarea.value = text;
};
// 删除对话
const delChat = async () => {
try {
let res = await delHistoryChatById(chatId.value);
if (res && res.code === 200) {
ElMessage.success("删除成功");
if (historyStatus.value) {
chatHistory.value.getHistoryList();
}
newChat();
return;
}
ElMessage.error("删除失败");
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
};
// 场景选择
const selectScenarioItem = (item) => {
if (isDeepThink.value) {
ElMessage.warning("您已选择智慧场景,已为您取消深度思考");
changeDeepThinkStatus(false);
}
scenaried.value = item.name;
scenarioTitle.value = item.title;
placeholder.value = item.prompt;
};
// 停止响应
const handleStop = async () => {
controller.abort();
controller = null;
const content = chatInfo.value[chatInfo.value.length - 1].ai.content;
let messageData = {
messageId: conversationId.value,
content,
};
try {
await interrupt(messageData);
} catch (error: any) {
ElMessage.warn('响应已停止');
}
if (
chatInfo.value[chatInfo.value.length - 1].ai.content == "" ||
chatInfo.value[chatInfo.value.length - 1].ai.content == "思考中..."
) {
chatInfo.value[chatInfo.value.length - 1].ai.content = "已停止响应";
}
sendStatus.value = false;
};
// 退出场景
const exitChatScenario = () => {
scenaried.value = "";
scenarioTitle.value = "";
placeholder.value = "请输入你想提的问题字数不能超过1000字";
};
// 场景列表
let fetchSmartPromptList = ref([]);
//场景列表备份
let fetchSmartPromptListBack = ref([]);
//文件列表
const fileList = ref<UploadUserFile[]>([]);
// 上传文件
const beforeAvatarUpload: UploadProps["beforeUpload"] = async (rawFile) => {
if (scenaried.value) {
ElMessage.warning("您正在进行文件对话,已退出智慧场景。");
}
if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.error("文件大小不能超过10M");
return false;
}
notAllowed.value = true;
const suffix = rawFile.name.match(/[^.]+$/)[0];
newChat();
// chatTitle.value = rawFile.name;
chatType.value = 1;
scenaried.value = "";
scenarioTitle.value = "";
placeholder.value = "请输入你想提的问题字数不能超过1000字";
if (firstChat.value) {
await getFetchChatAPI(rawFile.name);
}
const info = reactive({
user: {content: rawFile.name, suffix},
});
chatInfo.value.push(info);
return true;
};
// 文件上传成功
const handleAvatarSuccess: UploadProps["onSuccess"] = (
response,
uploadFile
) => {
response.data.data.context.length > 30000
? (fileContext.value = response.data.data.summary)
: (fileContext.value = response.data.data.context);
knowledgeId.value = response.data.data.id;
fileName.value = response.fileName;
firstChat.value ? (firstChat.value = false) : "";
notAllowed.value = false;
};
// 上传失败
const handleAvatarError: UploadProps["onError"] = (error) => {
chatType.value = 0;
ElMessage.error(error);
};
// 新建对话
const newChat = () => {
firstChat.value = true;
chatStatus.value = true;
chatTitle.value = "未命名";
chatType.value = 0;
sendStatus.value = false;
chatInfo.value = [];
};
onUnmounted(() => {
if (controller) {
controller.abort();
}
// 销毁全局的回车事件
//window.removeEventListener("keydown", keyDown, false);
});
// 获取场景
const getFetchSmartPrompt = async () => {
try {
const res = await fetchSmartPrompt();
fetchSmartPromptList.value = [];
fetchSmartPromptListBack.value = [];
let k = 0;
for (let i in res.data.llm_chat) {
for (let m in res.data.llm_chat[i].prompt) {
fetchSmartPromptList.value.push(res.data.llm_chat[i].prompt[m]);
if (k < 3) {
fetchSmartPromptListBack.value.push(res.data.llm_chat[i].prompt[m]);
}
k++;
}
}
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
};
watch(() => route.path, async (newValue, oldValue) => {
if (newValue !== oldValue && newValue === '/index') {
window.location.reload();
}
if (newValue !== oldValue && newValue === '/chat') {
newChat();
}
})
// 获取对话 chatNumber chatIdpromt是对话
const getFetchChatAPI = async (prompt) => {
const chatData = {
prompt,
type: chatType.value,
};
try {
const fetchChatAPIData = await fetchChatAPI(chatData);
chatNumber.value = fetchChatAPIData.data.chatNumber;
chatId.value = fetchChatAPIData.data.id;
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
};
const autoScroll = ref(true);
//根据所写的问题获取conversationId
const getFetchConversationId = async (prompt) => {
let model = isDeepThink.value ? 'R1-70B' : 'QIANWEN'
const messageData = {
chatNumber: chatNumber.value,
prompt,
model: model,
promptName: scenaried.value != "" ? scenaried.value : "default",
promptTitle: scenarioTitle.value != "" ? scenarioTitle.value : "default",
promptPrompt: placeholder.value,
chatId: chatId.value,
};
try {
const fetchConversationIdData = await fetchConversationId(messageData);
conversationId.value = fetchConversationIdData.data;
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
};
// 对话,流式对话
const getFetchChatAPIProcess = async () => {
const info = reactive({
user: { content: textarea.value.trim(), conversationId: "" },
ai: { content: "", question: [], contentStatus: false, thinkContent: '' },
});
chatInfo.value.push(info);
textarea.value = "";
let aiQuestion = ``;
if (!controller) {
controller = new AbortController();
}
if (chatType.value === 0) {
const response = await fetch(
`${baseURL}/app/chat/stream?conversationId=${
conversationId.value
}&promptName=${scenaried.value != "" ? scenaried.value : "default"}`,
{
method: "GET",
headers: {
Accept: "text/plain",
Authorization: `Bearer ${useAuthStore().token}`
},
signal: controller.signal,
}
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
let result = "";
let picTemp = false;
// 逐块读取流数据
while (!done) {
const {value, done: doneReading} = await reader.read();
done = doneReading;
result += decoder.decode(value, {stream: true});
// 校验token是否失效
streamResultVerify(result);
// 分割数据并逐条展示
const lines = result.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
let lineObj = JSON.parse(lines[i]);
if (
!info.user.conversationId ||
info.user.conversationId != lineObj.conversationId
) {
info.user.conversationId = lineObj.conversationId;
}
if (lineObj.question) {
aiQuestion = lineObj.question;
} else if (lineObj.think) {
info.ai.thinkContent += lineObj.think;
} else if (lineObj.text) {
let index = info.ai.content.indexOf("思考中...");
if (index != -1) {
let text = info.ai.content.substring(0, index);
info.ai.content = text;
}
if (info.ai.content.indexOf(".png)") != -1 && info.ai.content.indexOf("http") != -1) {
info.ai.content = info.ai.content.replace("(http", "http");
info.ai.content = info.ai.content.replace(".png)", ".png");
picTemp = true;
}
info.ai.content += lineObj.text;
} else if (lineObj.docs) {
info.ai.content += `\n**参考文献:**`;
info.ai.content += `<div class="docs">\n${lineObj.docs}</div>`;
}
}
// 保留最后一块数据,等待下一次读取
result = lines[lines.length - 1];
// 自动滚动到最新位置
}
if (picTemp) {
info.ai.content = info.ai.content.replace("http", "(http");
info.ai.content = info.ai.content.replace(".png", ".png)");
}
aiQuestion=aiQuestion.replace(/\n\n/g, "<strip>");
let questionArr = aiQuestion.split('<strip>');
for (let i = 0; i < questionArr.length; i++) {
if(questionArr[i].trim()){
let start = questionArr[i].indexOf(":");
info.ai.question.push(questionArr[i].slice(start + 1).trim());
}
}
} else {
// 文件对话
const response = await fetch(`${baseURL}/gpt/file/fileTalk`, {
method: "POST",
headers: {
Accept: "text/plain",
Authorization: `Bearer ${useAuthStore().token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
conversationId: conversationId.value,
knowledgeId: knowledgeId.value,
fileName: fileName.value,
context: fileContext.value,
}),
signal: controller.signal,
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
let result = "";
// 逐块读取流数据
while (!done) {
const {value, done: doneReading} = await reader.read();
done = doneReading;
result += decoder.decode(value, {stream: true});
// 校验token是否失效
streamResultVerify(result);
// 分割数据并逐条展示
const lines = result.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
let lineObj = JSON.parse(lines[i]);
if (
!info.user.conversationId ||
info.user.conversationId != lineObj.conversationId
) {
info.user.conversationId = lineObj.conversationId;
}
if (lineObj.question) {
aiQuestion = lineObj.question;
} else if (lineObj.answer) {
let index = info.ai.content.indexOf("思考中...");
if (index != -1) {
let text = info.ai.content.substring(0, index);
info.ai.content = text;
}
info.ai.content += lineObj.answer;
} else if (lineObj.docs) {
info.ai.content += `\n**参考文献:**`;
info.ai.content += `<div class="docs">\n${lineObj.docs}</div>`;
}
}
// 保留最后一块数据,等待下一次读取
result = lines[lines.length - 1];
}
aiQuestion=aiQuestion.replace(/\n\n/g, "<strip>");
let questionArr = aiQuestion.split(`<strip>`);
for (let i = 0; i < questionArr.length; i++) {
if(questionArr[i].trim()){
let start = questionArr[i].indexOf(":");
info.ai.question.push(questionArr[i].slice(start + 1).trim());
}
}
}
};
// 发送消息
let send = async () => {
if (textarea.value.trim() === "" || sendStatus.value) {
return;
}
autoScroll.value = true;
sendStatus.value = true;
if (chatInfo.value.length > 0 && chatType.value === 0) {
chatInfo.value[chatInfo.value.length - 1].ai.question = [];
}
//如果是第一次创建对话先获取对话的idchatNumber并且刷新历史记录
if (firstChat.value) {
await getFetchChatAPI(textarea.value.trim());
if (historyStatus.value) {
chatHistory.value.getHistoryList(false);
}
}
//获取conversationId
await getFetchConversationId(textarea.value.trim());
if (!chatStatus.value) {
chatStatus.value = true;
}
//流式会话
await getFetchChatAPIProcess();
firstChat.value ? (firstChat.value = false) : "";
sendStatus.value = false;
// 获取对话名称
await createTitle();
// 刷新历史记录
if (historyStatus.value) {
chatHistory.value.getHistoryList(false);
}
};
/**
* 根据对话获取对话名称
*/
const createTitle = async () => {
if (chatTitle.value !== "未命名" ) return;
let firstChat;
if (chatType.value === 0 ) {
if (chatInfo.value.length !== 1) return;
firstChat = chatInfo.value[0];
} else {
if (chatInfo.value.length !== 2) return;
firstChat = chatInfo.value[1];
}
let params = {
conversation_id: chatNumber.value,
history: [
{
role: 'user',
content: firstChat.user.content,
}, {
role: "assistant",
content: firstChat.ai.content
}
]
}
try {
let res = await getTitle(params);
if (res && res.code === 200) {
chatTitle.value = res.data.title;
}
} catch (error: any) {
ElMessage.error(error && error.message ? error.message : '未知错误');
}
}
/**
* 发送按钮的回车事件
*/
let keyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!sendStatus.value) {
debounce(() => {
send();
});
}
}
};
/**
* 控制历史对话框的显隐
*/
function showHistory() {
if (historyStatus.value) {
historyStatus.value = false;
} else {
historyStatus.value = true;
}
}
// 展示场景
function showScenario(status) {
scenarioStatus.value = status;
}
const widthChat = ref(0);
// 使用 ref 定义响应式变量
const chatDiv = ref<HTMLElement | null>(null);
// 计算右边距函数
const calculateRightMargin = () => {
if (chatDiv.value) {
const rect = chatDiv.value.getBoundingClientRect();
const viewportWidth = window.innerWidth;
widthChat.value = viewportWidth - rect.left - 1;
} else {
console.warn("元素未挂载或未找到");
}
};
const scrollableDiv = ref<HTMLDivElement | null>(null);
const scrollTop = ref<number>(0);
const scrollBottom = ref<number>(0);
const onScroll = () => {
if (scrollableDiv.value) {
if (scrollBottom.value > 1 && scrollTop.value != 0) {
autoScroll.value = false;
}
scrollTop.value = scrollableDiv.value.scrollTop;
scrollBottom.value =
scrollableDiv.value.scrollHeight -
scrollableDiv.value.scrollTop -
scrollableDiv.value.clientHeight;
}
};
const scrollToBottom = () => {
if (scrollableDiv.value) {
scrollableDiv.value.scrollTop = scrollableDiv.value.scrollHeight;
}
};
/**
* 深度思考
*/
const isDeepThink = ref<boolean>(false);
const changeDeepThinkStatus = (status: boolean = !isDeepThink.value) => {
if (scenaried.value && status) {
ElMessage.warning("您已选择深度思考,为您自动退出智慧场景。");
scenaried.value = "";
scenarioTitle.value = "";
placeholder.value = "请输入你想提的问题字数不能超过1000字";
}
isDeepThink.value = status;
}
watch(
chatInfo,
() => {
if (autoScroll.value) {
nextTick(() => {
scrollToBottom();
});
}
},
{deep: true}
);
watch(
[historyStatus, scenarioStatus],
() => {
nextTick(() => {
calculateRightMargin();
});
},
{immediate: true}
);
watch(textarea, (newValue) => {
if (newValue.trim() === "") {
notAllowed.value = false;
} else {
notAllowed.value = true;
}
});
onMounted(() => {
getFetchSmartPrompt();
nextTick(() => {
onScroll(); // 初始化时调用一次以设置初始值
});
window.addEventListener("resize", calculateRightMargin);
// 获取当前年份
currentYear.value = new Date().getFullYear();
// 添加全局回车事件
// window.addEventListener("keydown", keyDown);
});
</script>
<style lang="scss" scoped>
.chat {
display: flex;
flex: 1;
height: 100vh;
.scenarioBtn {
width: 96px;
height: 42px;
background: #fafbff;
border-radius: 8px 8px 8px 8px;
border: 1px solid #d5ddff;
position: absolute;
right: 32px;
top: 32px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.scenarioImg {
width: 24px;
height: 20px;
}
.scenarioText {
margin-left: 8px;
font-weight: 400;
font-size: 16px;
color: #004ea0;
line-height: 30px;
}
}
.chatAll {
margin: 0 auto;
width: 49.479vw;
max-width: 950px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
// .contentWrap {
// // z-index: 9999;
// margin-top: 19.46vh;
// overflow-y: auto;
// }
.content {
// width: 100%;
// width: 49.479vw;
overflow-y: auto;
margin-top: 19.46vh;
position: relative;
.title {
font-weight: bold;
font-family: "PingFangSC-Bold";
font-size: 52px;
color: #000000;
line-height: 58px;
.titleHead {
color: #004ea0;
}
}
.titleInfo {
margin-top: 16px;
font-weight: 400;
font-size: 20px;
color: #000000;
line-height: 30px;
text-align: left;
}
.operate {
display: flex;
margin-top: 40px;
width: 100%;
.scenario {
display: flex;
flex-direction: column;
height: 292px;
width: calc(100% - 262px);
flex: 1;
justify-content: space-between;
.scenarioItem {
img {
width: 7px;
height: 14px;
}
cursor: pointer;
width: 100%;
height: 84px;
background: #e6edff;
border-radius: 8px 8px 8px 8px;
border: 1px solid #fafbff;
padding-left: 20px;
padding-right: 20px;
display: flex;
justify-content: space-between;
align-items: center;
.scenarioItemContainer {
width: calc(100% - 10px);
.scenarioItemTitle {
font-weight: bold;
font-family: "PingFangSC-Bold";
font-size: 18px;
color: #000000;
line-height: 30px;
}
.scenarioItemInfo {
font-weight: 400;
font-size: 16px;
color: #858a94;
line-height: 30px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
.carousel {
width: 262px;
height: 292px;
margin-left: 20px;
.carouselImg {
cursor: pointer;
width: 100%;
height: 100%;
}
::v-deep .el-carousel__button {
width: 10px;
}
::v-deep .el-carousel__indicators .el-carousel__indicator.is-active {
.el-carousel__button {
background-color: #004ea0;
}
}
}
}
}
.content::-webkit-scrollbar {
width: 0; /* 隐藏滚动条 */
background: transparent; /* 保持背景透明 */
}
/* 针对Firefox */
.content {
scrollbar-width: none; /* 隐藏滚动条 */
}
.blurryWrap {
height: 30px;
width: 49.479vw;
max-width: 950px;
position: relative;
.blurry {
height: 20px;
width: 100%;
position: absolute;
top: -20px;
// background-color: #edf2ffcc; /* 半透明背景 */
background: linear-gradient(
to top,
#edf2ff 0%,
rgba(255, 255, 255, 0) 100%
);
z-index: 10;
pointer-events: none;
// border-top-left-radius: 5px;
// border-top-right-radius: 5px;
}
}
.dialogue {
display: flex;
flex-direction: column;
flex: 1;
// margin-bottom: 41px;
overflow-y: auto;
.blurryWrap {
height: 1px;
width: 49.479vw;
max-width: 950px;
position: relative;
.blurry {
height: 20px;
width: 100%;
position: absolute;
top: 40px;
// background-color: #edf2ffcc; /* 半透明背景 */
background: linear-gradient(
to top,
rgba(255, 255, 255, 0) 0%,
#edf2ff 100%
);
z-index: 10;
pointer-events: none;
// border-top-left-radius: 5px;
// border-top-right-radius: 5px;
}
}
.fixedTop {
width: 49.479vw;
max-width: 950px;
display: flex;
justify-content: center;
align-items: center;
margin-top: 23px;
height: 25px;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 30px;
.el-input {
margin-right: 12px;
width: 200px;
}
.chatTitle {
margin-right: 12px;
max-width: 250px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
}
.selectImg {
width: 8px;
height: 4px;
cursor: pointer;
}
:focus-visible {
outline: none !important;
}
}
.dialogueView {
margin-top: 42px;
height: calc(100% - 48px);
width: 100%;
overflow-y: auto;
.chatDialogue {
height: 100%;
width: 49.479vw;
max-width: 950px;
.chatContent {
width: 100%;
}
}
}
}
.interlocution {
width: 100%;
margin-bottom: 16px;
// z-index: 99999;
.btns {
display: flex;
.btn {
width: 124px;
height: 42px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: #ffffff;
border-radius: 8px;
border: 1px solid #d5ddff;
font-weight: 400;
font-size: 16px;
color: #858a94;
line-height: 30px;
.btnImg {
width: 20px;
height: 20px;
margin-right: 8px;
}
.newChatImg {
background: url("../../assets/images/chat/newChat.png") no-repeat center;
background-size: 100%;
}
.historyImg {
background: url("../../assets/images/chat/history.png") no-repeat center;
background-size: 100%;
}
}
.btn:hover {
border: 1px solid #004ea0;
color: #004ea0;
.newChatImg {
background: url("../../assets/images/chat/newChated.png") no-repeat center;
background-size: 100%;
}
.historyImg {
background: url("../../assets/images/chat/historyed.png") no-repeat center;
background-size: 100%;
}
}
.btned {
border: 1px solid #004ea0;
color: #004ea0;
.newChatImg {
background: url("../../assets/images/chat/newChated.png") no-repeat center;
background-size: 100%;
}
.historyImg {
background: url("../../assets/images/chat/historyed.png") no-repeat center;
background-size: 100%;
}
}
}
.chatTextarea {
width: 100%;
// height: 104px;
margin-top: 16px;
padding-bottom: 16px;
background: #ffffff;
border-radius: 8px 8px 8px 8px;
border: 1px solid #d5ddff;
.el-textarea {
font-weight: 400;
font-size: 18px;
color: #858a94;
line-height: 30px;
}
.chatScenario {
width: 100%;
height: 49px;
background: #d5ddff;
padding-left: 16px;
padding-right: 16px;
display: flex;
justify-content: space-between;
align-items: center;
.chatScenarioText {
font-weight: bold;
font-family: "PingFangSC-Bold";
font-size: 18px;
color: #004ea0;
line-height: 30px;
}
.exitChatScenario {
cursor: pointer;
font-weight: 400;
font-size: 16px;
color: #858a94;
line-height: 30px;
}
.exitChatScenario:hover {
color: #004ea0;
}
}
::v-deep .el-textarea__inner {
width: 100%;
/* height: 53px !important;*/
min-height: 53px !important;
outline: none;
border: none;
resize: none;
box-shadow: none;
background: #ffffff;
color: #000000;
padding-left: 16px;
padding-right: 16px;
padding-top: 16px;
padding-bottom: 10px;
}
.chatOperates {
display: flex;
justify-content: space-between;
align-items: center;
.chatOperatesLeft {
padding-left: 16px;
.deep-think {
width: 108px;
height: 28px;
background: #E6EDFF;
border-radius: 14px 14px 14px 14px;
border: 1px solid #D5DDFF;
font-size: 12px;
color: #858A94;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
img {
width: 12px;
margin-right: 3px;
}
}
.deep-think-active {
background: #E6EDFF;
border: 1px solid #004ea0;
color: #004ea0;
}
}
.chatOperatesRight {
display: flex;
justify-content: flex-end;
align-items: center;
padding-left: 16px;
padding-right: 16px;
.fileImg {
height: 28px;
margin-right: 16px;
}
.sendBtn {
width: 92px;
height: 38px;
cursor: pointer;
background: #004ea0;
border-radius: 22px 22px 22px 22px;
border: 1px solid #004ea0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 16px;
color: #fafbff;
line-height: 30px;
.sendImg {
margin-right: 8px;
width: 20px;
height: 18.53px;
}
}
.notAllowed {
cursor: default !important;
background: #ffffff !important;
color: #858a94 !important;
border: 1px solid #858a94 !important;
}
.stopBtn {
width: 120px;
height: 38px;
cursor: pointer;
background: #ffffff;
border-radius: 22px 22px 22px 22px;
border: 1px solid #004ea0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 16px;
color: #004ea0;
line-height: 30px;
.stopImg {
margin-right: 8px;
width: 20px;
height: 18.53px;
}
}
}
}
}
}
.footer {
width: 100%;
text-align: right;
margin-bottom: 16px;
font-weight: 400;
font-size: 14px;
color: #b9b9b9;
line-height: 30px;
.clicked {
color: #004ea0;
}
}
}
.noPlaceholder {
::v-deep .el-textarea__inner::placeholder {
color: transparent !important;
/* 或者 rgba(0, 0, 0, 0) */
}
}
}
</style>