前面做了Vue3版本的,现在碰巧做了vue2版本的,也来记录一下吧
效果图

前置依赖
切记版本不对会出问题的
javascript
"marked": "^4.2.12",
我自己的全部依赖也贴下吧,虽然其他的不相干,但是可以给你们参考下

拖动控件
效果图

javascript
<div
class="AiLogo"
ref="AIRef"
@mousedown.stop.prevent="mousedown($event)"
:style="{
right: aiInfo.Lat + 'px',
bottom: aiInfo.Lon + 'px',
'z-index': aiInfo.zIndex,
}"
></div>
<AIDialoguePopup :show="AIDialoguePopupShow" @changeShowAIDialoguePopup="AIDialoguePopupShow = false" />
</div>
<script>
import AIDialoguePopup from "@/components/AIDialoguePopup/Index.vue";
export default {
name: "Layout",
components: {
AIDialoguePopup
},
data() {
return {
aiInfo: {
Lat: 50,
Lon: 50,
zIndex: 11,
},
dragStartTime: 0,
disX: 0, // 新增:声明偏移量变量
disY: 0, // 新增:声明偏移量变量
isDragging: false, // 新增:声明拖动状态变量
AIDialoguePopupShow: false,
};
},
methods: {
mousedown(e) {
let startX = 0;
let startY = 0;
const DRAG_THRESHOLD = 5; // 移动5px以上认为是拖动
const CLICK_TIME_THRESHOLD = 200; // 200ms内释放认为是单击
this.dragStartTime = Date.now();
startX = e.clientX;
startY = e.clientY;
const odiv = e.target;
const elemWidth = odiv.offsetWidth; // 元素宽度
const elemHeight = odiv.offsetHeight; // 元素高度
// 关键修改:基于视口计算初始偏移,不再依赖父元素
// 计算鼠标在元素内的相对位置
this.disX = e.clientX - (window.innerWidth - this.aiInfo.Lat - elemWidth);
this.disY =
e.clientY - (window.innerHeight - this.aiInfo.Lon - elemHeight);
const _this = this;
document.onmousemove = (e) => {
const deltaX = Math.abs(e.clientX - startX);
const deltaY = Math.abs(e.clientY - startY);
// 移动距离超过阈值,开始拖动
if (deltaX > DRAG_THRESHOLD || deltaY > DRAG_THRESHOLD) {
_this.isDragging = true;
// 基于视口计算新的right和bottom
let newRight =
window.innerWidth - (e.clientX - _this.disX) - elemWidth;
let newBottom =
window.innerHeight - (e.clientY - _this.disY) - elemHeight;
// 边界限制:保证元素不超出视口
newRight = Math.max(
0,
Math.min(newRight, window.innerWidth - elemWidth)
);
newBottom = Math.max(
0,
Math.min(newBottom, window.innerHeight - elemHeight)
);
_this.aiInfo.Lat = newRight;
_this.aiInfo.Lon = newBottom;
_this.aiInfo.zIndex = 999;
}
};
document.onmouseup = (e) => {
const endTime = Date.now();
const moveDuration = endTime - _this.dragStartTime;
document.onmousemove = null;
document.onmouseup = null;
_this.aiInfo.zIndex = 11;
// 判断是否为单击
if (!_this.isDragging && moveDuration < CLICK_TIME_THRESHOLD) {
_this.dblClickAi();
}
_this.isDragging = false;
};
},
dblClickAi() {
this.AIDialoguePopupShow = true;
},
},
};
</script>
<style lang="scss" scoped>
.AiLogo {
position: fixed;
z-index: 11;
right: 50px;
bottom: 50px;
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
background-image: url("~@/assets/AI/AiLogo.png");
background-size: 100% 100%;
box-shadow: 0 0 10px 0 #43edfd;
}
</style>
api
javascript
import request from "@/utils/request"; // 基本请求封装
import { getLocal} from "@/utils/local"; // 存取window.localStorage
import store from "@/store"; // vuex
import { parseMarkdown } from "@/utils/markdown.js"; // markdown解析器
import { formatDate } from "@/utils/index"; // 时间戳
//ai历史对话
export function GetChatInfoListApi(params) {
return request({
url: "/system/chatInfoList",
params,
});
}
// 对话详情
export function GetChatInfoDetailApi(params) {
return request({
url: "/system/chatInfoDetail",
params,
});
}
// ai提问
export function GetChatAIApi(data) {
return request({
url: "/system/chatAI",
method: "POST",
data,
});
}
// 删除对话
export function deleteChatInfoApi(data) {
return request({
url: "/system/deleteChatInfo",
method: "POST",
data,
});
}
// 存储AI分析的当前控制器
let currentController = null;
// 存储当前请求的onError回调
let currentErrorCallback = null;
// 中止AI分析请求的函数
export const abortAIAnalysisRequest = () => {
if (currentController) {
currentController.abort();
// 手动触发onError回调,通知请求已中止
if (currentErrorCallback) {
currentErrorCallback(new Error("请求已手动中止")); // 传递明确的中止错误信息
}
currentController = null;
currentErrorCallback = null;
console.log("手动中止AI分析请求", currentController);
}
};
// AI分析
export const SendAIAnalysisApi = async (data, callbacks) => {
try {
// 中止任何现有请求
abortAIAnalysisRequest();
// 创建新的请求控制器
currentController = new AbortController();
const { signal } = currentController;
// 保存当前请求的onError回调
currentErrorCallback = callbacks.onError || null;
// 准备请求参数
const problem = data.Remark || "";
const token = getLocal('token') || store.state.token || "";
// 动态确定API地址
let httpUrl = "";
if (
location.href.includes("localhost") ||
location.href.indexOf("192.168.1.") !== -1
) {
httpUrl = "http://192.168.1.34:8080";
} else {
httpUrl = window.location.origin;
}
// 构建请求URL和参数
const url = `${httpUrl}${process.env.VUE_APP_BASE_API}/system/chatAI`;
const params = {
// breeds: data.Animal || "",
// type: data.Type || "",
questionText: problem,
questionImage: data.Photo || "",
// menu: data.Menu || "",
contextId: data.contextId || "",
RespId: data.RespId || "",
userId: data.userId || "",
createTime: formatDate(new Date()),
};
// 发起请求
const response = await fetch(url.toString(), {
headers: { token, "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify(params),
signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 处理流式响应
const reader = response.body?.getReader();
if (!reader) throw new Error("No readable stream received");
const decoder = new TextDecoder();
let fullText = "";
let RespId = "";
let ContextId = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
callbacks.onComplete?.();
currentController = null;
break;
}
const chunk = decoder.decode(value, { stream: true });
// console.log("返回内容:", chunk);
if (chunk.indexOf("}{") > -1) {
const jsonStrings = chunk.match(/\{.*?\}(?=\{|\s*$)/g);
jsonStrings.forEach((json) => {
let data = JSON.parse(json);
if (data.Status === "OK") {
fullText += data.Result;
if (ContextId === "") {
ContextId = data.ContextId;
}
if (RespId === "") {
RespId = data.RespId;
}
} else {
fullText = data.Result;
}
});
} else {
let data = JSON.parse(chunk);
if (data.Status === "OK") {
fullText += data.Result;
if (ContextId === "") {
ContextId = data.ContextId;
}
if (RespId === "") {
RespId = data.RespId;
}
} else {
fullText = data.Result;
}
}
// 解析并回调更新
const parsedText = parseMarkdown(fullText);
callbacks.onProgress(parsedText, ContextId, RespId);
}
return fullText;
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
callbacks.onError?.(error);
}
currentController = null;
currentErrorCallback = null;
throw error;
}
};
解析器
javascript
import { marked } from "marked";
// import DOMPurify from "dompurify"; // 可选:安全净化
// 配置允许的HTML标签(根据需求调整)
const safeConfig = {
ALLOWED_TAGS: [
"h1",
"h2",
"h3",
"h4",
"strong",
"em",
"p",
"br",
"ul",
"ol",
"li",
],
ALLOWED_ATTR: ["class", "style"],
};
/**
* 解析 Markdown 文本为安全 HTML
* @param content Markdown格式文本
* @returns 安全HTML字符串
*/
export function parseMarkdown(content) {
// 创建自定义渲染器
const renderer = new marked.Renderer();
renderer.listitem = (text) => {
return `<li class="cn-list-item">${text}</li>`;
};
// 启用marked的安全模式
marked.setOptions({
gfm: true, // 启用 GitHub Flavored Markdown
breaks: true, // 禁用单换行转 <br>(保持原换行逻辑)
pedantic: false, // 禁用严格模式(允许宽松的列表解析)
silent: true, // 如果为 true,则解析器不会抛出任何异常或记录任何警告。任何错误都将作为字符串返回。
renderer,
});
// 修复内容中的列表格式
const fixedContent = content
.replace(/^-\s+/gm, "- ") // 统一列表项格式
.replace(/\n\s*-/g, "\n-"); // 修复多行列表
return marked.parse(fixedContent);
// 解析Markdown并净化HTML
// return DOMPurify.sanitize(marked.parse(content) as string, safeConfig);
}
组件代码(最重要)
javascript
<template>
<!-- AI正常对话的弹窗 -->
<el-dialog
v-draggable
:class="'ivu AIDialoguePopup ' + theme"
:visible.sync="dialogVisible"
:title="dialogTitle"
width="1130px"
align-center
center
:append-to-body="true"
@close="close"
>
<div class="AIDialoguePopupBox">
<div class="AIDialoguePopupLeft">
<div class="Unfold" v-show="Unfold">
<div class="title">
<div class="logo">
<img :src="AiLogo" alt="logo" />
</div>
<div class="name">
<span>智牧AI小智</span>
</div>
<div class="foldingImg">
<img
:src="theme === 'dark' ? AiUnfoldIcon4Img : AiUnfoldIcon2Img"
alt="Folding"
@click="Unfold = false"
/>
</div>
</div>
<div class="add" @click="addConversation">
<img
:src="
theme === 'dark'
? AiNewDialogueAddedIcon3Img
: AiNewDialogueAddedIconImg
"
alt="Add"
/>
<span>新建对话</span>
</div>
<div class="answerHistory">
<div class="answerHistoryTitle">
<img
:src="theme === 'dark' ? AiTimeIcon2Img : AiTimeIconImg"
alt="Add"
/>
<span>问答历史</span>
</div>
<div class="list">
<div
class="item"
:class="{ active: activeHistory === index }"
v-for="(item, index) in AIAnalysisSessionList"
:key="index"
@click="GetAIAnalysisRecordList(item, index)"
>
<span>{{ item.questionText || "对话" }}</span>
<i
class="el-icon-close"
@click.stop="DeleteAIAnalysisSession(item)"
></i>
</div>
</div>
</div>
</div>
<div class="Folding" v-show="!Unfold">
<img
:src="theme === 'dark' ? AiUnfoldIcon3Img : AiUnfoldIcon1Img"
alt="Folding"
@click="Unfold = true"
/>
<img
:src="
theme === 'dark'
? AiNewDialogueAddedIcon2Img
: AiNewDialogueAddedIconImg
"
alt="Add"
@click="addConversation"
/>
</div>
</div>
<div class="AIDialoguePopupContent">
<div
class="AIDialogCard"
v-loading="fetchLoading"
element-loading-text="深度思考中"
>
<div
class="AIDialoguePopupChatList"
v-if="chatList && chatList.length > 0"
>
<div
class="item"
:class="{ my: item.user === 1 }"
v-for="(item, index) in chatList"
:key="index"
>
<div class="myChatBox">
<div class="imgList" v-if="item.user === 1 && item.ImgUrl">
<div
class="imgItem"
v-for="(imgItem, imgIndex) in item.ImgUrl.split(',')"
:key="imgIndex"
@click="showImagesInViewer(imgItem)"
>
<img :src="imgItem" alt="img" />
</div>
</div>
<div class="chatDetails" v-html="item.Result"></div>
</div>
</div>
</div>
<div class="nullData" v-else>
<img :src="MedianAiLogoImg" alt="logo" />
<h2>智牧AI "小智"</h2>
<div class="subtitle">
<span>欢迎使用智牧AI "小智",我可以为您答疑解惑</span>
</div>
</div>
</div>
<div class="inputBox">
<div class="fileList" v-if="fileList && fileList.length > 0">
<div
class="fileItem"
v-for="(item, index) in fileList"
:key="index"
>
<!-- 修复1:优先使用 previewUrl(本地预览),上传后用服务器路径,不再依赖 raw -->
<img
class="picture"
:src="item.previewUrl || item.response?.resultdata?.[0] || ''"
alt="img"
v-if="item.previewUrl || item.response"
/>
<div class="actions">
<!-- 修复2:传整个 file 对象,而非 raw -->
<div class="preview" @click="handlePictureCardPreview(item)">
<i class="el-icon-zoom-in"></i>
</div>
<div class="delete" @click="handleRemove(item)">
<i class="el-icon-delete"></i>
</div>
</div>
</div>
</div>
<div class="inputContent">
<textarea
v-model="form.Remark"
rows="3"
maxlength="1000"
placeholder="请输入问题, "小智"将为您解答(可输入1000字)"
@keyup.enter="toDiagnosis"
></textarea>
</div>
<div class="uploadOrBtn">
<div class="upload">
<!-- -->
<el-upload
ref="uploadRef"
:file-list="fileList"
:action="action"
:headers="headers"
:auto-upload="false"
:limit="3"
:multiple="true"
:show-file-list="false"
accept="image/png,image/jpg,image/jpeg"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-change="handleFileChange"
:on-success="handleAvatarSuccess"
:on-error="handleError"
:on-remove="handleRemove"
>
<div class="el-upload__trigger">
<img
:src="
theme === 'dark' ? AiPictureIcon2Img : AiPictureIconImg
"
alt="upload"
/>
</div>
</el-upload>
</div>
<div class="submit" @click="toDiagnosis">
<img
:src="theme === 'dark' ? AiSendIcon2Img : AiSendIconImg"
alt="submit"
/>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import {
SendAIAnalysisApi,
abortAIAnalysisRequest,
GetChatInfoListApi,
GetChatInfoDetailApi,
GetChatAIApi,
deleteChatInfoApi,
} from "@/api/system";
// 导入图片资源
import AiLogo from "@/assets/AI/AiLogo.png";
import MedianAiLogoImg from "@/assets/AI/MedianAiLogo.png";
import AiPictureIconImg from "@/assets/AI/AiPictureIcon.png";
import AiSendIconImg from "@/assets/AI/AiSendIcon.png";
import AiTimeIconImg from "@/assets/AI/AiTimeIcon.png";
import AiUnfoldIcon1Img from "@/assets/AI/AiUnfoldIcon1.png";
import AiUnfoldIcon2Img from "@/assets/AI/AiUnfoldIcon2.png";
import AiNewDialogueAddedIconImg from "@/assets/AI/AiNewDialogueAddedIcon.png";
import AiPictureIcon2Img from "@/assets/AI/AiPictureIcon2.png";
import AiSendIcon2Img from "@/assets/AI/AiSendIcon2.png";
import AiTimeIcon2Img from "@/assets/AI/AiTimeIcon2.png";
import AiUnfoldIcon3Img from "@/assets/AI/AiUnfoldIcon3.png";
import AiUnfoldIcon4Img from "@/assets/AI/AiUnfoldIcon4.png";
import AiNewDialogueAddedIcon2Img from "@/assets/AI/AiNewDialogueAddedIcon2.png";
import AiNewDialogueAddedIcon3Img from "@/assets/AI/AiNewDialogueAddedIcon3.png";
import { parseMarkdown } from "@/utils/markdown.js";
import { getLocal } from "@/utils/local";
import Cookies from "js-cookie";
export default {
name: "AIDialoguePopup",
components: {
// Vue2需显式注册Element UI组件(如果全局注册了可省略)
},
props: {
show: {
type: Boolean,
default: false,
},
theme: {
type: String,
default: "default",
},
},
data() {
return {
// 图片资源挂载到data中
AiLogo,
MedianAiLogoImg,
AiPictureIconImg,
AiSendIconImg,
AiTimeIconImg,
AiUnfoldIcon1Img,
AiUnfoldIcon2Img,
AiNewDialogueAddedIconImg,
AiPictureIcon2Img,
AiSendIcon2Img,
AiTimeIcon2Img,
AiUnfoldIcon3Img,
AiUnfoldIcon4Img,
AiNewDialogueAddedIcon2Img,
AiNewDialogueAddedIcon3Img,
// 弹窗信息
dialogVisible: false,
dialogTitle: "",
// 是否对话中
fetchLoading: false,
// 展开侧边栏
Unfold: true,
// 当前对话详情列表
chatList: [],
// 对话表单
form: {
Remark: "",
Photo: "",
contextId: "",
respId: "",
},
// 上传列表和上传中状态
fileList: [],
uploading: false,
// 图片上传配置
VUE_APP_BASE_API: process.env.VUE_APP_BASE_API,
action: `${process.env.VUE_APP_BASE_API}/resource/avatar`,
// 上传参数请求头
headers: {
Authorization: "Bearer " + this.$store.state.token,
},
// 会话列表
AIAnalysisSessionList: [],
activeHistory: -1,
// 如果没登录,存第一次对话时间
firstTime: "",
};
},
watch: {
// 监听props.show变化
show(newVal) {
this.dialogVisible = newVal;
if (newVal) {
let token = getLocal("token") || this.$store.state.token || "";
let firstTime = Cookies.get("firstTime") || "";
// 如果没登录,存第一次对话时间,用作 userId
if (!token) {
if (firstTime) {
this.firstTime = firstTime;
} else {
this.firstTime = new Date().getTime();
Cookies.set("firstTime", this.firstTime, { expires: 3 });
}
} else {
if (firstTime) {
Cookies.remove("firstTime");
}
}
this.GetAIAnalysisSessionList();
}
},
},
created() {
// 初始化弹窗状态
this.dialogVisible = this.show;
},
beforeDestroy() {
if (this.fileList && this.fileList.length > 0) {
this.fileList.forEach((file) => {
if (file.previewUrl) {
URL.revokeObjectURL(file.previewUrl);
}
});
}
},
methods: {
// 新增:文件选择/状态变化时生成本地预览路径
handleFileChange(file, fileList) {
// file.status 说明:
// ready - 刚选择文件;uploading - 上传中;success - 上传成功;fail - 上传失败
const nativeFile = file.raw || file;
// 仅在「刚选择文件」时生成本地预览路径
if (file.status === "ready" && nativeFile) {
// 给 el-upload 包装的文件对象新增 previewUrl 属性(不修改原生 File)
this.$set(file, "previewUrl", URL.createObjectURL(nativeFile));
}
// 关键:用 el-upload 内部维护的 fileList 覆盖,保证数据结构正确
this.fileList = fileList;
},
// 上传前-限制文件大小和类型
beforeUpload(file) {
// console.log(file);
// const isJPGorPNG =
// file.type === "image/jpeg" || file.type === "image/png";
// const isLt5M = file.size / 1024 / 1024 < 5;
// if (!isJPGorPNG) {
// this.$message.error("只能上传JPG/PNG格式的图片!");
// return false;
// }
// if (!isLt5M) {
// this.$message.error("图片大小不能超过5MB!");
// return false;
// }
return true;
},
// 处理超出限制
handleExceed(files) {
this.$message.warning(`最多只能上传3张图片,当前选择了${files.length}张`);
},
// 上传成功
handleAvatarSuccess(res, file, fileList) {
console.log("上传成功", res, file);
// 使用传入的 fileList 参数,而不是 this.fileList
const allSuccess = fileList.every((item) => item.status === "success");
if (allSuccess) {
this.form.Photo = fileList
.map((item) => item.response?.data?.url)
.filter(Boolean) // 过滤掉空值
.join(",");
// 确保文件列表更新
this.fileList = [...fileList];
// 延迟调用,确保状态更新完成
this.$nextTick(() => {
this.handleDetails();
});
this.uploading = false;
}
},
// 上传失败
handleError() {
this.uploading = false;
this.$message.warning("上传失败!");
},
// 删除上传文件
handleRemove(file, fileList) {
// 情况1:el-upload 自带触发(比如超出限制时),用传入的 fileList
if (fileList) {
this.fileList = [...fileList];
} else {
// 情况2:自定义删除按钮触发,通过 uid 精准过滤
this.fileList = this.fileList.filter((item) => item.uid !== file.uid);
}
// 强制释放预览路径,避免内存泄漏 + 残留数据
if (file.previewUrl) {
URL.revokeObjectURL(file.previewUrl);
// 清空 previewUrl,防止残留
file.previewUrl = "";
}
// 额外保障:强制更新视图(针对 Vue2 响应式可能的遗漏)
this.$forceUpdate();
},
// 预览图片
showImagesInViewer(images) {
if (images) {
const imgList =
images.indexOf(",") !== -1 ? images.split(",") : [images];
// Vue2中调用全局$viewerApi
this.$viewerApi({
options: {
zIndex: 9999,
},
images: imgList,
});
}
},
handlePictureCardPreview(file) {
const imgUrl = file.previewUrl || file.response?.resultdata?.[0] || "";
this.showImagesInViewer(imgUrl);
},
// 图片临时路径
myCreateObjectURL(file) {
if (file) {
return URL.createObjectURL(file);
} else {
return "";
}
},
// 滚动到底部
scrollToBottom() {
const chatListDome = document.querySelector(".AIDialoguePopupChatList");
if (chatListDome) {
chatListDome.scrollTo({
top: chatListDome.scrollHeight,
behavior: "smooth",
});
}
},
// 发送AI对话
async handleDetails() {
this.fetchLoading = true;
if (this.form.Remark) {
this.form.Remark = this.form.Remark.replace(/[\r\n]/g, "");
}
if (this.chatList && this.chatList.length <= 0) {
await this.SaveAIAnalysisSession();
}
this.chatList.push({
Result: this.form.Remark,
user: 1,
ImgUrl: this.form.Photo,
});
const params = {
// Animal: "牛",
// Type: "自由问答",
// Menu: "",
Remark: this.form.Remark || "",
Photo: this.form.Photo || "",
contextId: this.form.contextId || "",
RespId: this.form.respId || "",
userId: this.firstTime || this.$store.state.user.userInfo.userId || "",
};
this.form.Remark = "";
this.form.Photo = "";
this.fileList = [];
await SendAIAnalysisApi(params, {
onProgress: (text, ContextId, RespId) => {
if (this.fetchLoading) {
this.fetchLoading = false;
}
if (this.chatList[this.chatList.length - 1].user !== 0) {
this.chatList.push({
Result: text,
user: 0,
});
} else {
this.chatList[this.chatList.length - 1].Result = text;
}
this.form.contextId = ContextId || "";
this.form.respId = RespId || "";
// Vue2的nextTick
this.$nextTick(() => {
this.scrollToBottom();
});
},
onComplete: () => {
this.fetchLoading = false;
this.$nextTick(() => {
this.scrollToBottom();
});
},
onError: (error) => {
this.fetchLoading = false;
},
});
},
// 点击发送
async toDiagnosis() {
if (this.fetchLoading) {
this.$message.error("请先等"小智"回答完成!");
return;
}
abortAIAnalysisRequest();
if (this.form.Remark === "") {
this.$message.error("请输入问题!");
return;
}
this.uploading = true;
try {
if (this.fileList.length > 0) {
// 手动触发上传(Vue2 ref访问方式)
this.$refs.uploadRef.submit();
} else {
// 如果没有图片,直接调用AI接口
await this.handleDetails();
}
} catch (error) {
console.log("点击发送error", error);
this.uploading = false;
}
},
// 关闭对话弹窗
close() {
abortAIAnalysisRequest();
this.fetchLoading = false;
// Vue2触发自定义事件
this.$emit("changeShowAIDialoguePopup", false);
this.chatList = [];
this.form.contextId = "";
this.form.respId = "";
this.activeHistory = -1;
},
// 获取会话列表
GetAIAnalysisSessionList() {
GetChatInfoListApi({
// page: 1,
// rows: 999,
userId: this.firstTime || this.$store.state.user.userInfo.userId || "",
}).then((res) => {
this.AIAnalysisSessionList = res.data || [];
});
},
// 获取当前对话详情列表
GetAIAnalysisRecordList(info, index) {
this.activeHistory = index;
GetChatInfoDetailApi({
// page: 1,
// rows: 99,
contextId: info.contextId || "",
}).then((res) => {
this.chatList = [];
if (res.data && res.data.length > 0) {
this.form.contextId = res.data[0].contextId || "";
this.form.respId = res.data[res.data.length - 1].respId || "";
res.data.forEach((item) => {
// 解析Markdown
this.chatList.push(
{
Result: item.questionText || "",
ImgUrl: item.questionImage || "",
user: 1,
},
{
Result: parseMarkdown(item.answer) || "",
user: 0,
}
);
this.$nextTick(() => {
this.scrollToBottom();
});
});
}
});
},
// 新建一个会话--重置会话信息
addConversation() {
this.chatList = [];
this.form.contextId = "";
this.form.respId = "";
this.activeHistory = -1;
this.GetAIAnalysisSessionList();
},
// 保存会话
async SaveAIAnalysisSession() {
const res = await GetChatAIApi({});
if (res.code === 200) {
this.form.respId = res.data.RespId || "";
}
},
// 删除会话
DeleteAIAnalysisSession(info) {
deleteChatInfoApi(info).then((res) => {
if (res.code === 200) {
this.GetAIAnalysisSessionList();
if (info.contextId === this.form.contextId) {
this.addConversation();
}
}
});
},
},
};
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
border-radius: 35px;
position: relative;
background-image: url("~@/assets/AI/AIDialoguePopup.png");
background-size: 100% 100%;
.el-dialog__header {
position: absolute;
z-index: 3;
top: 0;
left: 0;
width: 100%;
background-color: transparent;
margin-right: 0;
.el-dialog__title {
color: #409eff;
font-weight: bold;
font-size: 30px;
}
.el-dialog__headerbtn {
width: 24px;
height: 24px;
top: 18px;
right: 26px;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-image: url("~@/assets/AI/AiDelIcon.png");
background-size: 100% 100%;
}
.el-dialog__close {
display: none;
}
}
}
.el-dialog__body {
padding: 0;
.AIDialoguePopupBox {
position: relative;
display: flex;
.AIDialoguePopupLeft {
.Unfold {
padding: 16px 10px 4px 15px;
width: 180px;
height: 695px;
border-right: 1px solid #73dcff;
font-family: PingFang SC;
transition: all 0.5s;
&::after {
content: "";
position: absolute;
left: 17px;
bottom: 4px;
width: 150px;
height: 150px;
background-image: url("~@/assets/AI/MedianAiLogo.png");
background-size: 100% 100%;
}
.title {
padding-right: 10px;
display: flex;
align-items: center;
justify-content: space-between;
img {
display: block;
width: 100%;
height: 100%;
}
.logo {
width: 32px;
height: 32px;
}
.name {
font-weight: bold;
font-size: 18px;
color: #091a34;
}
.foldingImg {
width: 14px;
height: 14px;
cursor: pointer;
}
}
.add {
display: flex;
align-items: center;
justify-content: center;
width: 155px;
height: 30px;
// background: #CCDEFF;
border-radius: 4px;
margin: 8px 0 16px;
font-weight: 400;
font-size: 16px;
color: #091a34;
cursor: pointer;
img {
display: block;
width: 24px;
height: 24px;
margin-right: 5px;
}
}
.answerHistory {
padding: 0 5px 0 0;
.answerHistoryTitle {
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
color: #091a34;
margin-bottom: 14px;
img {
display: block;
width: 14px;
height: 14px;
margin-right: 5px;
}
}
.list {
max-height: 425px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 5px;
height: 5px;
background: transparent;
}
&::-webkit-scrollbar-track,
&-small::-webkit-scrollbar-track {
border-radius: 10px;
background: transparent;
}
&::-webkit-scrollbar-thumb,
&-small::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: #d9d9d9;
cursor: pointer;
}
.item {
display: flex;
align-items: center;
padding: 5px 10px;
cursor: pointer;
font-weight: 400;
// font-size: 14px;
font-size: 16px;
color: #000000;
border-radius: 4px;
margin-bottom: 3px;
&:last-child {
margin-bottom: 0;
}
&:hover,
&.active {
background: #ccdeff;
}
&:hover {
.el-icon {
display: block;
}
}
span {
flex: 1;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.el-icon {
display: none;
}
}
}
}
}
.Folding {
position: absolute;
left: 24px;
top: 24px;
transition: all 0.5s;
img {
width: 24px;
height: 24px;
cursor: pointer;
margin-right: 8px;
&:last-child {
margin-right: 0;
}
}
}
}
.AIDialoguePopupContent {
flex: 1;
display: flex;
flex-direction: column;
height: 695px;
padding: 64px 50px 46px;
.AIDialogCard {
// height: 60vh;
flex: 1;
min-height: 0;
.AIDialoguePopupChatList {
height: 100%;
overflow-y: auto;
// padding: 0 2px 0 0;
padding: 0 53px;
&::-webkit-scrollbar {
width: 5px;
height: 5px;
background: transparent;
}
&::-webkit-scrollbar-track,
&-small::-webkit-scrollbar-track {
border-radius: 10px;
background: transparent;
}
&::-webkit-scrollbar-thumb,
&-small::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: #d9d9d9;
cursor: pointer;
}
.item {
display: flex;
margin-bottom: 15px;
.myChatBox {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.imgList {
display: flex;
margin-bottom: 5px;
.imgItem {
width: 40px;
height: 40px;
border: 1px solid #fff;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
img {
width: 100%;
height: 100%;
border-radius: 5px;
}
}
}
.chatDetails {
// flex: 1;
display: inline-block;
width: auto;
// background-color: rgba(255, 255, 255, .7);
color: #091a34;
padding: 5px 20px;
border-radius: 20px;
font-size: 16px;
line-height: 1.5;
font-family: "PingFang SC";
h1 {
font-size: 32px;
font-weight: bold;
margin: 6px 0;
}
h2 {
font-size: 24px;
font-weight: bold;
margin: 6px 0;
}
h3 {
font-size: 20px;
font-weight: bold;
margin: 6px 0;
}
h4 {
font-size: 16px;
font-weight: bold;
margin: 6px 0;
}
h5 {
font-size: 13.28px;
font-weight: bold;
margin: 6px 0;
}
h6 {
font-size: 12px;
font-weight: bold;
margin: 6px 0;
}
/* 中文风格列表 */
.cn-list-item {
list-style: none;
/* 隐藏默认符号 */
position: relative;
padding-left: 1.2em;
/* 留出符号空间 */
line-height: 1.5;
&::before {
content: "·";
/* 中文圆点符号 */
position: absolute;
left: 0;
font-weight: bold;
font-size: 1em;
color: #333;
}
}
/* 一级列表用 · */
.cn-list-item::before {
content: "·";
}
/* 二级列表用 ▪ */
ul ul .cn-list-item::before {
content: "▪";
}
/* 三级列表用 ▫ */
ul ul ul .cn-list-item::before {
content: "▫";
}
}
&.my {
width: auto;
justify-content: flex-end;
// flex-direction: column;
.imgList {
}
.chatDetails {
flex: initial;
background-color: #ccdeff;
font-size: 16px;
color: #091a34;
}
}
}
}
.nullData {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
img {
width: 234px;
height: 234px;
}
h2 {
font-weight: bold;
font-size: 28px;
color: #091a34;
margin-bottom: 14px;
}
.subtitle {
font-weight: 400;
font-size: 16px;
color: rgba(9, 26, 52, 0.8);
}
}
}
.inputBox {
flex-shrink: 0;
display: flex;
flex-direction: column;
margin: 10px auto 0;
width: 760px;
background: rgba(255, 255, 255, 0.4);
box-shadow: 0px 6px 10px 0px rgba(0, 0, 0, 0.1);
border: 2px solid #f4f8ff;
border-radius: 16px;
.fileList {
display: flex;
padding: 12px 10px 5px;
.fileItem {
position: relative;
width: 52px;
height: 52px;
border-radius: 10px;
overflow: hidden;
margin-right: 10px;
&:hover {
.actions {
opacity: 1;
}
}
.picture {
width: 100%;
height: 100%;
object-fit: cover;
}
.actions {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
display: inline-flex;
justify-content: center;
align-items: center;
opacity: 0;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 16px;
transition: opacity 0.3s;
.preview {
cursor: pointer;
margin-right: 4px;
}
.delete {
cursor: pointer;
}
}
}
}
.inputContent {
flex: 1;
textarea {
width: 100%;
height: 100%;
padding: 10px 12px 8px;
outline: none;
border: none;
border-radius: 4px;
font-size: 16px;
line-height: 1.5;
box-sizing: border-box;
// 允许垂直调整大小
// resize: vertical;
// 隐藏滚动条
scrollbar-width: none;
-ms-overflow-style: none;
&:focus {
outline: none;
border: none;
}
}
}
.uploadOrBtn {
padding: 10px 12px 15px;
display: flex;
justify-content: flex-end;
.upload,
.submit {
width: 24px;
height: 24px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.upload {
position: relative;
margin-right: 17px;
&::after {
content: "";
position: absolute;
top: 50%;
right: -8.5px;
transform: translate(0, -50%);
width: 1px;
height: 16px;
background: rgba(192, 192, 192, 0.5);
}
}
}
}
}
}
}
}
.dark {
::v-deep .el-dialog {
background-image: url("~@/assets/AI/AIDialoguePopup2.png");
background-size: 100% 100%;
border-radius: 45px;
.el-dialog__header {
.el-dialog__headerbtn {
&::after {
background-image: url("~@/assets/AI/AiDelIcon2.png");
}
}
}
.el-dialog__body {
.AIDialoguePopupBox {
.AIDialoguePopupLeft {
.Unfold {
border-right: 1px solid #2b8ce7;
.title {
.name {
color: #ffffff;
}
}
.add {
background: linear-gradient(180deg, #2b3ee7 0%, #2b8ce7 100%);
color: #ffffff;
img {
width: 14px;
height: 14px;
}
}
.answerHistory {
.answerHistoryTitle {
color: #fff;
}
.list {
&::-webkit-scrollbar-thumb,
&-small::-webkit-scrollbar-thumb {
background-color: #2b8ce7;
}
.item {
color: #fff;
&:hover,
&.active {
background: #2b8ce7;
}
}
}
}
}
.Folding {
position: absolute;
left: 24px;
top: 24px;
transition: all 0.5s;
img {
width: 24px;
height: 24px;
cursor: pointer;
margin-right: 8px;
&:last-child {
margin-right: 0;
}
}
}
}
.AIDialoguePopupContent {
.AIDialogCard {
.AIDialoguePopupChatList {
&::-webkit-scrollbar-thumb,
&-small::-webkit-scrollbar-thumb {
background-color: #2b8ce7;
}
.item {
.chatDetails {
color: #ffffff;
.cn-list-item {
&::before {
color: #ffffff;
}
}
}
&.my {
.chatDetails {
background-color: #ffffff;
color: #091a34;
}
}
}
}
.nullData {
h2 {
color: #ffffff;
}
.subtitle {
color: rgba(255, 255, 255, 0.8);
}
}
}
.inputBox {
position: relative;
background: #212867;
border: none;
border-radius: 8px;
&::before {
content: "";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
z-index: -1;
border-radius: 8px;
background: linear-gradient(180deg, #2b3ee7, #2b8ce7);
}
.fileList {
}
.inputContent {
textarea {
background-color: transparent;
color: #ffffff;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
}
}
.uploadOrBtn {
.upload {
&::after {
background: rgba(0, 157, 255, 1);
}
}
}
}
}
}
}
}
}
</style>