前端对接豆包AI(vue2版本)

前面做了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>
相关推荐
数字游民95272 小时前
推荐一个自带流量加成的小程序接口
人工智能·ai·小程序
—Qeyser2 小时前
Flutter AppBar 导航栏组件完全指南
前端·javascript·flutter
z20348315202 小时前
AI模型部署草稿
人工智能·单片机·嵌入式硬件
全栈开发圈2 小时前
干货分享|AI Agent全链路开发
人工智能
阿湯哥2 小时前
Agent、Skill、Tool、LLM 的四层关系与协同逻辑
人工智能
南_山无梅落2 小时前
create_deep_agent vs create_agent 的区别
人工智能·langchain·deepagent
Aliex_git2 小时前
提示词工程学习笔记
人工智能·笔记·学习
圣心2 小时前
Gemini3 开发指南 | Gemini AI 开发文档
大数据·人工智能
Amumu121382 小时前
React扩展(一)
前端·javascript·react.js