vue3对接豆包AI(完全体版)
这篇文章记录了作者开发的AI对话功能的技术实现细节。主要功能包括:1) 类似豆包和DeepSeek的会话对接,通过传递上条消息ID和会话ID实现连续对话;2) 使用element-plus的上传组件实现图片上传(但当前会话显示未完成);3) 对话请求管理机制,支持终止当前请求并防止API响应抖动;4) 自动滚动到底部的交互优化。技术方案包含marked解析Markdown、fetch实现流式响应、
·
1、这篇文章主要是写给自己以后看的
2、对接会话功能,和豆包和deepSeek相差不大,每次会话传上条消息的id,传会话id
3、 图片上传在输入框上面,用的是自定义element-plus的上传组件el-upload,虽然有上传但会话没有显示图片,我还没处理好
4、在触发对话后会终止这个对话的请求,而且会触发失败接口,防止两个api一起返回抖动
5、自动显示在滚动条最底部
效果图


前期准备
markdown解析器:
"marked": "^4.2.12",markdown.tsmarkdown解析器
下面是不重要的插件:"v-viewer": "^3.0.11",图片预览用的这个"element-plus": "^2.2.32",
// markdown.ts
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: string): string {
// 创建自定义渲染器
const renderer:any = new marked.Renderer();
renderer.listitem = (text: string) => {
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) as string;
// 解析Markdown并净化HTML
// return DOMPurify.sanitize(marked.parse(content) as string, safeConfig);
}
请求封装
// webBreed.ts
import { parseMarkdown } from "@/utils/markdown"; // 确保你有这个解析器
// 存储AI分析的当前控制器
let currentController: AbortController | null = null;
// 存储当前请求的onError回调
let currentErrorCallback: ((error: Error) => void) | null = 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: {
Animal?: string;
Type?: string;
Remark?: string;
Photo?: string;
Menu?: string;
contextId?: string | number;
SessionID?: string | number;
},
callbacks: {
onProgress: (text: string, RespId?: any) => void;
onComplete?: () => void;
onError?: (error: Error) => void;
}
) => {
try {
// 中止任何现有请求
abortAIAnalysisRequest();
// 创建新的请求控制器
currentController = new AbortController();
const { signal } = currentController;
// 保存当前请求的onError回调
currentErrorCallback = callbacks.onError || null;
// 准备请求参数
const problem = data.Remark || "";
const token =
Cookies.get("token") || localStorage.getItem("YooHooCowToken") || "";
// 动态确定API地址
let httpUrl = "";
if (
location.href.includes("localhost") ||
location.href.indexOf("192.168.1.") !== -1
) {
httpUrl = "http://192.168.1.11:8081";
} else {
httpUrl = window.location.origin;
}
// 构建请求URL和参数
const url = `${httpUrl}${process.env.VUE_APP_BASE_API}/api/WebBreed/SendAIAnalysis`;
const params = {
breeds: data.Animal || "",
type: data.Type || "",
problem,
imgUrl: data.Photo || "",
menu: data.Menu || "",
contextId: data.contextId || "",
SessionID: data.SessionID || "",
};
// 发起请求
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 = "";
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: any = chunk.match(/\{.*?\}(?=\{|\s*$)/g);
jsonStrings.forEach((json: any) => {
let data = JSON.parse(json);
if (data.Status === "OK") {
fullText += data.Result;
if (RespId === "") {
RespId = data.RespId;
}
} else {
fullText = data.Result;
}
});
} else {
// 正常处理一个对象
let data = JSON.parse(chunk);
if (data.Status === "OK") {
fullText += data.Result;
if (RespId === "") {
RespId = data.RespId;
}
} else {
fullText = data.Result;
}
}
// 解析并回调更新
const parsedText = parseMarkdown(fullText);
callbacks.onProgress(parsedText, RespId);
}
return fullText;
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
callbacks.onError?.(error);
}
currentController = null;
currentErrorCallback = null;
throw error;
}
};
对话弹窗组件代码
<template>
<div>
<!-- AI正常对话的弹窗 -->
<el-dialog :class="'AIDialoguePopup ' + theme" v-model="dialogVisible" :title="dialogTitle" width="1130px"
destroy-on-close align-center center :append-to-body="false" draggable @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.FirstProblem || '对话' }}</span>
<el-icon @click.stop="DeleteAIAnalysisSession(item.ID)">
<Close />
</el-icon>
</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="imgList" v-if="item.user === 1 && item.ImgUrl">
<div class="imgItem" v-for="(imgItem, imgIndex) in item.ImgUrl.split(',')"
:key="imgIndex">
<img :src="imgItem" alt="img">
</div>
</div> -->
<div class="chatDetails" v-html="item.Result">
</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">
<img class="picture" :src="item.raw ? myCreateObjectURL(item.raw) : ''" alt="img" />
<div class="actions">
<div class="preview" @click="handlePictureCardPreview(item.raw)">
<el-icon><zoom-in /></el-icon>
</div>
<div class="delete" @click="handleRemove(item.raw)">
<el-icon>
<Delete />
</el-icon>
</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" v-model: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"
:on-exceed="handleExceed" :on-success="handleAvatarSuccess" :on-error="handleError"
:on-remove="handleRemove" :before-upload="beforeUpload">
<template #trigger>
<img :src="theme === 'dark' ? AiPictureIcon2Img : AiPictureIconImg"
alt="upload">
</template>
</el-upload>
</div>
<div class="submit" @click="toDiagnosis">
<img :src="theme === 'dark' ? AiSendIcon2Img : AiSendIconImg" alt="submit">
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { SendAIAnalysisApi, abortAIAnalysisRequest, GetAIAnalysisSessionListApi, GetAIAnalysisRecordListApi, SaveAIAnalysisSessionApi, DeleteAIAnalysisSessionApi } from "@/api/webBreed"
import AiLogo from "/public/commonPage/Home/homeContent/AiLogo.png"
import MedianAiLogoImg from "/public/commonPage/Home/AI/MedianAiLogo.png"
import AiPictureIconImg from "/public/commonPage/Home/AI/AiPictureIcon.png"
import AiSendIconImg from "/public/commonPage/Home/AI/AiSendIcon.png"
import AiTimeIconImg from "/public/commonPage/Home/AI/AiTimeIcon.png"
import AiUnfoldIcon1Img from "/public/commonPage/Home/AI/AiUnfoldIcon1.png"
import AiUnfoldIcon2Img from "/public/commonPage/Home/AI/AiUnfoldIcon2.png"
import AiNewDialogueAddedIconImg from "/public/commonPage/Home/AI/AiNewDialogueAddedIcon.png"
import AiPictureIcon2Img from "/public/commonPage/Home/AI/AiPictureIcon2.png"
import AiSendIcon2Img from "/public/commonPage/Home/AI/AiSendIcon2.png"
import AiTimeIcon2Img from "/public/commonPage/Home/AI/AiTimeIcon2.png"
import AiUnfoldIcon3Img from "/public/commonPage/Home/AI/AiUnfoldIcon3.png"
import AiUnfoldIcon4Img from "/public/commonPage/Home/AI/AiUnfoldIcon4.png"
import AiNewDialogueAddedIcon2Img from "/public/commonPage/Home/AI/AiNewDialogueAddedIcon2.png"
import AiNewDialogueAddedIcon3Img from "/public/commonPage/Home/AI/AiNewDialogueAddedIcon3.png"
import {
ref, reactive, watch, computed, Ref, nextTick,
getCurrentInstance
} from "vue";
import { ElMessage } from "element-plus";
import { Close, Delete, ZoomIn } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile } from 'element-plus'
import Cookies from "js-cookie";
let emit = defineEmits(["changeShowAIDialoguePopup"]);
let props = defineProps<{
show: boolean;
theme?: string;
}>();
// 弹窗信息
let dialogVisible = ref<boolean>(props.show || false);
watch(
() => props.show,
(newVal) => {
dialogVisible.value = newVal;
GetAIAnalysisSessionList()
}
);
let dialogTitle = ref<string>("");
let theme = ref<string>(props.theme || "default");
// 是否对话中
let fetchLoading = ref<boolean>(false);
// 展开侧边栏
let Unfold = ref<boolean>(true);
// 当前对话详情列表
let chatList = ref<any[]>([
// {
// Result: '欢迎使用AI养殖超能小助,请问有什么能帮助您的吗?',
// ImgUrl: '',
// user: 0
// }
]);
// 对话表单
let form = reactive({
Remark: '',
Photo: '',
// 上下文id
contextId: '',
// 会话id
SessionID: ''
})
// 上传列表和上传中状态
const fileList = ref<any[]>([]);
const uploading = ref(false);
// 图片上传
const VUE_APP_BASE_API = process.env.VUE_APP_BASE_API;
let action = ref(
VUE_APP_BASE_API + "/api/WebBreed/UploadFile?flowName=PastureMap"
);
// 上传参数请求头
let headers = reactive({
token: Cookies.get("token") || "",
});
const uploadRef: Ref = ref(null);
// 上传前-限制文件大小和类型
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isJPGorPNG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isJPGorPNG) {
ElMessage.error('只能上传JPG/PNG格式的图片!');
return false;
}
if (!isLt5M) {
ElMessage.error('图片大小不能超过5MB!');
return false;
}
return true;
};
// 处理超出限制
const handleExceed: UploadProps['onExceed'] = (files) => {
ElMessage.warning(`最多只能上传3张图片,当前选择了${files.length}张`);
};
// 上传成功
const handleAvatarSuccess: UploadProps["onSuccess"] = (res, file) => {
console.log('上传成功', res, file, fileList.value);
// 所有文件上传完成后,调用AI接口
if (fileList.value.every(file => file.status === 'success')) {
form.Photo = fileList.value.map((item: any) => item.response?.resultdata?.[0]).join(',')
handleDetails();
uploading.value = false;
}
};
// 上传失败
const handleError = () => {
uploading.value = false;
ElMessage.warning("上传失败!");
};
// 删除
const handleRemove = (file: any) => {
fileList.value = fileList.value.filter(item => item.uid !== file.uid);
};
// 预览
// v-viewer 查看图片
const viewerApi = (getCurrentInstance() as any).appContext.config
.globalProperties.$viewerApi;
const showImagesInViewer = (images: string) => {
if (images) {
if (images.indexOf(",") !== -1) {
viewerApi({ images: images.split(",") });
} else {
viewerApi({ images: [images] });
}
}
};
const handlePictureCardPreview = (file: any) => {
showImagesInViewer(myCreateObjectURL(file));
}
// 图片临时路径
const myCreateObjectURL = (file: any) => {
if (file) {
return URL.createObjectURL(file);
} else {
return "";
}
}
// 滚动到底部
const scrollToBottom = () => {
const chatListDome = document.querySelector('.AIDialoguePopupChatList');
if (chatListDome) {
// chatList.scrollTop = chatList.scrollHeight; // 直接滚动到最底部
// 或者用平滑滚动:
chatListDome.scrollTo({
top: chatListDome.scrollHeight,
behavior: 'smooth'
});
}
};
// 发送AI对话
const handleDetails = async () => {
fetchLoading.value = true
if (form.Remark) {
form.Remark = form.Remark.replace(/[\r\n]/g, "")
}
if (chatList.value && chatList.value.length <= 0) {
await SaveAIAnalysisSession()
}
chatList.value.push({
Result: form.Remark,
user: 1
})
let params = {
Animal: '牛',
Type: '自由问答',
Menu: '',
Remark: form.Remark || '',
Photo: form.Photo || '',
contextId: form.contextId || '',
SessionID: form.SessionID || '',
}
form.Remark = ''
form.Photo = ''
fileList.value = []
await SendAIAnalysisApi(params,
{
onProgress: (text, RespId) => {
if (fetchLoading.value) {
fetchLoading.value = false
}
if (chatList.value[chatList.value.length - 1].user !== 0) {
chatList.value.push({
Result: text,
user: 0,
})
} else {
chatList.value[chatList.value.length - 1].Result = text
}
form.contextId = RespId || ''
nextTick(() => {
scrollToBottom();
});
},
onComplete: () => {
fetchLoading.value = false
nextTick(() => {
scrollToBottom();
});
},
onError: (error) => {
fetchLoading.value = false
}
})
}
// 点击发送
const toDiagnosis = async () => {
if (fetchLoading.value) {
ElMessage.error('请先等小助回答完成!');
return
}
abortAIAnalysisRequest()
if (form.Remark === '') {
ElMessage.error('请输入问题!');
return
}
uploading.value = true;
try {
if (fileList.value.length > 0) {
// 手动触发上传
uploadRef.value!.submit();
} else {
// 如果没有图片,直接调用AI接口
await handleDetails();
}
} catch (error) {
uploading.value = false;
}
}
// 关闭对话弹窗
const close = () => {
abortAIAnalysisRequest()
fetchLoading.value = false
emit("changeShowAIDialoguePopup", false);
addConversation()
}
// 会话列表
let AIAnalysisSessionList = ref<any[]>([])
let activeHistory = ref<number>(-1)
// 获取会话列表
const GetAIAnalysisSessionList = () => {
GetAIAnalysisSessionListApi({
page: 1,
rows: 999,
}).then(res => {
AIAnalysisSessionList.value = res.Data || []
})
}
// 获取当前对话详情列表
const GetAIAnalysisRecordList = (info: any, index: number) => {
activeHistory.value = index
GetAIAnalysisRecordListApi({
page: 1,
rows: 99,
// sord: 'DESC',
SessionID: info.ID || ''
}).then(res => {
chatList.value = []
if (res.Data && res.Data.length > 0) {
form.contextId = res.Data[0].ContextId || ''
form.SessionID = res.Data[0].SessionID || ''
res.Data.forEach((item: any) => {
chatList.value.unshift({
Result: item.Problem || '',
ImgUrl: item.ImgUrl || '',
user: 1
}, {
Result: item.Result || '',
user: 0
})
nextTick(() => {
scrollToBottom();
});
})
}
})
}
// 新建一个会话--重置会话信息
const addConversation = () => {
chatList.value = []
form.contextId = ''
form.SessionID = ''
activeHistory.value = -1
}
const SaveAIAnalysisSession = async () => {
let res = await SaveAIAnalysisSessionApi({})
if (res.ResultCode === 200) {
// console.log('新建一个会话', res);
form.SessionID = res.Data.ID || ''
}
}
// 删除会话
const DeleteAIAnalysisSession = (id: number) => {
DeleteAIAnalysisSessionApi(id).then(res => {
if (res.ResultCode === 200) {
GetAIAnalysisSessionList()
if (id === +form.SessionID) {
addConversation()
}
}
})
}
</script>
<style lang="less" scoped>
:deep(.el-dialog.AIDialoguePopup) {
position: relative;
border-radius: 35px;
background-image: url("/public/commonPage/Home/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('/public/commonPage/Home//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("/public/commonPage/Home/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: 16px;
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: 14px;
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;
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;
// flex-direction: column;
margin-bottom: 15px;
.imgList {
display: flex;
margin-bottom: 10px;
.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%;
}
}
}
.chatDetails {
flex: 1;
// 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: 14px;
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: 14px;
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 {
background-image: url("/public/commonPage/Home/AI/AIDialoguePopup2.png");
background-size: 100% 100%;
border-radius: 45px;
.el-dialog__header {
.el-dialog__headerbtn {
&::after {
background-image: url('/public/commonPage/Home/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>
使用组件
// show显示 theme主题不传白色 dark暗色 changeShowAIDialoguePopup关闭回调
<AIDialoguePopup :show="AIDialoguePopupShow" theme="dark" @changeShowAIDialoguePopup="AIDialoguePopupShow = false" />
import AIDialoguePopup from "@/components/AIDialoguePopup/Index.vue"
let AIDialoguePopupShow = ref(false)
const dblClickAi = () => {
// 显示对话聊天组件
AIDialoguePopupShow.value = true
}
更多推荐


所有评论(0)