houzhongjian
2025-05-29 3880399bef4144fa15264f470a0a51034c0253c9
ai工业大模型代码提交
已删除1个文件
已修改6个文件
已添加40个文件
7839 ■■■■ 文件已修改
src/api/ai/chat/message/index.ts 68 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ai/questionparamsetting/index.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ai/questiontemplate/index.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/assistant.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/common_title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/content.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/content_select.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/conversation.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/conversation_big.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/copy.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/data_bg3.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/data_bg4.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/ddtljl_result_title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/delete.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/edit.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/icon_bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/icon_conversion.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/ldggrqsyc_title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/ldghslyc_title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/left_label.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/mqhsjhxx_title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/refresh.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/right_label.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/scmbyyxzb_title.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/ai/zhuanlu/user.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/src/DialogDashboard.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/src/DialogHistory.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/MarkdownView/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/remaining.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/rem.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/CommonConversation.vue 964 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/CommonConversationList.vue 526 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/ConversationList.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/ConversationListEmpty.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/HistoryConversationList.vue 459 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/HistoryMessageDialog.vue 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/HistoryMessageList.vue 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/MessageList.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/MessageListEmpty.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/ModelMessageList.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/zhuanlu/Index.vue 1138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/zhuanlu/index.vue 2347 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/questionparamsetting/QuestionParamSettingForm.vue 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/questionparamsetting/index.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/questiontemplate/QuestionTemplateForm.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/questiontemplate/index.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ai/chat/message/index.ts
@@ -2,6 +2,7 @@
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'
import {refreshToken} from "@/api/login";
// 聊天VO
export interface ChatMessageVO {
@@ -13,6 +14,9 @@
  model: number // 模型标志
  modelId: number // 模型编号
  content: string // 聊天内容
  thinking: string // 聊天思考
  thinkingFlag: boolean // 聊天思考
  conclusion: string // 聊天结论
  tokens: number // 消耗 Token 数量
  segmentIds?: number[] // 段落编号
  segments?: {
@@ -73,34 +77,34 @@
    })
  },
  // 发送 Stream 消息 【工业大模型专用】
  sendEnergyChatMessageStream: async (
    conversationId: number,
    content: string,
    ctrl,
    enableContext: boolean,
    onMessage,
    onError,
    onClose
  ) => {
    const token = getAccessToken()
    return fetchEventSource(`${config.base_url}/ai/chat/message/send-energy-stream`, {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      openWhenHidden: true,
      body: JSON.stringify({
        conversationId,
        content,
        useContext: enableContext
      }),
      onmessage: onMessage,
      onerror: onError,
      onclose: onClose,
      signal: ctrl.signal
    })
  },
  // sendEnergyChatMessageStream: async (
  //   conversationId: number,
  //   content: string,
  //   ctrl,
  //   enableContext: boolean,
  //   onMessage,
  //   onError,
  //   onClose
  // ) => {
  //   const token = getAccessToken()
  //   return fetchEventSource(`${config.base_url}/ai/chat/message/send-energy-stream`, {
  //     method: 'post',
  //     headers: {
  //       'Content-Type': 'application/json',
  //       Authorization: `Bearer ${token}`
  //     },
  //     openWhenHidden: true,
  //     body: JSON.stringify({
  //       conversationId,
  //       content,
  //       useContext: enableContext
  //     }),
  //     onmessage: onMessage,
  //     onerror: onError,
  //     onclose: onClose,
  //     signal: ctrl.signal
  //   })
  // },
  // 删除消息
  deleteChatMessage: async (id: string) => {
@@ -114,10 +118,10 @@
    })
  },
  // 删除消息【工业大模型专用】
  deleteEnergyChatMessage: async (id: string) => {
    return await request.delete({ url: `/ai/chat/message/delete-energy?id=${id}` })
  },
  // // 删除消息【工业大模型专用】
  // deleteEnergyChatMessage: async (id: string) => {
  //   return await request.delete({ url: `/ai/chat/message/delete-energy?id=${id}` })
  // },
  // 删除指定对话的消息【工业大模型专用】
  deleteEnergyByConversationId: async (conversationId: number) => {
src/api/ai/questionparamsetting/index.js
对比新文件
@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 创建大模型问题设置参数
export function createQuestionParamSetting(data) {
  return request({
    url: '/ai/question-param-setting/create',
    method: 'post',
    data: data
  })
}
// 更新大模型问题设置参数
export function updateQuestionParamSetting(data) {
  return request({
    url: '/ai/question-param-setting/update',
    method: 'put',
    data: data
  })
}
// 删除大模型问题设置参数
export function deleteQuestionParamSetting(id) {
  return request({
    url: '/ai/question-param-setting/delete?id=' + id,
    method: 'delete'
  })
}
// 获得大模型问题设置参数
export function getQuestionParamSetting(id) {
  return request({
    url: '/ai/question-param-setting/get?id=' + id,
    method: 'get'
  })
}
// 获得大模型问题设置参数分页
export function getQuestionParamSettingPage(params) {
  return request({
    url: '/ai/question-param-setting/page',
    method: 'get',
    params
  })
}
// 导出大模型问题设置参数 Excel
export function exportQuestionParamSettingExcel(params) {
  return request({
    url: '/ai/question-param-setting/export-excel',
    method: 'get',
    params,
    responseType: 'blob'
  })
}
src/api/ai/questiontemplate/index.js
对比新文件
@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 创建大模型问题模板
export function createQuestionTemplate(data) {
  return request({
    url: '/ai/question-template/create',
    method: 'post',
    data: data
  })
}
// 更新大模型问题模板
export function updateQuestionTemplate(data) {
  return request({
    url: '/ai/question-template/update',
    method: 'put',
    data: data
  })
}
// 删除大模型问题模板
export function deleteQuestionTemplate(id) {
  return request({
    url: '/ai/question-template/delete?id=' + id,
    method: 'delete'
  })
}
// 获得大模型问题模板
export function getQuestionTemplate(id) {
  return request({
    url: '/ai/question-template/get?id=' + id,
    method: 'get'
  })
}
// 获得大模型问题模板分页
export function getQuestionTemplatePage(params) {
  return request({
    url: '/ai/question-template/page',
    method: 'get',
    params
  })
}
// 导出大模型问题模板 Excel
export function exportQuestionTemplateExcel(params) {
  return request({
    url: '/ai/question-template/export-excel',
    method: 'get',
    params,
    responseType: 'blob'
  })
}
src/assets/ai/zhuanlu/assistant.png
src/assets/ai/zhuanlu/common_title.png
src/assets/ai/zhuanlu/content.png
src/assets/ai/zhuanlu/content_select.png
src/assets/ai/zhuanlu/conversation.png
src/assets/ai/zhuanlu/conversation_big.png
src/assets/ai/zhuanlu/copy.png
src/assets/ai/zhuanlu/data_bg3.png
src/assets/ai/zhuanlu/data_bg4.png
src/assets/ai/zhuanlu/ddtljl_result_title.png
src/assets/ai/zhuanlu/delete.png
src/assets/ai/zhuanlu/edit.png
src/assets/ai/zhuanlu/icon_bg.png
src/assets/ai/zhuanlu/icon_conversion.png
src/assets/ai/zhuanlu/ldggrqsyc_title.png
src/assets/ai/zhuanlu/ldghslyc_title.png
src/assets/ai/zhuanlu/left_label.png
src/assets/ai/zhuanlu/mqhsjhxx_title.png
src/assets/ai/zhuanlu/refresh.png
src/assets/ai/zhuanlu/right_label.png
src/assets/ai/zhuanlu/scmbyyxzb_title.png
src/assets/ai/zhuanlu/user.png
src/components/Dialog/src/DialogDashboard.vue
对比新文件
@@ -0,0 +1,118 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
defineOptions({ name: 'DialogDashboard' })
const slots = useSlots()
const props = defineProps({
  modelValue: propTypes.bool.def(false),
  title: propTypes.string.def('Dialog'),
  fullscreen: propTypes.bool.def(true),
  width: propTypes.oneOfType([String, Number]).def('30%'),
  scroll: propTypes.bool.def(false), // 是否开启滚动条。如果是的话,按照 maxHeight 设置最大高度
  maxHeight: propTypes.oneOfType([String, Number]).def('400px')
})
const getBindValue = computed(() => {
  const delArr: string[] = ['fullscreen', 'title', 'maxHeight', 'appendToBody']
  const attrs = useAttrs()
  const obj = { ...attrs, ...props }
  for (const key in obj) {
    if (delArr.indexOf(key) !== -1) {
      delete obj[key]
    }
  }
  return obj
})
</script>
<template>
  <ElDialog
    v-bind="getBindValue"
    :close-on-click-modal="true"
    :width="width"
    destroy-on-close
    lock-scroll
    draggable
    class="dashboard-dialog"
    :show-close="false"
  >
    <template #header="{ close }">
      <div class="relative h-30px flex items-center justify-between pl-15px pr-15px">
        <slot name="title">
          {{ title }}
        </slot>
        <div
          class="absolute right-15px top-[50%] h-40px flex translate-y-[-50%] items-center justify-between"
        >
          <Icon
            class="is-hover cursor-pointer"
            icon="ep:close"
            hover-color="var(--el-color-primary)"
            color="#73C4FF"
            @click="close"
          />
        </div>
      </div>
    </template>
    <slot></slot>
    <template v-if="slots.footer" #footer>
      <slot name="footer"></slot>
    </template>
  </ElDialog>
</template>
<style lang="scss">
.el-dialog.is-draggable .el-dialog__header {
  color: white !important;
}
::v-deep .el-input {
  background: rgba(0,194,255,0.08) !important;
}
::v-deep .el-input__inner {
  color: #8FD6FE;
  background: rgba(0,194,255,0.08) !important;
  border: 1px solid #1D9FE8
}
.dashboard-dialog {
  color: #73C4FF;
  background: rgba(3,29,76,0.79);
  border-radius: 4px 4px 4px 4px;
  border: 1px solid;
  .#{$elNamespace}-overlay-dialog {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .#{$elNamespace}-dialog {
    margin: 0 !important;
    &__header {
      height: 40px;
      padding: 0;
      margin-right: 0 !important;
      margin-bottom: 20px;
      background:
        url("@/assets/ai/zhuanlu/common_title.png") no-repeat,
        linear-gradient(to bottom, #0a1633dd, #0a1633dd); /* 叠加深色遮罩 */
      div {
        color: #73C4FF;
        margin-left: 20px;
      }
    }
    &__body {
      padding: 15px !important;
    }
    &__headerbtn {
      color: #73C4FF;
      top: 0;
    }
  }
}
</style>
src/components/Dialog/src/DialogHistory.vue
对比新文件
@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
defineOptions({ name: 'DialogHistory' })
const slots = useSlots()
const props = defineProps({
  modelValue: propTypes.bool.def(false),
  title: propTypes.string.def('Dialog'),
  fullscreen: propTypes.bool.def(true),
  width: propTypes.oneOfType([String, Number]).def('30%'),
  scroll: propTypes.bool.def(false), // 是否开启滚动条。如果是的话,按照 maxHeight 设置最大高度
  maxHeight: propTypes.oneOfType([String, Number]).def('400px')
})
const getBindValue = computed(() => {
  const delArr: string[] = ['fullscreen', 'title', 'maxHeight', 'appendToBody']
  const attrs = useAttrs()
  const obj = { ...attrs, ...props }
  for (const key in obj) {
    if (delArr.indexOf(key) !== -1) {
      delete obj[key]
    }
  }
  return obj
})
const isFullscreen = ref(false)
const toggleFull = () => {
  isFullscreen.value = !unref(isFullscreen)
}
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
watch(
  () => isFullscreen.value,
  async (val: boolean) => {
    await nextTick()
    if (val) {
      const windowHeight = document.documentElement.offsetHeight
      dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
    } else {
      dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
    }
  },
  {
    immediate: true
  }
)
</script>
<template>
  <ElDialog
    v-bind="getBindValue"
    :fullscreen="isFullscreen"
    :close-on-click-modal="true"
    :width="width"
    destroy-on-close
    lock-scroll
    draggable
    class="history-dialog"
    :show-close="false"
  >
    <template #header="{ close }">
      <div class="relative h-30px flex items-center justify-between pl-15px pr-15px">
        <slot name="title">
          {{ title }}
        </slot>
        <div
          class="absolute right-15px top-[50%] h-40px flex translate-y-[-50%] items-center justify-between"
        >
          <Icon
            v-if="fullscreen"
            class="is-hover mr-10px cursor-pointer"
            :icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
            color="#73C4FF"
            hover-color="var(--el-color-primary)"
            @click="toggleFull"
          />
          <Icon
            class="is-hover cursor-pointer"
            icon="ep:close"
            hover-color="var(--el-color-primary)"
            color="#73C4FF"
            @click="close"
          />
        </div>
      </div>
    </template>
    <slot></slot>
    <template v-if="slots.footer" #footer>
      <slot name="footer"></slot>
    </template>
  </ElDialog>
</template>
<style lang="scss">
.history-dialog {
  height: 90vh;
  margin-top: 30px;
  color: #73C4FF;
  overflow: hidden; /* 防止内容溢出 */
  background: rgba(3,29,76,0.79);
  border-radius: 4px 4px 4px 4px;
  border: 1px solid;
  .#{$elNamespace}-overlay-dialog {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .#{$elNamespace}-dialog {
    margin: 0 !important;
    &__header {
      height: 40px;
      padding: 0;
      margin-right: 0 !important;
      background:
        url("@/assets/ai/zhuanlu/common_title.png") left no-repeat,
        linear-gradient(to bottom, #0a1633dd, #0a1633dd); /* 叠加深色遮罩 */
      div {
        color: #73C4FF;
        margin-left: 20px;
      }
    }
    &__headerbtn {
      color: #73C4FF;
      top: 0;
    }
  }
}
</style>
src/components/MarkdownView/index.vue
@@ -39,7 +39,10 @@
/** 保留换行符 */
const formatContent = (text) => {
  return text.replace(/\n/g, '<br>')
  if (text) {
    return text.replace(/\n/g, '<br>')
  }
  return text
}
/** 初始化 **/
src/router/modules/remaining.ts
@@ -1,6 +1,8 @@
import {Layout} from '@/utils/routerHelper'
import {meta} from "eslint-plugin-prettier";
const { t } = useI18n()
/**
 * redirect: noredirect        当设置 noredirect 的时候该路由在面包屑导航中不可被点击
 * name:'router-name'          设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
@@ -576,11 +578,11 @@
  {
    path: '/ai/zhuanlu',
    name: 'Zhuanlu',
    component: () => import('@/views/ai/dashboard/zhuanlu/Index.vue'),
    component: () => import('@/views/ai/dashboard/zhuanlu/index.vue'),
    meta: {
      hidden: true,
      noTagsView: true
    },
      noTagsView: true,
    }
  },
  {
    path: '/model/analysis',
src/utils/rem.ts
对比新文件
@@ -0,0 +1,9 @@
// utils/rem.js
const baseWidth = 1920 // 设计稿基准宽度
const baseFontSize = 16 // 基准字体大小
export const setRem = () => {
  const scale = document.documentElement.clientWidth / baseWidth
  document.documentElement.style.fontSize =
    `${Math.min(scale, 1.5) * baseFontSize}px` // 限制最大缩放比例
}
src/views/ai/dashboard/components/conversation/CommonConversation.vue
对比新文件
@@ -0,0 +1,964 @@
<template>
  <!-- 整体容器添加相对定位 -->
  <div class="container-wrapper" ref="containerRef">
    <!-- 折叠按钮 -->
    <div
      class="sidebar-toggle"
      :style="{ left: toggleLeft }"
      @click.stop="toggleSidebar"
      ref="toggleRef">
      <Icon :icon="isCollapsed ? 'ep:caret-right' : 'ep:caret-left'" />
    </div>
    <!-- 左侧对话列表 -->
    <div
      class="conversation-wrapper"
      :class="{ collapsed: isCollapsed }"
      :style="{ width: sidebarWidth }"
      ref="sidebarRef">
      <ConversationList
        :active-id="activeConversationId"
        ref="conversationListRef"
        @on-conversation-create="handleConversationCreateSuccess"
        @on-conversation-click="handleConversationClick"
        @on-conversation-clear="handleConversationClear"
        @on-conversation-delete="handlerConversationDelete"
      />
    </div>
    <!-- 右侧:对话详情 -->
    <el-container class="detail-container">
      <el-header class="header">
        <div class="title">
          {{ activeConversation?.title ? activeConversation?.title : '对话' }}
          <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
        </div>
        <div class="btns" v-if="activeConversation">
          <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
            <span v-html="activeConversation?.modelName"></span>
            <Icon icon="ep:setting" class="ml-10px" />
          </el-button>
          <el-button size="small" class="btn" @click="handlerMessageClear">
            <Icon icon="heroicons-outline:archive-box-x-mark" color="#73C4FF" />
          </el-button>
          <el-button size="small" class="btn" @click="handleGoBottomMessage">
            <Icon icon="ep:download" color="#73C4FF" />
          </el-button>
          <el-button size="small" class="btn" @click="handleGoTopMessage">
            <Icon icon="ep:top" color="#73C4FF" />
          </el-button>
        </div>
      </el-header>
      <!-- main:消息列表 -->
      <el-main class="main-container">
        <div class="message-container">
          <!-- 情况一:消息加载中 -->
          <MessageLoading v-if="activeMessageListLoading" />
          <!-- 情况二:无聊天对话时 -->
          <MessageNewConversation
            v-if="!activeConversation"
            @on-new-conversation="handleConversationCreate"
          />
          <!-- 情况三:消息列表为空 -->
          <MessageListEmpty
            v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation"
            @on-prompt="doSendMessage"
          />
          <!-- 情况四:消息列表不为空 -->
          <MessageList
            v-if="!activeMessageListLoading && messageList.length > 0"
            ref="messageRef"
            :conversation="activeConversation"
            :list="messageList"
            @on-delete-success="handleMessageDelete"
            @on-edit="handleMessageEdit"
            @on-refresh="handleMessageRefresh"
          />
        </div>
      </el-main>
      <!-- 底部 -->
      <el-footer class="footer-container">
        <!-- 输入框 -->
        <div class="input-container">
          <form class="prompt-from">
            <textarea
              class="prompt-input"
              v-model="prompt"
              @keydown="handleSendByKeydown"
              @input="handlePromptInput"
              @compositionstart="onCompositionstart"
              @compositionend="onCompositionend"
              placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
            ></textarea>
            <div class="prompt-btns">
              <div class="content">
                <el-button
                  :class="{ 'active-button': enableContext }"
                  @click="enableContext = !enableContext"
                >
                  <el-icon class="content-icon" />
                  上下文
                </el-button>
              </div>
              <div class="message">
                <el-button
                  type="primary"
                  size="default"
                  @click="handleSendByButton"
                  :loading="conversationInProgress"
                  v-if="conversationInProgress == false"
                >
                  {{ conversationInProgress ? '进行中' : '发消息' }}
                </el-button>
                <el-button
                  type="danger"
                  size="default"
                  @click="stopStream()"
                  v-if="conversationInProgress == true"
                >
                  停止
                </el-button>
              </div>
            </div>
          </form>
        </div>
      </el-footer>
    </el-container>
  </div>
  <!-- 更新对话 Form -->
  <ConversationUpdateForm
    ref="conversationUpdateFormRef"
    @success="handleConversationUpdateSuccess"
  />
</template>
<script setup lang="ts">
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import ConversationList from './CommonConversationList.vue'
import ConversationUpdateForm from './CommonConversationUpdateForm.vue'
import MessageList from '../message/MessageList.vue'
import MessageListEmpty from '../message/MessageListEmpty.vue'
import MessageLoading from '../message/MessageLoading.vue'
import MessageNewConversation from '../message/MessageNewConversation.vue'
import { onClickOutside } from '@vueuse/core'
import * as authUtil from "@/utils/auth";
import {refreshToken} from "@/api/login";
/** AI 聊天对话 列表 */
defineOptions({ name: 'NormalConversation' })
const route = useRoute() // 路由
const message = useMessage() // 消息弹窗
const isCollapsed = ref(true)
const sidebarWidth = ref('270px')
const toggleLeft = computed(() => isCollapsed.value ? '0' : sidebarWidth.value)
// 新增DOM引用用于会话列表的展开和收缩
const containerRef = ref<HTMLElement>()
const sidebarRef = ref<HTMLElement>()
const toggleRef = ref<HTMLElement>()
// 点击外部区域处理
onClickOutside(sidebarRef, (event) => {
  if (!isCollapsed.value &&
    !sidebarRef.value?.contains(event.target) &&
    !toggleRef.value?.contains(event.target)) {
    isCollapsed.value = true
  }
})
// 切换侧边栏
const toggleSidebar = () => {
  isCollapsed.value = !isCollapsed.value
}
// 聊天对话
const conversationListRef = ref()
const activeConversationId = ref<number | null>(null) // 选中的对话编号
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
// 消息列表
const messageRef = ref()
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表
const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中
const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中
// 消息滚动
const textSpeed = ref<number>(50) // Typing speed in milliseconds
const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
// 发送消息输入框
const isComposing = ref(false) // 判断用户是否在输入
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
const inputTimeout = ref<any>() // 处理输入中回车的定时器
const prompt = ref<string>() // prompt
const enableContext = ref<boolean>(true) // 是否开启上下文
// 接收 Stream 消息
const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('')
// =========== 【聊天对话】相关 ===========
/** 获取对话信息 */
const getConversation = async (id: number | null) => {
  if (!id) {
    return
  }
  const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id)
  if (!conversation) {
    return
  }
  activeConversation.value = conversation
  activeConversationId.value = conversation.id
}
/**
 * 点击某个对话
 *
 * @param conversation 选中的对话
 * @return 是否切换成功
 */
const handleConversationClick = async (conversation: ChatConversationVO) => {
  // 对话进行中,不允许切换
  if (conversationInProgress.value) {
    message.alert('对话中,不允许切换!')
    return false
  }
  // 更新选中的对话 id
  activeConversationId.value = conversation.id
  activeConversation.value = conversation
  // 刷新 message 列表
  await getMessageList()
  // 滚动底部
  scrollToBottom(true)
  // 清空输入框
  prompt.value = ''
  return true
}
/** 删除某个对话*/
const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
  // 删除的对话如果是当前选中的,那么就重置
  if (activeConversationId.value === delConversation.id) {
    await handleConversationClear()
  }
}
/** 清空选中的对话 */
const handleConversationClear = async () => {
  // 对话进行中,不允许切换
  if (conversationInProgress.value) {
    message.alert('对话中,不允许切换!')
    return false
  }
  activeConversationId.value = null
  activeConversation.value = null
  activeMessageList.value = []
}
/** 修改聊天对话 */
const conversationUpdateFormRef = ref()
const openChatConversationUpdateForm = async () => {
  conversationUpdateFormRef.value.open(activeConversationId.value)
}
const handleConversationUpdateSuccess = async () => {
  // 对话更新成功,刷新最新信息
  await getConversation(activeConversationId.value)
}
/** 处理聊天对话的创建成功 */
const handleConversationCreate = async () => {
  // 创建对话
  await conversationListRef.value.createConversation()
}
/** 处理聊天对话的创建成功 */
const handleConversationCreateSuccess = async () => {
  // 创建新的对话,清空输入框
  prompt.value = ''
}
// =========== 【消息列表】相关 ===========
/** 获取消息 message 列表 */
const getMessageList = async () => {
  try {
    if (activeConversationId.value === null) {
      return
    }
    // Timer 定时器,如果加载速度很快,就不进入加载中
    activeMessageListLoadingTimer.value = setTimeout(() => {
      activeMessageListLoading.value = true
    }, 60)
    // 获取消息列表
    activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
      activeConversationId.value
    )
    // 滚动到最下面
    await nextTick()
    await scrollToBottom()
  } finally {
    // time 定时器,如果加载速度很快,就不进入加载中
    if (activeMessageListLoadingTimer.value) {
      clearTimeout(activeMessageListLoadingTimer.value)
    }
    // 加载结束
    activeMessageListLoading.value = false
  }
}
/**
 * 消息列表
 *
 * 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去
 */
const messageList = computed(() => {
  if (activeMessageList.value.length > 0) {
    dealResult(activeMessageList.value)
    return activeMessageList.value
  }
  // 没有消息时,如果有 systemMessage 则展示它
  if (activeConversation.value?.systemMessage) {
    return [
      {
        id: 0,
        type: 'system',
        content: activeConversation.value.systemMessage
      }
    ]
  }
  return []
})
//处理调度推理结论
const dealResult = (conversations: any) => {
  const regex = /<think>(\n*)([\s\S]*?)(\n*)<\/think>(\n*)([\s\S]*)/;
  conversations.forEach((conversation) => {
    if(conversation.content.includes('<\/think>')) {
      conversation.thinkingFlag = false
    } else {
      conversation.thinkingFlag = true
    }
    const match = conversation.content.match(regex);
    if(match) {
      conversation.thinking = match[2];
      conversation.conclusion = match[5]
    }
  })
}
/** 处理删除 message 消息 */
const handleMessageDelete = () => {
  if (conversationInProgress.value) {
    message.alert('回答中,不能删除!')
    return
  }
  // 刷新 message 列表
  getMessageList()
}
/** 处理 message 清空 */
const handlerMessageClear = async () => {
  if (!activeConversationId.value) {
    return
  }
  try {
    // 确认提示
    await message.delConfirm('确认清空对话消息?')
    // 清空对话
    await ChatMessageApi.deleteByConversationId(activeConversationId.value)
    // 刷新 message 列表
    activeMessageList.value = []
  } catch {}
}
/** 回到 message 列表的顶部 */
const handleGoTopMessage = () => {
  messageRef.value.handlerGoTop()
}
/** 回到 message 列表的底部 */
const handleGoBottomMessage = () => {
  messageRef.value.handleGoBottom()
}
// =========== 【发送消息】相关 ===========
/** 处理来自 keydown 的发送消息 */
const handleSendByKeydown = async (event) => {
  // 判断用户是否在输入
  if (isComposing.value) {
    return
  }
  // 进行中不允许发送
  if (conversationInProgress.value) {
    return
  }
  const content = prompt.value?.trim() as string
  if (event.key === 'Enter') {
    if (event.shiftKey) {
      // 插入换行
      prompt.value += '\r\n'
      event.preventDefault() // 防止默认的换行行为
    } else {
      // 发送消息
      await doSendMessage(content)
      event.preventDefault() // 防止默认的提交行为
    }
  }
}
/** 处理来自【发送】按钮的发送消息 */
const handleSendByButton = () => {
  doSendMessage(prompt.value?.trim() as string)
}
/** 处理 prompt 输入变化 */
const handlePromptInput = (event) => {
  // 非输入法 输入设置为 true
  if (!isComposing.value) {
    // 回车 event data 是 null
    if (event.data == null) {
      return
    }
    isComposing.value = true
  }
  // 清理定时器
  if (inputTimeout.value) {
    clearTimeout(inputTimeout.value)
  }
  // 重置定时器
  inputTimeout.value = setTimeout(() => {
    isComposing.value = false
  }, 400)
}
// TODO @芋艿:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
const onCompositionstart = () => {
  isComposing.value = true
}
const onCompositionend = () => {
  // console.log('输入结束...')
  setTimeout(() => {
    isComposing.value = false
  }, 200)
}
/** 真正执行【发送】消息操作 */
const doSendMessage = async (content: string) => {
  // 校验
  if (content.length < 1) {
    message.error('发送失败,原因:内容为空!')
    return
  }
  if (activeConversationId.value == null) {
    message.error('还没创建对话,不能发送!')
    return
  }
  // 清空输入框
  prompt.value = ''
  // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
  authUtil.setToken(await refreshToken())
  // 执行发送
  await doSendMessageStream({
    conversationId: activeConversationId.value,
    content: content
  } as ChatMessageVO)
}
/** 真正执行【发送】消息操作 */
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
  // 创建 AbortController 实例,以便中止请求
  conversationInAbortController.value = new AbortController()
  // 标记对话进行中
  conversationInProgress.value = true
  // 设置为空
  receiveMessageFullText.value = ''
  try {
    // 1.1 先添加两个假数据,等 stream 返回再替换
    activeMessageList.value.push({
      id: -1,
      conversationId: activeConversationId.value,
      type: 'user',
      content: userMessage.content,
      createTime: new Date()
    } as ChatMessageVO)
    activeMessageList.value.push({
      id: -2,
      conversationId: activeConversationId.value,
      type: 'assistant',
      content: '思考中...',
      createTime: new Date()
    } as ChatMessageVO)
    // 1.2 滚动到最下面
    await nextTick()
    await scrollToBottom() // 底部
    // 1.3 开始滚动
    textRoll()
    // 2. 发送 event stream
    let isFirstChunk = true // 是否是第一个 chunk 消息段
    await ChatMessageApi.sendChatMessageStream(
      userMessage.conversationId,
      userMessage.content,
      conversationInAbortController.value,
      enableContext.value,
      async (res) => {
        const { code, data, msg } = JSON.parse(res.data)
        if (code !== 0) {
          message.alert(`对话异常! ${msg}`)
          return
        }
        // 如果内容为空,就不处理。
        if (data.receive.content === '') {
          return
        }
        // 首次返回需要添加一个 message 到页面,后面的都是更新
        if (isFirstChunk) {
          isFirstChunk = false
          // 弹出两个假数据
          activeMessageList.value.pop()
          activeMessageList.value.pop()
          // 更新返回的数据
          activeMessageList.value.push(data.send)
          activeMessageList.value.push(data.receive)
        }
        // debugger
        receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
        // 滚动到最下面
        await scrollToBottom()
      },
      (error) => {
        message.alert(`对话异常! ${error}`)
        stopStream()
      },
      () => {
        stopStream()
      }
    )
  } catch {}
}
/** 停止 stream 流式调用 */
const stopStream = async () => {
  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
  if (conversationInAbortController.value) {
    conversationInAbortController.value.abort()
  }
  // 设置为 false
  conversationInProgress.value = false
}
/** 编辑 message:设置为 prompt,可以再次编辑 */
const handleMessageEdit = (message: ChatMessageVO) => {
  prompt.value = message.content
}
/** 刷新 message:基于指定消息,再次发起对话 */
const handleMessageRefresh = (message: ChatMessageVO) => {
  doSendMessage(message.content)
}
// ============== 【消息滚动】相关 =============
/** 滚动到 message 底部 */
const scrollToBottom = async (isIgnore?: boolean) => {
  await nextTick()
  if (messageRef.value) {
    messageRef.value.scrollToBottom(isIgnore)
  }
}
/** 自提滚动效果 */
const textRoll = async () => {
  let index = 0
  try {
    // 只能执行一次
    if (textRoleRunning.value) {
      return
    }
    // 设置状态
    textRoleRunning.value = true
    receiveMessageDisplayedText.value = ''
    const task = async () => {
      // 调整速度
      const diff =
        (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
      if (diff > 5) {
        textSpeed.value = 10
      } else if (diff > 2) {
        textSpeed.value = 30
      } else if (diff > 1.5) {
        textSpeed.value = 50
      } else {
        textSpeed.value = 100
      }
      // 对话结束,就按 30 的速度
      if (!conversationInProgress.value) {
        textSpeed.value = 10
      }
      if (index < receiveMessageFullText.value.length) {
        receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
        index++
        // 更新 message
        const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
        lastMessage.content = receiveMessageDisplayedText.value
        // 滚动到住下面
        await scrollToBottom()
        // 重新设置任务
        timer = setTimeout(task, textSpeed.value)
      } else {
        // 不是对话中可以结束
        if (!conversationInProgress.value) {
          textRoleRunning.value = false
          clearTimeout(timer)
        } else {
          // 重新设置任务
          timer = setTimeout(task, textSpeed.value)
        }
      }
    }
    let timer = setTimeout(task, textSpeed.value)
  } catch {}
}
/** 初始化 **/
onMounted(async () => {
  // 如果有 conversationId 参数,则默认选中
  if (route.query.conversationId) {
    const id = route.query.conversationId as unknown as number
    activeConversationId.value = id
    await getConversation(id)
  }
  // 获取列表数据
  activeMessageListLoading.value = true
  await getMessageList()
})
</script>
<style lang="scss" scoped>
.container-wrapper {
  position: relative;  // 关键定位容器
  width: 100%;
  height: 100%;
}
.sidebar-toggle {
  position: absolute;
  left: 300px;  // 初始展开位置
  top: 40%;
  z-index: 1000;
  width: 20px;
  height: 80px;
  background: rgba(115, 196, 255, 0.5);
  border-radius: 0 8px 8px 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease;
  color: rgba(255,215,0);
  transition: left 0.3s ease, background 0.2s ease;
  &:hover {
    background: #409EFF;
    left: 304px;  // 悬停微调
    transition: left 0.3s ease, background 0.2s ease;
  }
}
.conversation-wrapper {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 300px;
  background: rgba(13,28,58,0.9);
  box-shadow: 2px 0 8px rgba(0,0,0,0.1);
  transition: transform 0.3s ease, opacity 0.2s ease;
  z-index: 999;
  overflow: hidden;
  &.collapsed {
    transform: translateX(-100%);
    opacity: 0;
    pointer-events: none;
  }
}
// 头部
.detail-container {
  width: 100%;
  height: 885px;
  margin-left: 5px;
  background-color: rgba(0, 0, 0, 0); /* 透明背景 */
  transition: margin 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 1;
  .header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    box-shadow: 0 0 0 0 #dcdfe6;
    .title {
      font-size: 18px;
      font-weight: bold;
      color: gold;
    }
    .btns {
      display: flex;
      width: 300px;
      flex-direction: row;
      justify-content: flex-end;
      .btn {
        padding: 10px;
      }
      /* 所有状态通用透明背景 */
      :deep(.el-button) {
        background: transparent !important;
        border-color: currentColor; /* 保持与文字同色 */
        color: #409EFF; /* 蓝色文字 */
      }
      /* 悬停状态 */
      :deep(.el-button:hover) {
        background: rgba(0, 0, 0, 0.05) !important; /* 轻微悬停反馈 */
      }
      /* 点击状态 */
      :deep(.el-button:active) {
        background: rgba(0, 0, 0, 0.1) !important;
      }
      /* 禁用状态 */
      :deep(.el-button.is-disabled) {
        opacity: 0.6;
        background: transparent !important;
      }
    }
  }
  &[style*="0"] {
    margin-left: 0 !important;
  }
}
// main 容器
.main-container {
  margin-left: 10px;
  padding: 0;
  position: relative;
  height: 500px;
  width: 100%;
  .message-container {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    overflow-y: hidden;
    padding: 0;
    margin: 0;
    /* Firefox */
    scrollbar-width: thin;
    scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
    /* WebKit */
    &::-webkit-scrollbar {
      width: 6px;
      background: transparent;
    }
    &::-webkit-scrollbar-thumb {
      border-radius: 4px;
      background: rgba(0, 0, 0, 0.15);
      transition: background 0.3s;
      &:hover { background: rgba(0, 0, 0, 0.25); }
    }
  }
}
// 底部
.footer-container {
  display: flex;
  flex-direction: column;
  height: 114px;
  margin-left: 10px;
  padding: 0;
  // 输入框
  .input-container {
    display: flex;
    flex-direction: column;
    height: auto;
    width: 876px;
    margin: 0;
    padding: 0;
    overflow-y: auto;        /* 垂直方向溢出时显示滚动条 */
    overflow-x: hidden;      /* 水平方向隐藏滚动条 */
    /* Firefox */
    scrollbar-width: thin;
    scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
    /* WebKit */
    &::-webkit-scrollbar {
      width: 6px;
      background: transparent;
    }
    &::-webkit-scrollbar-thumb {
      border-radius: 4px;
      background: rgba(0, 0, 0, 0.15);
      transition: background 0.3s;
      &:hover { background: rgba(0, 0, 0, 0.25); }
    }
    .prompt-from {
      display: flex;
      flex-direction: column;
      padding: 9px 10px;
      width: 876px;
      height: 114px;
      background: rgba(115,196,255,0.05);
      border-radius: 4px 4px 4px 4px;
      border: 1px solid #73C4FF;
    }
    textarea::placeholder {
      color: #DBEEFF;
    }
    .prompt-input {
      width: 876px;
      height: 113.55px;
      font-weight: 400;
      font-size: 14px;
      background-color: rgba(219,238,255,0);
      line-height: 21px;
      text-align: left;
      font-style: normal;
      text-transform: none;
      border: 0;
      color: #73C4FF;
    }
    .prompt-input:focus {
      outline: none;
    }
    .prompt-btns {
      display: flex;
      justify-content: space-between;
      padding-bottom: 0;
      padding-top: 5px;
      .content {
        /* 默认状态 */
        .el-button {
          background: transparent !important;
          border-color: rgba(115, 196, 255, 0.5);
          color: #73C4FF;
          border-radius: 15px !important;
        }
        /* 上下文图标处理 */
        .content-icon {
          color: blue;  /* 图标颜色 */
          font-size: 18px;
          margin-right: 10px;
          background: url("@/assets/ai/zhuanlu/content.png");
          vertical-align: middle;
        }
        /* 选中状态 */
        .active-button {
          background: #409eff !important;
          border-color: #409eff !important;
          color: white !important;
          .content-icon {
            background: url("@/assets/ai/zhuanlu/content_select.png");
            vertical-align: middle;
          }
        }
        /* 按钮组间距处理 */
        .button-group .el-button {
          margin-left: 0;
          border-radius: 4px;
        }
        /* 悬停效果 */
        .el-button:not(.active-button):hover {
          border-color: rgba(115,196,255,0.5);
          color: #409eff;
        }
      }
      .message {
        /* 所有状态通用透明背景 */
        :deep(.el-button) {
          background: rgba(73, 254, 210, 0.8) !important;
          border-color: currentColor; /* 保持与文字同色 */
          font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
          font-weight: bold;
          font-size: 16px;
          color: #123C4E;
          clip-path: polygon(
              0 0,
              100% 0,
              100% 100%,
              10px 100%,          /* 右下方向留出10px */
              0 calc(100% - 10px) /* 左上方向留出10px */
          );
          position: relative;
          padding-left: 15px; /* 增加右侧留白 */
        }
        /* 悬停状态 */
        :deep(.el-button:hover) {
          background: rgba(73, 254, 210, 0.6) !important; /* 轻微悬停反馈 */
        }
        /* 点击状态 */
        :deep(.el-button:active) {
          background: rgba(73, 254, 210, 1) !important;
        }
        /* 禁用状态 */
        :deep(.el-button.is-disabled) {
          opacity: 0.6;
          background: transparent !important;
        }
        /* 核心样式覆盖 */
        :deep(.el-switch__core) {
          background: transparent !important;
          border-radius: 0 0 15px 0 !important;
          border: none !important;
          height: 40px !important;
        }
        /* 按钮内容容器 */
        .button-content {
          display: flex;
          align-items: center;
          padding: 0 15px;
          height: 100%;
        }
      }
    }
  }
}
</style>
src/views/ai/dashboard/components/conversation/CommonConversationList.vue
对比新文件
@@ -0,0 +1,526 @@
<!--  AI 对话  -->
<template>
  <el-aside width="260px" class="conversation-container h-100%">
    <!-- 左顶部:对话 -->
    <div class="h-100%">
      <div class="conversation-title">
        <img
          src="@/assets/ai/zhuanlu/conversation_big.png"
          class="mr-3px w-[1.2em] h-[1.2em]"
          alt="icon"
        />
        对话列表
      </div>
<!--      <hr class="line"/>-->
      <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
        <img
          src="@/assets/ai/zhuanlu/conversation_big.png"
          class="mr-8px w-[1.5em] h-[1.5em]"
          alt="icon"
        />
        开始新对话
      </el-button>
      <!-- 左顶部:搜索对话 -->
      <el-input
        v-model="searchName"
        size="large"
        class="mt-10px search-input"
        placeholder="搜索历史记录"
        @keyup="searchConversation"
      >
        <template #prefix>
          <Icon icon="ep:search" />
        </template>
      </el-input>
      <!-- 左中间:对话列表 -->
      <div class="conversation-list">
        <!-- 情况一:加载中 -->
        <el-empty v-if="loading" description="." :v-loading="loading" />
        <!-- 情况二:按照 group 分组,展示聊天会话 list 列表 -->
        <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
          <div
            class="conversation-item classify-title"
            v-if="conversationMap[conversationKey].length"
          >
            <el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
          </div>
          <div
            class="conversation-item"
            v-for="conversation in conversationMap[conversationKey]"
            :key="conversation.id"
            @click="handleConversationClick(conversation.id)"
            @mouseover="hoverConversationId = conversation.id"
            @mouseout="hoverConversationId = ''"
          >
            <div
              :class="
                conversation.id === activeConversationId ? 'conversation active' : 'conversation'
              "
            >
              <div class="title-wrapper">
                <img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" />
                <span class="title">{{ conversation.title }}</span>
              </div>
              <div class="button-wrapper" v-show="hoverConversationId === conversation.id">
                <el-button class="btn" link @click.stop="handleTop(conversation)">
                  <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon>
                  <el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon>
                </el-button>
                <el-button class="btn" link @click.stop="updateConversationTitle(conversation)">
                  <el-icon title="编辑">
                    <Icon icon="ep:edit" />
                  </el-icon>
                </el-button>
                <el-button class="btn" link @click.stop="deleteChatConversation(conversation)">
                  <el-icon title="删除对话">
                    <Icon icon="ep:delete" />
                  </el-icon>
                </el-button>
              </div>
            </div>
          </div>
        </div>
        <!-- 底部占位  -->
        <div class="h-160px w-100%"></div>
      </div>
    </div>
    <!-- 左底部:工具栏 -->
    <div class="tool-box">
      <div @click="handleClearConversation">
        <Icon icon="ep:delete" />
        <el-text size="small">清空未置顶对话</el-text>
      </div>
    </div>
  </el-aside>
</template>
<script setup lang="ts">
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { Bottom, Top } from '@element-plus/icons-vue'
import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png'
const message = useMessage() // 消息弹窗
// 定义属性
const searchName = ref<string>('') // 对话搜索
const modelName = ref<string>('common') // 对话搜索
const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null
const hoverConversationId = ref<number | null>(null) // 悬浮上去的对话
const conversationList = ref([] as ChatConversationVO[]) // 对话列表
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
const loading = ref<boolean>(false) // 加载中
const loadingTime = ref<any>() // 加载中定时器
// 定义组件 props
const props = defineProps({
  activeId: {
    type: String || null,
    required: true
  }
})
// 定义钩子
const emits = defineEmits([
  'onConversationCreate',
  'onConversationClick',
  'onConversationClear',
  'onConversationDelete'
])
/** 搜索对话 */
const searchConversation = async (e) => {
  // 恢复数据
  if (!searchName.value.trim().length) {
    conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
  } else {
    // 过滤
    const filterValues = conversationList.value.filter((item) => {
      return item.title.includes(searchName.value.trim())
    })
    conversationMap.value = await getConversationGroupByCreateTime(filterValues)
  }
}
/** 点击对话 */
const handleConversationClick = async (id: number) => {
  // 过滤出选中的对话
  const filterConversation = conversationList.value.filter((item) => {
    return item.id === id
  })
  // 回调 onConversationClick
  // noinspection JSVoidFunctionReturnValueUsed
  const success = emits('onConversationClick', filterConversation[0])
  // 切换对话
  if (success) {
    activeConversationId.value = id
  }
}
/** 获取对话列表 */
const getChatConversationList = async () => {
  try {
    // 加载中
    loadingTime.value = setTimeout(() => {
      loading.value = true
    }, 50)
    // 1.1 获取 对话数据
    conversationList.value = await ChatConversationApi.getChatConversationEnergyList(modelName.value)
    if(conversationList.value.length == 0) {
      await createConversation()
    }
    // 1.2 排序
    conversationList.value.sort((a, b) => {
      return b.createTime - a.createTime
    })
    // 1.3 没有任何对话情况
    if (conversationList.value.length === 0) {
      activeConversationId.value = null
      conversationMap.value = {}
      return
    }
    // 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前)
    conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
  } finally {
    // 清理定时器
    if (loadingTime.value) {
      clearTimeout(loadingTime.value)
    }
    // 加载完成
    loading.value = false
  }
}
/** 按照 creteTime 创建时间,进行分组 */
const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
  // 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
  // noinspection NonAsciiCharacters
  const groupMap = {
    置顶: [],
    今天: [],
    一天前: [],
    三天前: [],
    七天前: [],
    三十天前: []
  }
  // 当前时间的时间戳
  const now = Date.now()
  // 定义时间间隔常量(单位:毫秒)
  const oneDay = 24 * 60 * 60 * 1000
  const threeDays = 3 * oneDay
  const sevenDays = 7 * oneDay
  const thirtyDays = 30 * oneDay
  for (const conversation of list) {
    // 置顶
    if (conversation.pinned) {
      groupMap['置顶'].push(conversation)
      continue
    }
    // 计算时间差(单位:毫秒)
    const diff = now - conversation.createTime
    // 根据时间间隔判断
    if (diff < oneDay) {
      groupMap['今天'].push(conversation)
    } else if (diff < threeDays) {
      groupMap['一天前'].push(conversation)
    } else if (diff < sevenDays) {
      groupMap['三天前'].push(conversation)
    } else if (diff < thirtyDays) {
      groupMap['七天前'].push(conversation)
    } else {
      groupMap['三十天前'].push(conversation)
    }
  }
  return groupMap
}
/** 新建对话 */
const createConversation = async () => {
  // 1. 新建对话
  const conversationId = await ChatConversationApi.createChatConversationEnergy(
    {modelName: modelName.value} as unknown as ChatConversationVO
  )
  // 2. 获取对话内容
  await getChatConversationList()
  // 3. 选中对话
  await handleConversationClick(conversationId)
  // 4. 回调
  emits('onConversationCreate')
}
/** 修改对话的标题 */
const updateConversationTitle = async (conversation: ChatConversationVO) => {
  // 1. 二次确认
  const { value } = await ElMessageBox.prompt('修改标题', {
    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
    inputErrorMessage: '标题不能为空',
    inputValue: conversation.title
  })
  // 2. 发起修改
  await ChatConversationApi.updateChatConversationMy({
    id: conversation.id,
    title: value
  } as ChatConversationVO)
  message.success('重命名成功')
  // 3. 刷新列表
  await getChatConversationList()
  // 4. 过滤当前切换的
  const filterConversationList = conversationList.value.filter((item) => {
    return item.id === conversation.id
  })
  if (filterConversationList.length > 0) {
    // tip:避免切换对话
    if (activeConversationId.value === filterConversationList[0].id) {
      emits('onConversationClick', filterConversationList[0])
    }
  }
}
/** 删除聊天对话 */
const deleteChatConversation = async (conversation: ChatConversationVO) => {
  try {
    // 删除的二次确认
    await message.delConfirm(`是否确认删除对话 - ${conversation.title}?`)
    // 发起删除
    await ChatConversationApi.deleteChatConversationMy(conversation.id)
    message.success('对话已删除')
    // 刷新列表
    await getChatConversationList()
    // 回调
    emits('onConversationDelete', conversation)
  } catch {}
}
/** 清空对话 */
const handleClearConversation = async () => {
  try {
    await message.confirm('确认后对话会全部清空,置顶的对话除外。')
    await ChatConversationApi.deleteChatConversationMyByUnpinned()
    ElMessage({
      message: '操作成功!',
      type: 'success'
    })
    // 清空 对话 和 对话内容
    activeConversationId.value = null
    // 获取 对话列表
    await getChatConversationList()
    // 回调 方法
    emits('onConversationClear')
  } catch {}
}
/** 对话置顶 */
const handleTop = async (conversation: ChatConversationVO) => {
  // 更新对话置顶
  conversation.pinned = !conversation.pinned
  await ChatConversationApi.updateChatConversationMy(conversation)
  // 刷新对话
  await getChatConversationList()
}
// ============ 角色仓库 ============
/** 角色仓库抽屉 */
const roleRepositoryOpen = ref<boolean>(false) // 角色仓库是否打开
const handleRoleRepository = async () => {
  roleRepositoryOpen.value = !roleRepositoryOpen.value
}
/** 监听选中的对话 */
const { activeId } = toRefs(props)
watch(activeId, async (newValue, oldValue) => {
  activeConversationId.value = newValue as string
})
// 定义 public 方法
defineExpose({ createConversation })
/** 初始化 */
onMounted(async () => {
  // 获取 对话列表
  await getChatConversationList()
  // 默认选中
  if (props.activeId) {
    activeConversationId.value = props.activeId
  } else {
    // 首次默认选中第一个
    if (conversationList.value.length) {
      activeConversationId.value = conversationList.value[0].id
      // 回调 onConversationClick
      await emits('onConversationClick', conversationList.value[0])
    }
  }
})
</script>
<style scoped lang="scss">
.conversation-container {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 10px 10px 0;
  overflow: hidden;
  .conversation-title {
    color: #73C4FF;
    line-height: 25px;
    margin-bottom: 8px;
    border-bottom: 1px solid rgba(69,133,255,0.2);
    img {
      padding: 6px 0 0 5px;
    }
  }
  .line {
    margin: 5px 0;
  }
  .btn-new-conversation {
    border-radius: 4px;
    border: 1px solid rgba(69,133,255,0.6);
    background: rgba(69,133,255,0.4);
    color: #73C4FF;
  }
  .search-input {
    height: 30px;
    border-radius: 4px;
    border: 1px solid rgba(69,133,255,0.6);
    background: rgba(69,133,255,0.4);
  }
  .conversation-list {
    overflow: auto;
    height: 100%;
    .classify-title {
      padding-top: 10px;
      b {
        color: white;
      }
    }
    .conversation-item {
      margin-top: 5px;
    }
    .conversation {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      flex: 1;
      padding: 0 5px;
      cursor: pointer;
      border-radius: 5px;
      align-items: center;
      line-height: 30px;
      background-color: rgba(69,133,255,0.1);
      &.active {
        background-color: rgba(69,133,255,0.5);
        .button {
          display: inline-block;
        }
        .title-wrapper {
          > span {
            font-weight: bold;
            color: rgba(115, 196, 255);
          }
        }
      }
      .title-wrapper {
        display: flex;
        flex-direction: row;
        align-items: center;
        > span {
          color: rgba(115, 196, 255, 0.5);
        }
      }
      .title {
        padding: 2px 10px;
        max-width: 220px;
        font-size: 14px;
        font-weight: 400;
        color: rgba(0, 0, 0, 0.77);
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
      .avatar {
        width: 25px;
        height: 25px;
        border-radius: 5px;
        display: flex;
        flex-direction: row;
        justify-items: center;
      }
      // 对话编辑、删除
      .button-wrapper {
        right: 2px;
        display: flex;
        flex-direction: row;
        justify-items: center;
        color: #606266;
        .btn {
          margin: 0;
          color: #73C4FF;
        }
      }
    }
  }
  // 角色仓库、清空未设置对话
  .tool-box {
    bottom: 0;
    padding: 0 20px;
    background-color: rgba(69,133,255,0.2);
    box-shadow: 0 0 1px 1px rgba(69,133,255,0.4);
    line-height: 35px;
    justify-content: space-between;
    align-items: center;
    color: var(--el-text-color);
    div {
      display: flex;
      margin-left: 20%;
      align-items: center;
      color: #73C4FF;
      padding: 0;
      cursor: pointer;
      > span {
        color: #73C4FF;
        margin-left: 5px;
      }
    }
  }
}
/* 移除所有输入框边框 */
:deep(.el-form-item .el-input__wrapper) {
  border: none !important;
  box-shadow: none !important;
  background: rgba(255,255,255,0.1) !important; /* 保留浅色背景 */
}
/* 移除输入框边框 */
:deep(.el-input .el-input__wrapper) {
  border: 1px solid #1E5A86 !important;
  box-shadow: none !important;
}
:deep(.el-input__inner) {
  color: #73C4FF;
}
:deep(.el-input__wrapper) {
  background: rgba(0,194,255,0.08) !important;
}
</style>
src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue
对比新文件
@@ -0,0 +1,219 @@
<template>
  <DialogDashboard title="模型设定" v-model="dialogVisible">
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="130px"
      v-loading="formLoading"
    >
      <el-form-item label="模型" prop="modelId">
        <el-select v-model="formData.modelId" disabled>
          <el-option
            v-for="model in models"
            :key="model.id"
            :label="model.name"
            :value="model.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="温度参数" prop="temperature">
        <el-input-number
          v-model="formData.temperature"
          placeholder="请输入温度参数"
          :min="0"
          :max="2"
          :precision="2"
          class="!w-1/1"
        />
      </el-form-item>
      <el-form-item label="回复数 Token 数" prop="maxTokens">
        <el-input-number
          v-model="formData.maxTokens"
          placeholder="请输入回复数 Token 数"
          :min="0"
          :max="8192"
          class="!w-1/1"
        />
      </el-form-item>
      <el-form-item label="上下文数量" prop="maxContexts">
        <el-input-number
          v-model="formData.maxContexts"
          placeholder="请输入上下文数量"
          :min="0"
          :max="20"
          class="!w-1/1"
        />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </DialogDashboard>
</template>
<script setup lang="ts">
import { ModelApi, ModelVO } from '@/api/ai/model/model'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
/** AI 聊天对话的更新表单 */
defineOptions({ name: 'ChatConversationUpdateForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
  id: undefined,
  systemMessage: undefined,
  modelId: undefined,
  temperature: undefined,
  maxTokens: undefined,
  maxContexts: undefined
})
const formRules = reactive({
  modelId: [{ required: true, message: '模型不能为空', trigger: 'blur' }],
  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
  temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }],
  maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }],
  maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const models = ref([] as ModelVO[]) // 聊天模型列表
/** 打开弹窗 */
const open = async (id: number) => {
  dialogVisible.value = true
  resetForm()
  // 修改时,设置数据
  if (id) {
    formLoading.value = true
    try {
      const data = await ChatConversationApi.getChatConversationMy(id)
      formData.value = Object.keys(formData.value).reduce((obj, key) => {
        if (data.hasOwnProperty(key)) {
          obj[key] = data[key]
        }
        return obj
      }, {})
    } finally {
      formLoading.value = false
    }
  }
  // 获得下拉数据
  models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
  // 校验表单
  await formRef.value.validate()
  // 提交请求
  formLoading.value = true
  try {
    const data = formData.value as unknown as ChatConversationVO
    await ChatConversationApi.updateChatConversationMy(data)
    message.success('对话配置已更新')
    dialogVisible.value = false
    // 发送操作成功的事件
    emit('success')
  } finally {
    formLoading.value = false
  }
}
/** 重置表单 */
const resetForm = () => {
  formData.value = {
    id: undefined,
    systemMessage: undefined,
    modelId: undefined,
    temperature: undefined,
    maxTokens: undefined,
    maxContexts: undefined
  }
  formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
:deep(.el-form-item__label) {
  color: #73C4FF;
}
:deep(.el-form-item__content) {
  .el-input-number {
    border: black solid 1px !important;
  }
}
/* 移除所有输入框边框 */
:deep(.el-form-item .el-input__wrapper) {
  border: none !important;
  box-shadow: none !important;
  background: rgba(255,255,255,0.1) !important; /* 保留浅色背景 */
}
/* 移除数字输入框边框 */
:deep(.el-input-number .el-input__wrapper) {
  border: 1px solid #1E5A86 !important;
  box-shadow: none !important;
}
/* 下拉组件 */
:deep(.el-select) {
  /* 下拉箭头 */
  .el-select__caret {
    color: #73C4FF !important; /* 匹配图中的浅蓝箭头 */
    font-size: 16px !important;
  }
}
/* 深度选择器调整边框细节 */
:deep(.el-select__wrapper) {
  border-radius: 6px;        /* 圆角大小 */
  border-width: 1.5px;        /* 边框粗细 */
  box-shadow: 0 0 0 1px #1E5A86 !important; /* 聚焦阴影 */
}
/* 移除按钮组边框(增减按钮) */
:deep(.el-input-number__decrease),
:deep(.el-input-number__increase) {
  border: 1px solid #1E5A86;
  background: transparent !important;
  i {
    color: #73C4FF;
  }
}
:deep(.el-loading-mask) {
  background: black !important;
  border-radius: 10px;
}
:deep(.el-input__inner) {
  color: #73C4FF;
}
:deep(.el-select__selected-item ) {
  color: #73C4FF !important;
}
:deep(.el-select__wrapper) {
  background: rgba(0,194,255,0.08) !important;
}
:deep(.el-input-number span) {
  background: rgba(0,194,255,0.08) !important;
}
:deep(.el-input__wrapper) {
  background: rgba(0,194,255,0.08) !important;
}
.el-dialog__footer {
  button {
    background: rgba(0,194,255,0.08) !important;
    border-color: rgba(0,194,255,0.8) !important;
  }
  button:first-child {
    color: #73C4FF;
  }
}
</style>
src/views/ai/dashboard/components/conversation/ConversationList.vue
@@ -149,7 +149,6 @@
    // 1.1 获取 对话数据
    conversationList.value = await ChatConversationApi.getChatConversationEnergyList(modelName.value)
    console.log(conversationList.value)
    if(conversationList.value.length == 0) {
      await createConversation()
    }
src/views/ai/dashboard/components/conversation/ConversationListEmpty.vue
对比新文件
@@ -0,0 +1,78 @@
<!-- 无聊天对话时,在 message 区域-->
<template>
  <div class="conversation-empty">
    <div class="center-container">
      <div class="title no-data">暂无数据</div>
      <div class="title">欢迎来到转炉煤气调度大模型</div>
    </div>
  </div>
</template>
<script setup lang="ts">
const emits = defineEmits(['onNewConversation'])
import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png'
const roleAvatar = roleAvatarDefaultImg
</script>
<style scoped lang="scss">
.conversation-empty {
  display: flex;
  flex-direction: row;
  justify-content: center;
  width: 100%;
  height: 100%;
  .box-center {
    margin-top: 35%;
    margin-left: 12%;
    display: inline-block;
    .tip {
      width: 120px;
      height: 40px;
      padding: 2px 0 0 10px;
      border-radius: 2px;
      font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
      font-weight: bold;
      font-size: 24px;
      text-align: left;
      font-style: normal;
      text-transform: none;
      background: linear-gradient(0deg, #49FFD3 0%, #4585FF 100%);
    }
  }
  .conversation-empty {
    position: relative;
    display: flex;
    flex-direction: row;
    justify-content: center;
    width: 100%;
    height: 100%;
    .center-container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      .title {
        width: 120px;
        height: 40px;
        padding: 2px 0 0 10px;
        border-radius: 2px;
        font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
        font-weight: bold;
        font-size: 24px;
        text-align: left;
        font-style: normal;
        text-transform: none;
        background: linear-gradient(0deg, #49FFD3 0%, #4585FF 100%);
      }
      .no-data {
        color: rgba(143,214,254,0.8) !important;
      }
    }
  }
}
</style>
src/views/ai/dashboard/components/conversation/HistoryConversationList.vue
对比新文件
@@ -0,0 +1,459 @@
<!--  AI 对话  -->
<template>
  <el-aside width="260px" class="conversation-container h-100%">
    <!-- 左顶部:对话 -->
    <div class="h-100%">
      <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
        <Icon icon="ep:plus" class="mr-5px" />
        新建对话
      </el-button>
      <!-- 左顶部:搜索对话 -->
      <el-input
        v-model="searchName"
        size="large"
        class="mt-10px search-input"
        placeholder="搜索历史记录"
        @keyup="searchConversation"
      >
        <template #prefix>
          <Icon icon="ep:search" />
        </template>
      </el-input>
      <!-- 左中间:对话列表 -->
      <div class="conversation-list">
        <!-- 情况一:加载中 -->
        <el-empty v-if="loading" description="." :v-loading="loading" />
        <!-- 情况二:按照 group 分组,展示聊天会话 list 列表 -->
        <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
          <div
            class="conversation-item classify-title"
            v-if="conversationMap[conversationKey].length"
          >
            <el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
          </div>
          <div
            class="conversation-item"
            v-for="conversation in conversationMap[conversationKey]"
            :key="conversation.id"
            @click="handleConversationClick(conversation.id)"
            @mouseover="hoverConversationId = conversation.id"
            @mouseout="hoverConversationId = ''"
          >
            <div
              :class="
                conversation.id === activeConversationId ? 'conversation active' : 'conversation'
              "
            >
              <div class="title-wrapper">
                <img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" />
                <span class="title">{{ conversation.title }}</span>
              </div>
              <div class="button-wrapper" v-show="hoverConversationId === conversation.id">
                <el-button class="btn" link @click.stop="handleTop(conversation)">
                  <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon>
                  <el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon>
                </el-button>
                <el-button class="btn" link @click.stop="updateConversationTitle(conversation)">
                  <el-icon title="编辑">
                    <Icon icon="ep:edit" />
                  </el-icon>
                </el-button>
                <el-button class="btn" link @click.stop="deleteChatConversation(conversation)">
                  <el-icon title="删除对话">
                    <Icon icon="ep:delete" />
                  </el-icon>
                </el-button>
              </div>
            </div>
          </div>
        </div>
        <!-- 底部占位  -->
        <div class="h-160px w-100%"></div>
      </div>
    </div>
  </el-aside>
</template>
<script setup lang="ts">
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { Bottom, Top } from '@element-plus/icons-vue'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
const message = useMessage() // 消息弹窗
// 定义属性
const searchName = ref<string>('') // 对话搜索
const modelName = ref<string>('common') // 对话搜索
const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null
const hoverConversationId = ref<number | null>(null) // 悬浮上去的对话
const conversationList = ref([] as ChatConversationVO[]) // 对话列表
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
const loading = ref<boolean>(false) // 加载中
const loadingTime = ref<any>() // 加载中定时器
// 定义组件 props
const props = defineProps({
  activeId: {
    type: String || null,
    required: true
  }
})
// 定义钩子
const emits = defineEmits([
  'onConversationCreate',
  'onConversationClick',
  'onConversationClear',
  'onConversationDelete'
])
/** 搜索对话 */
const searchConversation = async (e) => {
  // 恢复数据
  if (!searchName.value.trim().length) {
    conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
  } else {
    // 过滤
    const filterValues = conversationList.value.filter((item) => {
      return item.title.includes(searchName.value.trim())
    })
    conversationMap.value = await getConversationGroupByCreateTime(filterValues)
  }
}
/** 点击对话 */
const handleConversationClick = async (id: number) => {
  // 过滤出选中的对话
  const filterConversation = conversationList.value.filter((item) => {
    return item.id === id
  })
  // 回调 onConversationClick
  // noinspection JSVoidFunctionReturnValueUsed
  const success = emits('onConversationClick', filterConversation[0])
  // 切换对话
  if (success) {
    activeConversationId.value = id
  }
}
/** 获取对话列表 */
const getChatConversationList = async () => {
  try {
    // 加载中
    loadingTime.value = setTimeout(() => {
      loading.value = true
    }, 50)
    // 1.1 获取 对话数据
    conversationList.value = await ChatConversationApi.getChatConversationEnergyList(modelName.value)
    if(conversationList.value.length == 0) {
      await createConversation()
    }
    // 1.2 排序
    conversationList.value.sort((a, b) => {
      return b.createTime - a.createTime
    })
    // 1.3 没有任何对话情况
    if (conversationList.value.length === 0) {
      activeConversationId.value = null
      conversationMap.value = {}
      return
    }
    // 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前)
    conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
  } finally {
    // 清理定时器
    if (loadingTime.value) {
      clearTimeout(loadingTime.value)
    }
    // 加载完成
    loading.value = false
  }
}
/** 按照 creteTime 创建时间,进行分组 */
const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
  // 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
  // noinspection NonAsciiCharacters
  const groupMap = {
    置顶: [],
    今天: [],
    一天前: [],
    三天前: [],
    七天前: [],
    三十天前: []
  }
  // 当前时间的时间戳
  const now = Date.now()
  // 定义时间间隔常量(单位:毫秒)
  const oneDay = 24 * 60 * 60 * 1000
  const threeDays = 3 * oneDay
  const sevenDays = 7 * oneDay
  const thirtyDays = 30 * oneDay
  for (const conversation of list) {
    // 置顶
    if (conversation.pinned) {
      groupMap['置顶'].push(conversation)
      continue
    }
    // 计算时间差(单位:毫秒)
    const diff = now - conversation.createTime
    // 根据时间间隔判断
    if (diff < oneDay) {
      groupMap['今天'].push(conversation)
    } else if (diff < threeDays) {
      groupMap['一天前'].push(conversation)
    } else if (diff < sevenDays) {
      groupMap['三天前'].push(conversation)
    } else if (diff < thirtyDays) {
      groupMap['七天前'].push(conversation)
    } else {
      groupMap['三十天前'].push(conversation)
    }
  }
  return groupMap
}
/** 新建对话 */
const createConversation = async () => {
  // 1. 新建对话
  const conversationId = await ChatConversationApi.createChatConversationEnergy(
    {modelName: modelName.value} as unknown as ChatConversationVO
  )
  // 2. 获取对话内容
  await getChatConversationList()
  // 3. 选中对话
  await handleConversationClick(conversationId)
  // 4. 回调
  emits('onConversationCreate')
}
/** 修改对话的标题 */
const updateConversationTitle = async (conversation: ChatConversationVO) => {
  // 1. 二次确认
  const { value } = await ElMessageBox.prompt('修改标题', {
    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
    inputErrorMessage: '标题不能为空',
    inputValue: conversation.title
  })
  // 2. 发起修改
  await ChatConversationApi.updateChatConversationMy({
    id: conversation.id,
    title: value
  } as ChatConversationVO)
  message.success('重命名成功')
  // 3. 刷新列表
  await getChatConversationList()
  // 4. 过滤当前切换的
  const filterConversationList = conversationList.value.filter((item) => {
    return item.id === conversation.id
  })
  if (filterConversationList.length > 0) {
    // tip:避免切换对话
    if (activeConversationId.value === filterConversationList[0].id) {
      emits('onConversationClick', filterConversationList[0])
    }
  }
}
/** 删除聊天对话 */
const deleteChatConversation = async (conversation: ChatConversationVO) => {
  try {
    // 删除的二次确认
    await message.delConfirm(`是否确认删除对话 - ${conversation.title}?`)
    // 发起删除
    await ChatConversationApi.deleteChatConversationMy(conversation.id)
    message.success('对话已删除')
    // 刷新列表
    await getChatConversationList()
    // 回调
    emits('onConversationDelete', conversation)
  } catch {}
}
/** 清空对话 */
const handleClearConversation = async () => {
  try {
    await message.confirm('确认后对话会全部清空,置顶的对话除外。')
    await ChatConversationApi.deleteChatConversationMyByUnpinned()
    ElMessage({
      message: '操作成功!',
      type: 'success'
    })
    // 清空 对话 和 对话内容
    activeConversationId.value = null
    // 获取 对话列表
    await getChatConversationList()
    // 回调 方法
    emits('onConversationClear')
  } catch {}
}
/** 对话置顶 */
const handleTop = async (conversation: ChatConversationVO) => {
  // 更新对话置顶
  conversation.pinned = !conversation.pinned
  await ChatConversationApi.updateChatConversationMy(conversation)
  // 刷新对话
  await getChatConversationList()
}
// ============ 角色仓库 ============
/** 角色仓库抽屉 */
const roleRepositoryOpen = ref<boolean>(false) // 角色仓库是否打开
const handleRoleRepository = async () => {
  roleRepositoryOpen.value = !roleRepositoryOpen.value
}
/** 监听选中的对话 */
const { activeId } = toRefs(props)
watch(activeId, async (newValue, oldValue) => {
  activeConversationId.value = newValue as string
})
// 定义 public 方法
defineExpose({ createConversation })
/** 初始化 */
onMounted(async () => {
  // 获取 对话列表
  await getChatConversationList()
  // 默认选中
  if (props.activeId) {
    activeConversationId.value = props.activeId
  } else {
    // 首次默认选中第一个
    if (conversationList.value.length) {
      activeConversationId.value = conversationList.value[0].id
      // 回调 onConversationClick
      await emits('onConversationClick', conversationList.value[0])
    }
  }
})
</script>
<style scoped lang="scss">
.conversation-container {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 10px 10px 0;
  overflow: hidden;
  .btn-new-conversation {
    padding: 18px 0;
  }
  .search-input {
    margin-top: 20px;
  }
  .conversation-list {
    overflow: auto;
    height: 100%;
    .classify-title {
      padding-top: 10px;
    }
    .conversation-item {
      margin-top: 5px;
    }
    .conversation {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      flex: 1;
      padding: 0 5px;
      cursor: pointer;
      border-radius: 5px;
      align-items: center;
      line-height: 30px;
      &.active {
        background-color: #e6e6e6;
        .button {
          display: inline-block;
        }
      }
      .title-wrapper {
        display: flex;
        flex-direction: row;
        align-items: center;
      }
      .title {
        padding: 2px 10px;
        max-width: 220px;
        font-size: 14px;
        font-weight: 400;
        color: rgba(0, 0, 0, 0.77);
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
      .avatar {
        width: 25px;
        height: 25px;
        border-radius: 5px;
        display: flex;
        flex-direction: row;
        justify-items: center;
      }
      // 对话编辑、删除
      .button-wrapper {
        right: 2px;
        display: flex;
        flex-direction: row;
        justify-items: center;
        color: #606266;
        .btn {
          margin: 0;
        }
      }
    }
  }
  // 角色仓库、清空未设置对话
  .tool-box {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    //width: 100%;
    padding: 0 20px;
    background-color: #f4f4f4;
    box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8);
    line-height: 35px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: var(--el-text-color);
    > div {
      display: flex;
      align-items: center;
      color: #606266;
      padding: 0;
      margin: 0;
      cursor: pointer;
      > span {
        margin-left: 5px;
      }
    }
  }
}
</style>
src/views/ai/dashboard/components/message/HistoryMessageDialog.vue
对比新文件
@@ -0,0 +1,375 @@
<template>
  <DialogHistory title="历史建议" v-model="dialogVisible" width="1200">
    <!-- 左侧:对话列表 -->
    <ConversationList
      v-show="false"
      :active-id="activeConversationId"
      ref="conversationListRef"
    />
    <!-- 右侧:对话详情 -->
    <el-container class="detail-container">
      <el-header class="header">
        <div class="title">
          {{ activeConversation?.title ? activeConversation?.title : '' }}
          <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
        </div>
        <div class="btns" v-if="activeConversation">
          <el-button size="small" class="btn" @click="handlerMessageClear">
            <Icon icon="heroicons-outline:archive-box-x-mark" color="#73C4FF" />
          </el-button>
          <el-button size="small" class="btn" @click="handleGoBottomMessage">
            <Icon icon="ep:download" color="#73C4FF" />
          </el-button>
          <el-button size="small" class="btn" @click="handleGoTopMessage">
            <Icon icon="ep:top" color="#73C4FF" />
          </el-button>
        </div>
      </el-header>
      <!-- main:消息列表 -->
      <el-main class="main-container">
        <div class="message-container">
          <!-- 情况一:无聊天对话时 -->
          <ConversationListEmpty
            v-if="!activeConversation"
          />
          <!-- 情况二:消息列表为空 -->
          <MessageListEmpty
            v-if="activeMessageList.length === 0 && activeConversation"
          />
          <!-- 情况三:消息列表不为空 -->
          <HistoryMessageList
            v-if="activeMessageList.length > 0"
            ref="messageRef"
            :conversation="activeConversation"
            :list="activeMessageList"
          />
        </div>
      </el-main>
    </el-container>
  </DialogHistory>
</template>
<script setup lang="ts">
import {ChatMessageApi, ChatMessageVO} from '@/api/ai/chat/message'
import { ChatConversationVO } from '@/api/ai/chat/conversation'
import ConversationList from '../conversation/HistoryConversationList.vue'
import HistoryMessageList from './HistoryMessageList.vue'
import MessageListEmpty from './MessageListEmpty.vue'
import ConversationListEmpty from '../conversation/ConversationListEmpty.vue'
/** AI 聊天对话 列表 */
defineOptions({ name: 'HistoryMessageDialog' })
const route = useRoute() // 路由
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
// 聊天对话
const conversationListRef = ref()
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
// 消息列表
const messageRef = ref()
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表
/** 打开弹窗 */
const open = async (messages: ChatMessageVO[], conversation: ChatConversationVO) => {
  dialogVisible.value = true
  await nextTick() // 等待弹窗DOM挂载
  activeMessageList.value = messages
  activeConversation.value = conversation
}
defineExpose({ open }) // 提供方法给 parent 调用
/** 回到 message 列表的顶部 */
const handleGoTopMessage = () => {
  messageRef.value.handlerGoTop()
}
/** 回到 message 列表的底部 */
const handleGoBottomMessage = () => {
  messageRef.value.handleGoBottom()
}
/** 处理 message 清空 */
const handlerMessageClear = async () => {
  if (!activeConversation.value) {
    return
  }
  try {
    // 确认提示
    await message.delConfirm('确认清空对话消息?')
    // 清空对话
    await ChatMessageApi.deleteByConversationId(activeConversation.value.id)
    // 刷新 message 列表
    activeMessageList.value = []
  } catch {}
}
/** 初始化 **/
onMounted(async () => {
})
</script>
<style lang="scss" scoped>
// 头部
.detail-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 820px;
  background-color: rgba(0, 0, 0, 0); /* 透明背景 */
  z-index: 1;
  .header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    box-shadow: 0 0 0 0 #dcdfe6;
    .title {
      font-size: 18px;
      font-weight: bold;
      color: gold;
    }
    .btns {
      display: flex;
      width: 300px;
      flex-direction: row;
      justify-content: flex-end;
      .btn {
        padding: 10px;
      }
      /* 所有状态通用透明背景 */
      :deep(.el-button) {
        background: transparent !important;
        border-color: currentColor; /* 保持与文字同色 */
        color: #409EFF; /* 蓝色文字 */
      }
      /* 悬停状态 */
      :deep(.el-button:hover) {
        background: rgba(0, 0, 0, 0.05) !important; /* 轻微悬停反馈 */
      }
      /* 点击状态 */
      :deep(.el-button:active) {
        background: rgba(0, 0, 0, 0.1) !important;
      }
      /* 禁用状态 */
      :deep(.el-button.is-disabled) {
        opacity: 0.6;
        background: transparent !important;
      }
    }
  }
}
// main 容器
.main-container {
  padding: 0;
  position: relative;
  overflow: hidden;          /* 隐藏外层滚动条 */
  width: 100%;
  .message-container {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    overflow-y: hidden;
    /* Firefox */
    scrollbar-width: thin;
    scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
    /* WebKit */
    &::-webkit-scrollbar {
      width: 6px;
      background: transparent;
    }
    &::-webkit-scrollbar-thumb {
      border-radius: 4px;
      background: rgba(0, 0, 0, 0.15);
      transition: background 0.3s;
      &:hover { background: rgba(0, 0, 0, 0.25); }
    }
  }
}
// 底部
.footer-container {
  display: flex;
  flex-direction: column;
  height: 114px;
  margin-left: 10px;
  padding: 0;
  // 输入框
  .input-container {
    display: flex;
    flex-direction: column;
    height: auto;
    width: 876px;
    margin: 0;
    padding: 0;
    overflow-y: auto;        /* 垂直方向溢出时显示滚动条 */
    overflow-x: hidden;      /* 水平方向隐藏滚动条 */
    /* Firefox */
    scrollbar-width: thin;
    scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
    /* WebKit */
    &::-webkit-scrollbar {
      width: 6px;
      background: transparent;
    }
    &::-webkit-scrollbar-thumb {
      border-radius: 4px;
      background: rgba(0, 0, 0, 0.15);
      transition: background 0.3s;
      &:hover { background: rgba(0, 0, 0, 0.25); }
    }
    .prompt-from {
      display: flex;
      flex-direction: column;
      padding: 9px 10px;
      width: 876px;
      height: 114px;
      background: rgba(115,196,255,0.05);
      border-radius: 4px 4px 4px 4px;
      border: 1px solid #73C4FF;
    }
    .prompt-input {
      width: 876px;
      height: 113.55px;
      font-weight: 400;
      font-size: 14px;
      background-color: rgba(219,238,255,0);
      line-height: 21px;
      text-align: left;
      font-style: normal;
      text-transform: none;
      border: 0;
      color: rgba(219,238,255,0.6);
    }
    .prompt-input:focus {
      outline: none;
    }
    .prompt-btns {
      display: flex;
      justify-content: space-between;
      padding-bottom: 0;
      padding-top: 5px;
      .content {
        /* 默认状态 */
        .el-button {
          background: transparent !important;
          border-color: rgba(115, 196, 255, 0.5);
          color: #73C4FF;
          border-radius: 15px !important;
        }
        /* 上下文图标处理 */
        .content-icon {
          color: blue;  /* 图标颜色 */
          font-size: 18px;
          margin-right: 10px;
          background: url("@/assets/ai/zhuanlu/content.png");
          vertical-align: middle;
        }
        /* 选中状态 */
        .active-button {
          background: #409eff !important;
          border-color: #409eff !important;
          color: white !important;
          .content-icon {
            background: url("@/assets/ai/zhuanlu/content_select.png");
            vertical-align: middle;
          }
        }
        /* 按钮组间距处理 */
        .button-group .el-button {
          margin-left: 0;
          border-radius: 4px;
        }
        /* 悬停效果 */
        .el-button:not(.active-button):hover {
          border-color: rgba(115,196,255,0.5);
          color: #409eff;
        }
      }
      .message {
        /* 所有状态通用透明背景 */
        :deep(.el-button) {
          background: rgba(73, 254, 210, 0.8) !important;
          border-color: currentColor; /* 保持与文字同色 */
          font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
          font-weight: bold;
          font-size: 16px;
          color: #123C4E;
          clip-path: polygon(
              0 0,
              100% 0,
              100% 100%,
              10px 100%,          /* 右下方向留出10px */
              0 calc(100% - 10px) /* 左上方向留出10px */
          );
          position: relative;
          padding-left: 15px; /* 增加右侧留白 */
        }
        /* 悬停状态 */
        :deep(.el-button:hover) {
          background: rgba(73, 254, 210, 0.6) !important; /* 轻微悬停反馈 */
        }
        /* 点击状态 */
        :deep(.el-button:active) {
          background: rgba(73, 254, 210, 1) !important;
        }
        /* 禁用状态 */
        :deep(.el-button.is-disabled) {
          opacity: 0.6;
          background: transparent !important;
        }
        /* 核心样式覆盖 */
        :deep(.el-switch__core) {
          background: transparent !important;
          border-radius: 0 0 15px 0 !important;
          border: none !important;
          height: 40px !important;
        }
        /* 按钮内容容器 */
        .button-content {
          display: flex;
          align-items: center;
          padding: 0 15px;
          height: 100%;
        }
      }
    }
  }
}
</style>
src/views/ai/dashboard/components/message/HistoryMessageList.vue
对比新文件
@@ -0,0 +1,290 @@
<template>
  <div ref="messageContainer" class="h-100% overflow-y-auto relative">
    <div class="chat-list" v-for="(item, index) in list" :key="index">
      <!-- 靠左 message:system、assistant 类型 -->
      <div class="left-message message-item" v-if="item.type !== 'user'">
        <div class="avatar">
          <el-avatar :src="roleAvatar" />
        </div>
        <div class="message">
          <div>
            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
          </div>
          <div v-if="item.thinkingFlag" class="left-text-container-thinking" ref="markdownViewRef">
            <MarkdownView v-if="item.thinking" class="left-text thinking" :content="item.thinking" />
            <MarkdownView v-else class="left-text thinking" :content="item.content" />
          </div>
          <div v-else-if="item.thinking" class="left-text-container-thinking" ref="markdownViewRef">
            <MarkdownView class="left-text thinking" :content="item.thinking" />
          </div>
          <div v-else class="left-text-container-conclusion" ref="markdownViewRef">
            <MarkdownView class="left-text" :content="item.content" />
          </div>
          <div class="left-text-container-conclusion" ref="markdownViewRef">
            <MarkdownView class="left-text" :content="item.conclusion" />
          </div>
          <div class="right-btns">
            <el-button class="btn-cus" link @click="copyContent(item.content)">
              <img class="btn-image" src="@/assets/ai/zhuanlu/copy.png" />
            </el-button>
            <!-- 暂时不能删除,随意删除会影响首页echarts图表展示 -->
<!--            <el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">-->
<!--              <img class="btn-image h-17px" src="@/assets/ai/zhuanlu/delete.png" />-->
<!--            </el-button>-->
          </div>
        </div>
      </div>
      <!-- 靠右 message:user 类型 -->
      <div class="left-message message-item" v-if="item.type === 'user'">
        <div class="avatar">
          <el-avatar :src="userAvatar" />
        </div>
        <div class="message">
          <div>
            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
          </div>
          <div class="right-text-container">
            <div class="right-text">{{ item.content }}</div>
          </div>
          <div class="right-btns">
            <el-button class="btn-cus" link @click="copyContent(item.content)">
              <img class="btn-image" src="@/assets/ai/zhuanlu/copy.png" />
            </el-button>
            <el-button class="btn-cus" link @click="onDelete(item.id)">
              <img class="btn-image h-17px mr-12px" src="@/assets/ai/zhuanlu/delete.png" />
            </el-button>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- 回到底部 -->
  <div v-if="isScrolling" class="to-bottom" @click="handleGoBottom">
    <el-button :icon="ArrowDownBold" circle />
  </div>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { formatDate } from '@/utils/formatTime'
import MarkdownView from '@/components/MarkdownView/index.vue'
import { useClipboard } from '@vueuse/core'
import { ArrowDownBold } from '@element-plus/icons-vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationVO } from '@/api/ai/chat/conversation'
import { useUserStore } from '@/store/modules/user'
import userAvatarDefaultImg from '@/assets/ai/zhuanlu/user.png'
import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png'
const dialogVisible = ref(false) // 弹窗的是否展示
const message = useMessage() // 消息弹窗
const { copy } = useClipboard() // 初始化 copy 到粘贴板
const userStore = useUserStore()
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
const messageContainer: any = ref(null)
const isScrolling = ref(false) //用于判断用户是否在滚动
const userAvatar = computed(() => userAvatarDefaultImg)
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
// 定义 props
const props = defineProps({
  conversation: {
    type: Object as PropType<ChatConversationVO>,
    required: true
  },
  list: {
    type: Array as PropType<ChatMessageVO[]>,
    required: true
  }
})
const { list } = toRefs(props) // 消息列表
const emits = defineEmits(['onDeleteSuccess']) // 定义 emits
// ============ 处理对话滚动 ==============
/** 滚动到底部 */
const scrollToBottom = async (isIgnore?: boolean) => {
  // 注意要使用 nextTick 以免获取不到 dom
  await nextTick()
  if (isIgnore || !isScrolling.value) {
    messageContainer.value.scrollTop =
      messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
  }
}
function handleScroll() {
  const scrollContainer = messageContainer.value
  const scrollTop = scrollContainer.scrollTop
  const scrollHeight = scrollContainer.scrollHeight
  const offsetHeight = scrollContainer.offsetHeight
  if (scrollTop + offsetHeight < scrollHeight - 100) {
    // 用户开始滚动并在最底部之上,取消保持在最底部的效果
    isScrolling.value = true
  } else {
    // 用户停止滚动并滚动到最底部,开启保持到最底部的效果
    isScrolling.value = false
  }
}
/** 回到底部 */
const handleGoBottom = async () => {
  const scrollContainer = messageContainer.value
  scrollContainer.scrollTop = scrollContainer.scrollHeight
}
/** 回到顶部 */
const handlerGoTop = async () => {
  const scrollContainer = messageContainer.value
  scrollContainer.scrollTop = 0
}
defineExpose({ scrollToBottom, handlerGoTop, handleGoBottom }) // 提供方法给 parent 调用
// ============ 处理消息操作 ==============
/** 复制 */
const copyContent = async (content) => {
  await copy(content)
  message.success('复制成功!')
}
/** 删除 */
const onDelete = async (id) => {
  // 删除 message
  await ChatMessageApi.deleteChatMessage(id)
  message.success('删除成功!')
  // 回调
  emits('onDeleteSuccess')
}
/** 初始化 */
onMounted(async () => {
  messageContainer.value.addEventListener('scroll', handleScroll)
})
</script>
<style scoped lang="scss">
/* 添加或修改以下样式 */
div[ref="messageContainer"] {
  height: 100%;          /* 继承父容器高度 */
  overflow-y: auto;      /* 启用垂直滚动 */
  max-height: 720px;     /* 或根据实际需求调整 */
  padding-bottom: 20px; /* 避免底部内容被截断 */
}
// 中间
.chat-list {
  display: flex;
  flex-direction: column;
  height: auto;
  padding: 0 20px;
  .left-message {
    display: flex;
    flex-direction: row;
  }
  .message {
    display: flex;
    flex-direction: column;
    text-align: left;
    margin: 0 15px;
    .time {
      text-align: left;
      line-height: 30px;
    }
    .left-text-container-thinking {
      position: relative;
      display: flex;
      flex-direction: column;
      overflow-wrap: break-word;
      background: rgba(115,196,255,0.05);
      border-radius: 4px 4px 4px 4px;
      border-left: 1px solid #73C4FF;
      padding: 10px 10px 5px 10px;
      .left-text {
        color: rgba(219,238,255,0.5);
        font-size: 0.85rem;
      }
    }
    .left-text-container-conclusion {
      position: relative;
      display: flex;
      flex-direction: column;
      overflow-wrap: break-word;
      background: rgba(115,196,255,0);
      border-radius: 4px 4px 4px 4px;
      padding: 0 10px 5px 0;
      .left-text {
        color: rgba(219,238,255,0.8);
        font-size: 1rem;
      }
    }
    .right-text-container {
      display: flex;
      flex-direction: row-reverse;
      .right-text {
        font-size: 0.95rem;
        color: #DBEEFF;
        display: inline;
        background: rgba(40,139,255,0.1);
        box-shadow: 0 0 0 0 rgba(40,139,255,0.3);
        border-radius: 10px;
        padding: 10px;
        width: auto;
        overflow-wrap: break-word;
        white-space: pre-wrap;
      }
    }
    .left-btns {
      display: flex;
      flex-direction: row;
      margin-top: 8px;
    }
    .right-btns {
      display: flex;
      flex-direction: row-reverse;
      margin-top: 8px;
    }
  }
  // 复制、删除按钮
  .btn-cus {
    display: flex;
    background-color: transparent;
    align-items: center;
    .btn-image {
      height: 20px;
    }
  }
  .btn-cus:hover {
    cursor: pointer;
    background-color: #f6f6f6;
  }
}
// 回到底部
.to-bottom {
  position: absolute;
  z-index: 1000;
  bottom: 0;
  right: 50%;
  .el-button {
    background: rgba(255,215,0,0.2);
    border: solid 1px rgba(255,215,0,0.8);
    color: rgba(255,215,0,0.8);
  }
}
</style>
src/views/ai/dashboard/components/message/MessageList.vue
@@ -3,9 +3,58 @@
    <div class="chat-list" v-for="(item, index) in list" :key="index">
      <!-- 靠左 message:system、assistant 类型 -->
      <div class="left-message message-item" v-if="item.type !== 'user'">
        <div class="avatar">
          <el-avatar :src="roleAvatar" />
        </div>
        <div class="message">
          <div class="left-text-container" ref="markdownViewRef">
            <MarkdownView class="left-text" :content="item.content" />
          <div>
            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
          </div>
          <div v-if="item.thinkingFlag" class="left-text-container-thinking" ref="markdownViewRef">
            <MarkdownView v-if="item.thinking" class="left-text thinking" :content="item.thinking" />
            <MarkdownView v-else class="left-text thinking" :content="item.content" />
          </div>
          <div v-else-if="item.thinking" class="left-text-container-thinking" ref="markdownViewRef">
            <MarkdownView class="left-text thinking" :content="item.thinking" />
          </div>
          <div class="left-text-container-conclusion" ref="markdownViewRef">
            <MarkdownView class="left-text" :content="item.conclusion" />
          </div>
          <div class="left-btns">
            <el-button class="btn-cus" link @click="copyContent(item.content)">
              <img class="btn-image" src="@/assets/ai/zhuanlu/copy.png" />
            </el-button>
            <el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">
              <img class="btn-image h-17px" src="@/assets/ai/zhuanlu/delete.png" />
            </el-button>
          </div>
        </div>
      </div>
      <!-- 靠右 message:user 类型 -->
      <div class="right-message message-item" v-if="item.type === 'user'">
        <div class="avatar">
          <el-avatar :src="userAvatar" />
        </div>
        <div class="message">
          <div>
            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
          </div>
          <div class="right-text-container">
            <div class="right-text">{{ item.content }}</div>
          </div>
          <div class="right-btns">
            <el-button class="btn-cus" link @click="copyContent(item.content)">
              <img class="btn-image" src="@/assets/ai/zhuanlu/copy.png" />
            </el-button>
            <el-button class="btn-cus" link @click="onDelete(item.id)">
              <img class="btn-image h-17px mr-12px" src="@/assets/ai/zhuanlu/delete.png" />
            </el-button>
            <el-button class="btn-cus" link @click="onRefresh(item)">
              <img class="btn-image h-17px mr-12px" src="@/assets/ai/zhuanlu/refresh.png" />
            </el-button>
            <el-button class="btn-cus" link @click="onEdit(item)">
              <img class="btn-image h-17px mr-12px" src="@/assets/ai/zhuanlu/edit.png" />
            </el-button>
          </div>
        </div>
      </div>
@@ -18,12 +67,15 @@
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { formatDate } from '@/utils/formatTime'
import MarkdownView from '@/components/MarkdownView/index.vue'
import { useClipboard } from '@vueuse/core'
import { ArrowDownBold} from '@element-plus/icons-vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationVO } from '@/api/ai/chat/conversation'
import { useUserStore } from '@/store/modules/user'
import userAvatarDefaultImg from '@/assets/ai/zhuanlu/user.png'
import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png'
const message = useMessage() // 消息弹窗
const { copy } = useClipboard() // 初始化 copy 到粘贴板
@@ -32,6 +84,10 @@
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
const messageContainer: any = ref(null)
const isScrolling = ref(false) //用于判断用户是否在滚动
const userAvatar = computed(() => userAvatarDefaultImg)
// const userAvatar = computed(() => userStore.user.avatar || userAvatarDefaultImg)
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
// 定义 props
const props = defineProps({
@@ -78,16 +134,18 @@
/** 回到底部 */
const handleGoBottom = async () => {
  const scrollContainer = messageContainer.value
  console.log(scrollContainer.scrollHeight)
  scrollContainer.scrollTop = scrollContainer.scrollHeight
}
/** 回到顶部 */
const handlerGoTop = async () => {
  const scrollContainer = messageContainer.value
  console.log(scrollContainer.scrollHeight)
  scrollContainer.scrollTop = 0
}
defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用
defineExpose({ scrollToBottom, handlerGoTop, handleGoBottom }) // 提供方法给 parent 调用
// ============ 处理消息操作 ==============
@@ -100,7 +158,7 @@
/** 删除 */
const onDelete = async (id) => {
  // 删除 message
  await ChatMessageApi.deleteEnergyChatMessage(id)
  await ChatMessageApi.deleteChatMessage(id)
  message.success('删除成功!')
  // 回调
  emits('onDeleteSuccess')
@@ -143,15 +201,24 @@
    flex-direction: row;
  }
  .right-message {
    display: flex;
    flex-direction: row-reverse;
    justify-content: flex-start;
  }
  .message {
    display: flex;
    flex-direction: column;
    text-align: left;
    height: 462px;
    margin: 0 15px;
    .left-text-container {
      width: 855px;
      height: 462px;
    .time {
      text-align: left;
      line-height: 30px;
    }
    .left-text-container-thinking {
      position: relative;
      display: flex;
      flex-direction: column;
@@ -160,14 +227,55 @@
      border-radius: 4px 4px 4px 4px;
      border-left: 1px solid #73C4FF;
      padding: 10px 10px 5px 10px;
      .left-text {
        font-weight: 400;
        font-size: 14px;
        color: rgba(219,238,255,0.6);
        color: rgba(219,238,255,0.5);
        font-size: 0.85rem;
      }
    }
    .left-text-container-conclusion {
      position: relative;
      display: flex;
      flex-direction: column;
      overflow-wrap: break-word;
      background: rgba(115,196,255,0);
      border-radius: 4px 4px 4px 4px;
      padding: 20px 10px 5px 0;
      .left-text {
        color: rgba(219,238,255,0.8);
        font-size: 1rem;
      }
    }
    .right-text-container {
      display: flex;
      flex-direction: row-reverse;
      .right-text {
        font-size: 0.95rem;
        color: #DBEEFF;
        display: inline;
        background: rgba(40,139,255,0.1);
        box-shadow: 0 0 0 1px rgba(40,139,255,0.3);
        border-radius: 10px;
        padding: 10px;
        width: auto;
        overflow-wrap: break-word;
        white-space: pre-wrap;
      }
    }
    .left-btns {
      display: flex;
      flex-direction: row;
      margin-top: 8px;
    }
    .right-btns {
      display: flex;
      flex-direction: row-reverse;
      margin-top: 8px;
    }
  }
  // 复制、删除按钮
src/views/ai/dashboard/components/message/MessageListEmpty.vue
@@ -3,26 +3,15 @@
  <div class="chat-empty">
    <!-- title -->
    <div class="center-container">
      <div class="title">工业大模型 AI</div>
      <div class="avatar"><img src="@/assets/ai/zhuanlu/assistant.png" /></div>
      <div class="gradient-text">欢迎来到转炉煤气调度大模型</div>
    </div>
  </div>
</template>
<script setup lang="ts">
const promptList = [
  {
    prompt: '今天气怎么样?'
  },
  {
    prompt: '写一首好听的诗歌?'
  }
] // prompt 列表
const emits = defineEmits(['onPrompt'])
/** 选中 prompt 点击 */
const handlerPromptClick = async ({ prompt }) => {
  emits('onPrompt', prompt)
}
</script>
<style scoped lang="scss">
.chat-empty {
@@ -32,42 +21,29 @@
  justify-content: center;
  width: 100%;
  height: 100%;
  .center-container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-top: 30%;
    display: inline-block;
    .title {
      font-size: 28px;
      font-weight: bold;
      text-align: center;
      color: #8FD6FE;
    div {
      float: left;
    }
    .role-list {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      align-items: center;
      justify-content: center;
      width: 460px;
      margin-top: 20px;
      .role-item {
        display: flex;
        justify-content: center;
        width: 180px;
        line-height: 50px;
        border: 1px solid #e4e4e4;
        border-radius: 10px;
        margin: 10px;
        cursor: pointer;
      }
      .role-item:hover {
        background-color: rgba(243, 243, 243, 0.73);
      }
    .avatar {
      background: transparent;
      width: 50px;
      height: 50px;
      position: relative;
    }
    /* 渐变文字样式 */
    .gradient-text {
      font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
      font-weight: bold;
      font-size: 24px;
      background: linear-gradient(0deg, #49FFD3 0%, #4585FF 100%);
      -webkit-background-clip: text;
      color: transparent;
      margin: 4px 0 0 10px;
    }
  }
}
src/views/ai/dashboard/components/message/ModelMessageList.vue
对比新文件
@@ -0,0 +1,193 @@
<template>
  <div ref="messageContainer" class="h-100%">
    <div class="chat-list" v-for="(item, index) in list" :key="index">
      <!-- 靠左 message:system、assistant 类型 -->
      <div class="left-message message-item" v-if="item.type !== 'user'">
        <div class="message">
          <div class="left-text-container" ref="markdownViewRef">
            <MarkdownView class="left-text" :content="item.thinking" />
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- 回到底部 -->
  <div v-if="isScrolling" class="to-bottom" @click="handleGoBottom">
    <el-button :icon="ArrowDownBold" circle />
  </div>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import MarkdownView from '@/components/MarkdownView/index.vue'
import { useClipboard } from '@vueuse/core'
import { ArrowDownBold} from '@element-plus/icons-vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationVO } from '@/api/ai/chat/conversation'
import { useUserStore } from '@/store/modules/user'
import {formatDate} from "@/utils/formatTime";
const message = useMessage() // 消息弹窗
const { copy } = useClipboard() // 初始化 copy 到粘贴板
const userStore = useUserStore()
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
const messageContainer: any = ref(null)
const isScrolling = ref(false) //用于判断用户是否在滚动
// 定义 props
const props = defineProps({
  conversation: {
    type: Object as PropType<ChatConversationVO>,
    required: true
  },
  list: {
    type: Array as PropType<ChatMessageVO[]>,
    required: true
  }
})
const { list } = toRefs(props) // 消息列表
const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 定义 emits
// ============ 处理对话滚动 ==============
/** 滚动到底部 */
const scrollToBottom = async (isIgnore?: boolean) => {
  // 注意要使用 nextTick 以免获取不到 dom
  await nextTick()
  if (isIgnore || !isScrolling.value) {
    messageContainer.value.scrollTop =
      messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
  }
}
function handleScroll() {
  const scrollContainer = messageContainer.value
  const scrollTop = scrollContainer.scrollTop
  const scrollHeight = scrollContainer.scrollHeight
  const offsetHeight = scrollContainer.offsetHeight
  if (scrollTop + offsetHeight < scrollHeight - 100) {
    // 用户开始滚动并在最底部之上,取消保持在最底部的效果
    isScrolling.value = true
  } else {
    // 用户停止滚动并滚动到最底部,开启保持到最底部的效果
    isScrolling.value = false
  }
}
/** 回到底部 */
const handleGoBottom = async () => {
  const scrollContainer = messageContainer.value
  scrollContainer.scrollTop = scrollContainer.scrollHeight
}
/** 回到顶部 */
const handlerGoTop = async () => {
  const scrollContainer = messageContainer.value
  scrollContainer.scrollTop = 0
}
defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用
// ============ 处理消息操作 ==============
/** 复制 */
const copyContent = async (content) => {
  await copy(content)
  message.success('复制成功!')
}
/** 删除 */
const onDelete = async (id) => {
  // 删除 message
  await ChatMessageApi.deleteChatMessage(id)
  message.success('删除成功!')
  // 回调
  emits('onDeleteSuccess')
}
/** 刷新 */
const onRefresh = async (message: ChatMessageVO) => {
  emits('onRefresh', message)
}
/** 编辑 */
const onEdit = async (message: ChatMessageVO) => {
  emits('onEdit', message)
}
/** 初始化 */
onMounted(async () => {
  messageContainer.value.addEventListener('scroll', handleScroll)
})
</script>
<style scoped lang="scss">
.message-container {
  position: relative;
  overflow-y: scroll;
}
// 中间
.chat-list {
  display: flex;
  flex-direction: column;
  overflow-y: hidden;
  padding: 0 20px;
  .message-item {
    margin-top: 30px;
  }
  .message {
    display: flex;
    flex-direction: column;
    text-align: left;
    height: 480px;
    .left-text-container {
      width: 855px;
      height: 450px;
      position: relative;
      display: flex;
      flex-direction: column;
      overflow-wrap: break-word;
      background: rgba(115,196,255,0.05);
      border-radius: 4px 4px 4px 4px;
      border-left: 1px solid #73C4FF;
      padding: 10px 10px 5px 10px;
      .left-text {
        font-weight: 400;
        font-size: 14px;
        color: rgba(219,238,255,0.6);
      }
    }
  }
  // 复制、删除按钮
  .btn-cus {
    display: flex;
    background-color: transparent;
    align-items: center;
    .btn-image {
      height: 20px;
    }
  }
  .btn-cus:hover {
    cursor: pointer;
    background-color: #f6f6f6;
  }
}
// 回到底部
.to-bottom {
  position: absolute;
  z-index: 1000;
  bottom: 0;
  right: 50%;
}
</style>
src/views/ai/dashboard/zhuanlu/Index.vue
文件已删除
src/views/ai/dashboard/zhuanlu/index.vue
对比新文件
@@ -0,0 +1,2347 @@
<template>
  <div class="gas-scheduling-container">
    <el-button size="small" class="fullscreen-btn" @click="toggleFullscreen">
      <Icon
        class="is-hover mr-12px cursor-pointer"
        :icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
        color="#8FD6FE"
        hover-color="var(--el-color-primary)"
      />
      {{ isFullscreen ? '退出全屏' : '全屏' }}
    </el-button>
    <div class="gas-scheduling-left">
      <div id="mqhsssxx">
        <div class="title"></div>
        <div class="data1-item" v-for="(item, index) in mqhsList" :key="`dynamics-${index}`">
          <div class="content">
            <div class="value">
              <span>{{item.value}}</span> <span>{{item.unit}}</span>
            </div>
            <div class="name">
              {{item.name}}
            </div>
          </div>
        </div>
      </div>
      <div id="tsxx">
        <div class="title"></div>
        <div class="data1-item" v-for="(item, index) in tsxxList" :key="`dynamics-${index}`">
          <div class="content">
            <div class="value">
              <span>{{item.value}}</span> <span>{{item.unit}}</span>
            </div>
            <div class="name">
              {{item.name}}
            </div>
          </div>
        </div>
      </div>
      <div id="zlxx">
        <div class="title"></div>
        <el-table :data="zlxxList" class="transparent-table">
          <el-table-column prop="name" label="控制器名称" header-class-name="custom-header" width="150"/>
          <el-table-column prop="zl1" label="1#转炉" header-class-name="custom-header" />
          <el-table-column prop="zl2" label="2#转炉" header-class-name="custom-header" />
          <el-table-column prop="zl3" label="3#转炉" header-class-name="custom-header" />
        </el-table>
      </div>
      <div id="mqxhssxx">
        <div class="title"></div>
        <div class="data2-item" v-for="(item, index) in mqxhssxxList" :key="`dynamics-${index}`">
          <div class="content2">
            <div class="name">
              {{item.name}}
            </div>
            <div class="value">
              <span>{{item.value}}</span> <span>{{item.unit}}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="gas-scheduling-center">
      <div class="mode-switch">
        <el-radio-group v-model="tabPosition" class="custom-radio-group">
          <el-radio-button label="model">大模型模式</el-radio-button>
          <el-radio-button label="conversation">对话模式</el-radio-button>
        </el-radio-group>
      </div>
      <div v-if="tabPosition === 'model'">
        <!-- 对话列表 -->
        <ConversationList
          v-show="false"
          :active-id="activeConversationId"
          ref="conversationListRef"
          @on-conversation-click="handleConversationClick"
          @on-conversation-clear="handleConversationClear"
          @on-conversation-delete="handlerConversationDelete"
        />
        <div class="detail-container">
          <!-- 输入框 -->
          <div class="input-container">
            <form class="prompt-from">
              <textarea
                class="prompt-input"
                v-model="prompt"
                @keydown="handleSendByKeydown"
                @input="handlePromptInput"
                @compositionstart="onCompositionstart"
                @compositionend="onCompositionend"
                placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
              ></textarea>
              <div class="prompt-btns">
                <div class="content">
                  <el-button
                    :class="{ 'active-button': enableContext }"
                    @click="enableContext = !enableContext"
                  >
                    <el-icon class="content-icon" />
                    上下文
                  </el-button>
                </div>
                <div class="message">
                  <el-button
                    type="primary"
                    size="default"
                    @click="handleSendByButton"
                    :loading="conversationInProgress"
                    v-if="conversationInProgress == false"
                  >
                    {{ conversationInProgress ? '进行中' : '发消息' }}
                  </el-button>
                  <el-button
                    type="danger"
                    size="default"
                    @click="stopStream()"
                    v-if="conversationInProgress == true"
                  >
                    停止
                  </el-button>
                </div>
              </div>
            </form>
          </div>
          <!-- main:消息列表 -->
          <el-main class="main-container">
            <div class="title">
              <span>工业能源大模型思考</span>
            </div>
            <div>
              <div class="message-container">
                <!-- 情况一:消息加载中 -->
                <MessageLoading v-if="activeMessageListLoading" />
                <!-- 情况二:消息列表为空 -->
                <MessageListEmpty
                  v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation"
                  @on-prompt="doSendMessage"
                />
                <!-- 情况三:消息列表不为空 -->
                <ModelMessageList
                  v-if="!activeMessageListLoading && messageList.length > 0"
                  ref="messageRef"
                  :conversation="activeConversation"
                  :list="messageList"
                  @on-delete-success="handleMessageDelete"
                  @on-edit="handleMessageEdit"
                  @on-refresh="handleMessageRefresh"
                />
              </div>
            </div>
          </el-main>
          <!-- main:调度推理结论 -->
          <div class="result-container-title">
            <span>调度推理结论</span><el-button @click="openHistoryMessage" size="small" class="history-button" :icon="ArrowUpBold">历史建议</el-button>
          </div>
          <el-main class="result-container">
            <div class="result">
              <textarea class="result-content" v-model="ddtlResult"></textarea>
            </div>
          </el-main>
        </div>
        <!-- 历史建议 -->
        <HistoryMessageDialog
          ref="historyMessageRef"
          :conversation="activeConversation"
        />
      </div>
      <div v-else>
        <NormalConversation />
      </div>
    </div>
    <div class="gas-scheduling-right">
      <div id="ldghslyc">
        <div class="title"></div>
        <div ref="LDGHSLYCEhartContainer" style="width: 100%; height: 180px"></div>
      </div>
      <div id="ldggrqsyc">
        <div class="title"></div>
        <div ref="LDGGRYCEhartContainer" style="width: 100%; height: 180px"></div>
      </div>
      <div id="mqhsjhxx">
        <div class="title"></div>
        <div class="time-content" v-for="(item, index) in mqhsjhTimeList" :key="`dynamics-${index}`">
          <div class="time-content-item">
            <div class="name">
              <span>{{item.name}}</span>
            </div>
            <div class="time">
              <div class="in-pot"></div><div class="in">装入{{item.inTime}}</div>
              <div class="out-pot"></div><div class="out">结束{{item.outTime}}</div>
            </div>
          </div>
        </div>
        <div class="data2-item" v-for="(item, index) in mqhsjhxxList" :key="`dynamics-${index}`">
          <div class="content">
            <div class="name">
              {{item.name}}
            </div>
            <div class="value">
              <span>{{item.value}}</span> <span>{{item.unit}}</span>
            </div>
          </div>
        </div>
      </div>
      <div id="scmbyyxzb">
        <div class="title"></div>
        <div class="little-title">生产目标/班</div>
        <div class="data3-item" v-for="(item, index) in scmbList" :key="`dynamics-${index}`">
          <div class="content2">
            <div class="name">
              {{item.name}}
            </div>
            <el-progress
              :percentage="percentage(item)"
              :stroke-width="12"
              :text-inside="true"
              :color="customColor"
              :show-text="false"
            />
            <div class="value">
              <span>{{item.current}}/{{item.total}}</span>
            </div>
            <div class="value-content">
              <span>已吹炼/总炉数</span>
            </div>
          </div>
        </div>
        <div class="little-title">运行指标/天</div>
        <div class="zb-content">
          <div class="item left-label"></div>
          <div class="item data4-item" v-for="(item, index) in yxzbList" :key="`dynamics-${index}`">
            <div class="content">
              <div class="value">
                <span>{{item.value}}</span> <span>{{item.unit}}</span>
              </div>
              <div class="name">
                {{item.name}}
              </div>
            </div>
          </div>
          <div class="item right-label"></div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import {ChatConversationApi, ChatConversationVO} from "@/api/ai/chat/conversation";
import {ChatMessageApi, ChatMessageVO} from "@/api/ai/chat/message";
import ModelMessageList from '../components/message/ModelMessageList.vue'
import NormalConversation from '../components/conversation/CommonConversation.vue'
import MessageListEmpty from '../components/message/MessageListEmpty.vue'
import MessageLoading from '../components/message/MessageLoading.vue'
import ConversationList from "../components/conversation/ConversationList.vue";
import HistoryMessageDialog from "../components/message/HistoryMessageDialog.vue"
import * as echarts from "echarts";
import {formatToDateTime} from "@/utils/dateUtil";
import {refreshToken} from "@/api/login";
import {round} from "lodash-es";
import {ArrowUpBold} from "@element-plus/icons-vue";
import * as authUtil from "@/utils/auth";
const mqhsList = ref([
  {
    name: '单转炉煤气回收流量',
    value: 130,
    unit: 'km³/h'
  },
  {
    name: '转炉煤气 O 含量',
    value: 618,
    unit: '%'
  },
  {
    name: '转炉煤气 CO 含量',
    value: 15,
    unit: '%'
  },
  {
    name: '转炉铁水碳含量',
    value: 20,
    unit: '%'
  },
  {
    name: '三通阀信号',
    value: 0,
    unit: ''
  },
  {
    name: '单转炉吹氧流量',
    value: 400,
    unit: 'kNm³/h'
  }
])
const tsxxList = ref([
  {
    name: '各高炉出铁水信号',
    value: '进行',
    unit: ''
  },
  {
    name: '各高炉出铁量',
    value: 5000,
    unit: '吨'
  },
  {
    name: '各高炉铁水装入鱼雷罐车信号',
    value: '进行',
    unit: 'm³/h'
  },
  {
    name: '鱼雷罐车等待信号',
    value: '进行',
    unit: 'm³/h'
  },
  {
    name: '铁水倒入铁水包信号',
    value: '不进行',
    unit: 'm³/h'
  },
  {
    name: '铁产量计划',
    value: 6000,
    unit: '吨'
  },
])
const zlxxList = ref([
  {
    name: '吹炼状态',
    zl1: '正在吹炼',
    zl2: '正在吹炼',
    zl3: '正在吹炼'
  },
  {
    name: '当前状态持续时间',
    zl1: '10min',
    zl2: '10min',
    zl3: '10min'
  },
  {
    name: '当前炉吹炼开始时刻',
    zl1: '18:40',
    zl2: '18:40',
    zl3: '18:40'
  },
  {
    name: '当前炉吹炼结束时刻',
    zl1: '18:40',
    zl2: '18:40',
    zl3: '18:40'
  },
  {
    name: '前一炉吹炼开始时刻',
    zl1: '18:40',
    zl2: '18:40',
    zl3: '18:40'
  },
  {
    name: '前一炉吹炼结束时刻',
    zl1: '18:40',
    zl2: '18:40',
    zl3: '18:40'
  }
])
const mqxhssxxList = ref([
  {
    name: '去棒三混合站',
    value: 57.1,
    unit: 'km³/h'
  },
  {
    name: '东区掺混 LDG',
    value: 49.4,
    unit: 'km³/h'
  },
  {
    name: '去焦化方向',
    value: 67.4,
    unit: 'm³/h'
  },
  {
    name: '西区掺混 LDG',
    value: 70,
    unit: 'm³/h'
  },
  {
    name: '送 BFG 管网',
    value: 50.1,
    unit: 'm³/h'
  },
  {
    name: '去热卷二',
    value: 72.2,
    unit: 'km³/h'
  },
  {
    name: '热卷一',
    value: 13.9,
    unit: 'km³/h'
  },
  {
    name: '转底炉 1',
    value: 7.0,
    unit: 'km³/h'
  },
  {
    name: '超薄带',
    value: 67.4,
    unit: 'km³/h'
  },
  {
    name: '转底炉 2',
    value: 13.5,
    unit: 'km³/h'
  },
  {
    name: '135MW 1',
    value: 45.3,
    unit: 'km³/h'
  },
  {
    name: '135MW 2',
    value: 36.2,
    unit: 'km³/h'
  },
])
const mqhsjhxxList = ref([
  {
    name: '转炉总炉数\n' +
      '日计划',
    value: 567,
    unit: '炉'
  },
  {
    name: '转炉入炉铁水量\n' +
      '日计划',
    value: 200,
    unit: '吨'
  },
  {
    name: '转炉检修计划',
    value: '未进行',
    unit: ''
  },
  {
    name: '钢产量日计划',
    value: 300,
    unit: '吨'
  },
  {
    name: '转炉加入废钢总量',
    value: 500,
    unit: '吨'
  },
  {
    name: '转炉实绩钢产量',
    value: 100,
    unit: '吨'
  }
])
const mqhsjhTimeList = ref([
  {
    name: '兑铁',
    inTime: '04-23 03:19',
    outTime: '04-28 14:54',
  },
  {
    name: '吹炼',
    inTime: '04-23 03:19',
    outTime: '04-28 14:54',
  },
  {
    name: '出钢',
    inTime: '04-23 03:19',
    outTime: '04-28 14:54',
  }
])
const scmbList = ref([
  {
    id: 1,
    name: '1#转炉',
    current: 20,
    total: 30
  },
  {
    id: 2,
    name: '2#转炉',
    current: 25,
    total: 100
  },
  {
    id: 3,
    name: '3#转炉',
    current: 4,
    total: 29
  }
])
// 自定义进度条颜色(可选)
const customColor = '#409EFF';
const percentage = (item) => {
  return Math.round((item.current / item.total) * 100);
};
const yxzbList = ref([
  {
    name: '昨日LDG拒收时间',
    value: 0.00,
    unit: 'min'
  },
  {
    name: '昨日吨钢回收量',
    value: 86.08,
    unit: 'm³/t'
  },{
    name: '昨日LDG混入累积量',
    value: 2687.25,
    unit: 'km³'
  }
])
const ddtlResult = ref('')
const route = useRoute() // 路由
const message = useMessage() // 消息弹窗
// 聊天对话
const conversationListRef = ref()
const activeConversationId = ref<number | null>(null) // 选中的对话编号
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
// 消息列表
const messageRef = ref()
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表
const activeHistoryMessageList = ref<ChatMessageVO[]>([]) // 历史建议列表
const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中
const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中
// 消息滚动
const textSpeed = ref<number>(50) // Typing speed in milliseconds
const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
// 发送消息输入框
const isComposing = ref(false) // 判断用户是否在输入
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
const inputTimeout = ref<any>() // 处理输入中回车的定时器
const prompt = ref<string>() // prompt
const enableContext = ref<boolean>(false) // 是否开启上下文
// 接收 Stream 消息
const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('')
const tabPosition = ref('model')
// 模型数据
const modelData = ref()
/** 历史建议 */
const historyMessageRef = ref()
const openHistoryMessage = async () => {
  // 刷新 message 列表
  await getHistoryMessageList()
  historyMessageRef.value.open(activeHistoryMessageList.value, activeConversation.value)
}
// =========== 【聊天对话】相关 ===========
/** 获取对话信息 */
const getConversation = async (id: number | null) => {
  if (!id) {
    return
  }
  const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id)
  if (!conversation) {
    return
  }
  activeConversation.value = conversation
  activeConversationId.value = conversation.id
}
/**
 * 点击某个对话
 *
 * @param conversation 选中的对话
 * @return 是否切换成功
 */
const handleConversationClick = async (conversation: ChatConversationVO) => {
  // 对话进行中,不允许切换
  if (conversationInProgress.value) {
    message.alert('对话中,不允许切换!')
    return false
  }
  // 更新选中的对话 id
  activeConversationId.value = conversation.id
  activeConversation.value = conversation
  // 刷新 message 列表
  await getMessageList()
  // 滚动底部
  scrollToBottom(true)
  // 清空输入框
  // prompt.value = ''
  return true
}
/** 删除某个对话*/
const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
  // 删除的对话如果是当前选中的,那么就重置
  if (activeConversationId.value === delConversation.id) {
    await handleConversationClear()
  }
}
/** 清空选中的对话 */
const handleConversationClear = async () => {
  // 对话进行中,不允许切换
  if (conversationInProgress.value) {
    message.alert('对话中,不允许切换!')
    return false
  }
  activeConversationId.value = null
  activeConversation.value = null
  activeMessageList.value = []
}
// =========== 【消息列表】相关 ===========
/** 获取消息 message 列表 */
const getMessageList = async () => {
  try {
    if (activeConversationId.value === null) {
      return
    }
    // Timer 定时器,如果加载速度很快,就不进入加载中
    activeMessageListLoadingTimer.value = setTimeout(() => {
      activeMessageListLoading.value = true
    }, 60)
    // 获取消息列表
    activeMessageList.value = await ChatMessageApi.getEnergyChatMessageListByConversationId(
      activeConversationId.value
    )
    if(activeMessageList.value.length != 0) {
      prompt.value = activeMessageList.value[0].content
    }
    // 滚动到最下面
    await nextTick()
    await scrollToBottom()
  } finally {
    // time 定时器,如果加载速度很快,就不进入加载中
    if (activeMessageListLoadingTimer.value) {
      clearTimeout(activeMessageListLoadingTimer.value)
    }
    // 加载结束
    activeMessageListLoading.value = false
  }
}
/** 获取消息 message 列表 */
const getHistoryMessageList = async () => {
  if (activeConversationId.value === null) {
    return
  }
  // 获取消息列表
  activeHistoryMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
    activeConversationId.value
  )
  if (activeHistoryMessageList.value.length > 0) {
    activeHistoryMessageList.value.forEach((message: ChatMessageVO) => {
      if(message.type != 'user') {
        dealResult(message)
      }
    })
    return activeHistoryMessageList.value
  }
}
//处理调度推理结论
const dealResult = (message: any) => {
  const spliceText = message.content.includes("总结:") ? "总结:" : "结论:";
  const regex = new RegExp('(\\n*)([\\s\\S]*?)(\\n*)' + spliceText + '([\\s\\S]*)');
  const match = message.content.match(regex);
  if(match) {
    message.thinking = match[2];
    message.conclusion = match[4]
  }
  return message
}
/**
 * 消息列表
 *
 * 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去
 */
const messageList = computed(() => {
  if (activeMessageList.value.length > 0) {
    activeMessageList.value[1].thinking = dealResultAndData(activeMessageList.value[1].content)
    console.log(activeMessageList.value)
    return activeMessageList.value
  }
  // 没有消息时,如果有 systemMessage 则展示它
  if (activeConversation.value?.systemMessage) {
    return [
      {
        id: 0,
        type: 'system',
        content: activeConversation.value.systemMessage
      }
    ]
  }
  return []
})
//处理调度推理结论及数据
const dealResultAndData = (content: string) => {
  const spliceText = content.includes("总结:") ? "总结:" : "结论:";
// 创建同时捕获前后内容的正则表达式
  const regex = new RegExp(`^([\\s\\S]*?)${spliceText}([\\s\\S]*)$`);
  const match = content.match(regex);
// 获取前面段落(优先返回匹配结果,若无匹配返回全文)
  content = match ? match[1].trim() : content;
// 已存在的后面段落获取方式
  const result = match ? match[2].trim() : '';
  ddtlResult.value = result
  const dataRegex = /转炉煤气回收情况:\s*((?:.*?)(?=\n\d\.|\n|$))/s;
  const dataMatch = content.match(dataRegex);
  const dataContent = dataMatch ? dataMatch[1] : '';
  modelData.value = extractRecoveryDetails(dataContent, 78, 90);
  if(modelData.value.schedule.length === 3) {
    initLDGHSLYCChart()
  }
  initLDGGRQSYCChart()
  return content
}
const extractRecoveryDetails = (text, consume, gui, totalMinutes = 60) => {
  // 正则表达式匹配转炉数据块
  const furnaceBlocks = Array.from(text.matchAll(/(\d+#转炉.*?)(?=\d+#转炉|$)/gs))
    .map(match => match[0]);
  // 初始化数据结构
  const state = reactive({
    allSchedule: [],
    result: {},
    totalRecovery: Array.from({length: totalMinutes}, () => [0]),
    tankLevels: Array.from({length: totalMinutes + 1}, () => [0])
  });
  // 解析每个转炉的数据
  furnaceBlocks.forEach(block => {
    // 提取转炉编号
    const furnaceNum = parseInt(block.match(/(\d+)#/)[1]);
    // 解析时间段和回收量
    const periods = Array.from(block.matchAll(/第(\d+)-(\d+)[^\d]+?(\d+\.?\d*)km³/gs))
      .map(match => [
        parseInt(match[1]),
        parseInt(match[2]),
        parseFloat(match[3])
      ]);
    // 存储到结果
    state.result[furnaceNum] = periods;
    // 计算每分钟回收量
    periods.forEach(([start, end, amount]) => {
      const duration = end - start;
      const perMin = amount / duration;
      for(let t = start; t < end; t++) {
        state.totalRecovery[t][0] += perMin;
      }
    });
  });
  // 生成0/1序列
  furnaceBlocks.forEach(block => {
    const schedule = new Array(totalMinutes).fill(0);
    Array.from(block.matchAll(/第(\d+)-(\d+)/gs)).forEach(match => {
      const start = parseInt(match[1]) - 1;
      const end = parseInt(match[2]) - 1;
      for(let i = start; i <= end; i++) {
        if(i >= 0 && i < totalMinutes) schedule[i] = 1;
      }
    });
    state.allSchedule.push(schedule);
  });
  // 计算柜位
  let cumulative = 0;
  const consumptionRate = consume / 60;
  state.tankLevels = Array.from({length: totalMinutes + 1}, (_, t) => {
    if(t > 0) cumulative += state.totalRecovery[t-1][0];
    const consumed = consumptionRate * t;
    return [Number((gui + cumulative - consumed).toFixed(2))];
  });
  // 格式化输出
  return {
    schedule: state.allSchedule,
    result: state.result,
    totalRecovery: state.totalRecovery.map(v => [Number(v[0].toFixed(2))]),
    tankLevels: state.tankLevels
  }
}
/** 处理删除 message 消息 */
const handleMessageDelete = () => {
  if (conversationInProgress.value) {
    message.alert('回答中,不能删除!')
    return
  }
  // 刷新 message 列表
  getMessageList()
}
/** 处理 message 清空 */
const handlerMessageClear = async () => {
  if (!activeConversationId.value) {
    return
  }
  try {
    // 刷新 message 列表
    activeMessageList.value = []
  } catch {}
}
// =========== 【发送消息】相关 ===========
/** 处理来自 keydown 的发送消息 */
const handleSendByKeydown = async (event) => {
  // 判断用户是否在输入
  if (isComposing.value) {
    return
  }
  // 进行中不允许发送
  if (conversationInProgress.value) {
    return
  }
  const content = prompt.value?.trim() as string
  if (event.key === 'Enter') {
    if (event.shiftKey) {
      // 插入换行
      prompt.value += '\r\n'
      event.preventDefault() // 防止默认的换行行为
    } else {
      // 发送消息
      await doSendMessage(content)
      event.preventDefault() // 防止默认的提交行为
    }
  }
}
/** 处理来自【发送】按钮的发送消息 */
const handleSendByButton = () => {
  doSendMessage(prompt.value?.trim() as string)
}
/** 处理 prompt 输入变化 */
const handlePromptInput = (event) => {
  // 非输入法 输入设置为 true
  if (!isComposing.value) {
    // 回车 event data 是 null
    if (event.data == null) {
      return
    }
    isComposing.value = true
  }
  // 清理定时器
  if (inputTimeout.value) {
    clearTimeout(inputTimeout.value)
  }
  // 重置定时器
  inputTimeout.value = setTimeout(() => {
    isComposing.value = false
  }, 400)
}
// TODO :是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
const onCompositionstart = () => {
  isComposing.value = true
}
const onCompositionend = () => {
  // console.log('输入结束...')
  setTimeout(() => {
    isComposing.value = false
  }, 200)
}
/** 真正执行【发送】消息操作 */
const doSendMessage = async (content: string) => {
  // 校验
  if (content.length < 1) {
    message.error('发送失败,原因:内容为空!')
    return
  }
  if (activeConversationId.value == null) {
    message.error('还没创建对话,不能发送!')
    return
  }
  // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
  authUtil.setToken(await refreshToken())
  // 执行发送
  await doSendMessageStream({
    conversationId: activeConversationId.value,
    content: content
  } as ChatMessageVO)
}
/** 真正执行【发送】消息操作 */
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
  // 创建 AbortController 实例,以便中止请求
  conversationInAbortController.value = new AbortController()
  // 标记对话进行中
  conversationInProgress.value = true
  // 设置为空
  receiveMessageFullText.value = ''
  try {
    // 1.0 每次发送消息前先将消息记录chat message清空
    await handlerMessageClear()
    // 1.1 先添加两个假数据,等 stream 返回再替换
    activeMessageList.value.push({
      id: -1,
      conversationId: activeConversationId.value,
      type: 'user',
      content: userMessage.content,
      createTime: new Date()
    } as ChatMessageVO)
    activeMessageList.value.push({
      id: -2,
      conversationId: activeConversationId.value,
      type: 'assistant',
      content: '思考中...',
      createTime: new Date()
    } as ChatMessageVO)
    // 1.2 滚动到最下面
    await nextTick()
    await scrollToBottom() // 底部
    // 1.3 开始滚动
    textRoll()
    // 2. 发送 event stream
    let isFirstChunk = true // 是否是第一个 chunk 消息段
    await ChatMessageApi.sendChatMessageStream(
      userMessage.conversationId,
      userMessage.content,
      conversationInAbortController.value,
      enableContext.value,
      async (res) => {
        const { code, data, msg } = JSON.parse(res.data)
        if (code !== 0) {
          message.alert(`对话异常! ${msg}`)
        }
        // 如果内容为空,就不处理。
        if (data.receive.content === '') {
          return
        }
        // 首次返回需要添加一个 message 到页面,后面的都是更新
        if (isFirstChunk) {
          isFirstChunk = false
          // 弹出两个假数据
          activeMessageList.value.pop()
          activeMessageList.value.pop()
          // 更新返回的数据
          activeMessageList.value.push(data.send)
          activeMessageList.value.push(data.receive)
        }
        // debugger
        receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
        // 滚动到最下面
        await scrollToBottom()
      },
      (error) => {
        message.alert(`对话异常! ${error}`)
        stopStream()
      },
      () => {
        stopStream()
      }
    )
  } catch {}
}
/** 停止 stream 流式调用 */
const stopStream = async () => {
  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
  if (conversationInAbortController.value) {
    conversationInAbortController.value.abort()
  }
  // 设置为 false
  conversationInProgress.value = false
}
/** 编辑 message:设置为 prompt,可以再次编辑 */
const handleMessageEdit = (message: ChatMessageVO) => {
  prompt.value = message.content
}
/** 刷新 message:基于指定消息,再次发起对话 */
const handleMessageRefresh = (message: ChatMessageVO) => {
  doSendMessage(message.content)
}
// ============== 【消息滚动】相关 =============
/** 滚动到 message 底部 */
const scrollToBottom = async (isIgnore?: boolean) => {
  await nextTick()
  if (messageRef.value) {
    messageRef.value.scrollToBottom(isIgnore)
  }
}
/** 自提滚动效果 */
const textRoll = async () => {
  let index = 0
  try {
    // 只能执行一次
    if (textRoleRunning.value) {
      return
    }
    // 设置状态
    textRoleRunning.value = true
    receiveMessageDisplayedText.value = ''
    const task = async () => {
      // 调整速度
      const diff =
        (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
      if (diff > 5) {
        textSpeed.value = 10
      } else if (diff > 2) {
        textSpeed.value = 30
      } else if (diff > 1.5) {
        textSpeed.value = 50
      } else {
        textSpeed.value = 100
      }
      // 对话结束,就按 30 的速度
      if (!conversationInProgress.value) {
        textSpeed.value = 10
      }
      if (index < receiveMessageFullText.value.length) {
        receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
        index++
        // 更新 message
        const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
        lastMessage.content = receiveMessageDisplayedText.value
        // 滚动到住下面
        await scrollToBottom()
        // 重新设置任务
        timer = setTimeout(task, textSpeed.value)
      } else {
        // 不是对话中可以结束
        if (!conversationInProgress.value) {
          textRoleRunning.value = false
          clearTimeout(timer)
        } else {
          // 重新设置任务
          timer = setTimeout(task, textSpeed.value)
        }
      }
    }
    let timer = setTimeout(task, textSpeed.value)
  } catch {}
}
const LDGHSLYCEhartContainer = ref();
  // 生成未来60秒的时间标签(LDG回收量预测)
const generateLDGHSLYCTimeLabels = () => {
    const labels = [];
    for (let i = 0; i < 60; i++) {
      labels.push(i);
    }
    return labels;
  };
// 生成未来60秒和过去60秒的时间标签
const generateLDGGRQSYCTimeLabels = () => {
  const labels = [];
  const now = new Date();
  // 补零函数
  const padZero = num => num.toString().padStart(2, '0')
  for (let i = 0; i < 121; i++) {
    const date = new Date(now.getTime() + (i-60) * 1000);
    const formatted = `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())} ` +
      `${padZero(date.getHours())}:${padZero(date.getMinutes())}:${padZero(date.getSeconds())}`;
    labels.push(formatted);
  }
  return labels;
};
// 数据格式转换示例(LDG回收量预测)
const seriesHSLYCDataConverter = () => {
  const recovery = modelData.value.totalRecovery
  const totalRecovery = []
  recovery.forEach(item => {
    totalRecovery.push(item[0])
  })
  return totalRecovery;
};
const seriesHSLYCDataSchedule = (type) => {
  const max = LDGMaxTotalValue()
  const schedule = modelData.value.schedule[type]
  // 返回对象格式数据,包含原始值和基准值
  const baseline = round(max, 0) + (6 - 2 * type)
  return schedule.map(item => ({
    value: item + baseline, // 显示值 = 原始值 + 基准值
    original: item        // 原始值
  }));
};
// 计算总回收量的最大值
const LDGMaxTotalValue = () => {
  const total = modelData.value.totalRecovery
  let returnValue = computed(() => {
    return Math.max(...total)
  })
  return returnValue.value
};
// 计算柜容预测趋势的最大值和最小值,用于上下界限显示
const LDGComputedValue = (type) => {
  const tank = modelData.value.tankLevels
  let returnValue = 0;
  if(type == 'max') {
    returnValue = computed(() => {
      return Math.max(...tank) + 20
    })
  } else if(type == 'min') {
    returnValue = computed(() => {
      return Math.min(...tank) - 60
    })
  } else if(type == 'average') {
    returnValue = computed(() => {
      let sum = 0
      tank.forEach((item) => {
        sum += item[0]
      })
      return (sum / tank.length).toFixed(0);
    })
  }
  return returnValue.value
};
// 数据格式转换示例(LDG柜容趋势预测)
const seriesGRQSYCDataConverter = () => {
  const tank = modelData.value.tankLevels
  const tankLevels = []
  tank.forEach(item => {
    tankLevels.push(item[0])
  })
  return tankLevels;
};
// 图表配置
const initLDGHSLYCChart = () => {
  const LDGHSLYCChart = echarts.init(LDGHSLYCEhartContainer.value);
  const option = {
    tooltip: {
      trigger: 'axis',
      formatter: function (params) {
        let tooltipContent = params[0].name + '<br/>'; // 时间标签
        params.forEach(param => {
          const seriesName = param.seriesName;
          let originalValue;
          // 判断是否为转炉系列
          if (seriesName.includes('转炉')) {
            // 直接从数据项中获取原始值
            originalValue = param.data.original;
            tooltipContent += `
              ${param.marker}
              ${seriesName}: ${originalValue.toFixed(0)}<br/>
            `;
          } else {
            // 总回收量直接显示值
            originalValue = param.value;
            tooltipContent += `
              ${param.marker}
              ${seriesName}: ${originalValue.toFixed(2)}<br/>
            `;
          }
        });
        return tooltipContent;
      }
    },
    grid: {
      left: 25,
      right: 25,
      bottom: 10,
      top: 30,
      containLabel: true
    },
    legend: {
      top: 10,
      right: 10,
      data: ['1#转炉', '2#转炉', '3#转炉', '总回收量'],
      textStyle: {
        color: '#8FD6FE'
      },
      itemWidth: 20, // 图例标记的宽度
      itemHeight: 0, // 图例标记的高度,设为较小值使其更像线条
    },
    xAxis: {
      type: 'category',
      boundaryGap: false,
      data: generateLDGHSLYCTimeLabels(),
      axisTick: {
        show: false
      },
      axisLine: {
        lineStyle: {
          color: '#C7E7FF'
        }
      },
      axisLabel: {
        color: '#fff'
      }
    },
    yAxis: {
      type: 'value',
      min: 0,
      axisLine: {
        show: true,
        lineStyle: {
          color: '#C7E7FF',
        }
      },
      axisTick: {
        show: false
      },
      axisLabel: {
        show: false
      },
      splitLine: {
        show: false // Y轴网格线
      }
    },
    series: [
      {
        name: '1#转炉',
        type: 'line',
        step: 'start',
        data: seriesHSLYCDataSchedule(0),
        showSymbol: false, // 取消数据点
        emphasis: {
          focus: 'series'
        },
        lineStyle: {
          color: '#FF7686' // 粉色
        }
      },
      {
        name: '2#转炉',
        type: 'line',
        step: 'start',
        data: seriesHSLYCDataSchedule(1),
        showSymbol: false, // 取消数据点
        emphasis: {
          focus: 'series'
        },
        lineStyle: {
          color: '#49FFD3' // 绿色
        }
      },
      {
        name: '3#转炉',
        type: 'line',
        step: 'start',
        showSymbol: false, // 取消数据点
        data: seriesHSLYCDataSchedule(2),
        color: '#FAC858',
        emphasis: {
          focus: 'series'
        },
        lineStyle: {
          color: '#FFAE81' // 橙色
        },
      },
      {
        name: '总回收量',
        type: 'line',
        step: 'start',
        showSymbol: false, // 取消数据点
        data: seriesHSLYCDataConverter(),
        emphasis: {
          focus: 'series'
        },
        lineStyle: {
          color: 'white'
        },
      }
    ]
  };
  LDGHSLYCChart.setOption(option);
};
const LDGGRYCEhartContainer = ref();
/** 带预测的转炉数据阶梯图配置 */
const initLDGGRQSYCChart = () => {
  const labels = generateLDGGRQSYCTimeLabels()
  const tankLevels = seriesGRQSYCDataConverter()
  const fullData = [];
  for(let i = 0; i < 121; i ++ ) {
    let value = 90
    if(i >= 60) {
      value = tankLevels[i - 60]
    }
    fullData.push([labels[i], value]);
  }
  const splitTime = formatToDateTime(new Date()); // 分割时间点(当前时间)
  const upperLimit = LDGComputedValue('max');
  const lowerLimit = LDGComputedValue('min');
  const averageValue = LDGComputedValue('average');
  // 分割真实数据和预测数据
  const splitIndex = fullData.findIndex(item => item[0] === splitTime);
  const realData = fullData.slice(0, splitIndex + 1);
  const predictData = fullData.slice(splitIndex);
  // 创建纯净的上下限数据数组
  const upperLimitData = [
    [labels[0], upperLimit],
    [labels[labels.length - 1], upperLimit]
  ];
  const lowerLimitData = [
    [labels[0], lowerLimit],
    [labels[labels.length - 1], lowerLimit]
  ];
  const LDGGRQSYCChart = echarts.init(LDGGRYCEhartContainer.value);
  const option = {
    grid: {
      left: 0,
      right: 0,
      bottom: 10,
      top: 20,
      containLabel: true
    },
    tooltip: {
      trigger: 'axis'
    },
    xAxis: {
      type: 'time',
      axisLabel: {
        formatter: (value) => {
          return echarts.time.format(value, '{mm}:{ss}', false)
        },
        color: '#C7E7FF'
      },
      axisLine: {
        show: true,
        lineStyle: {
          color: '#C7E7FF'
        }
      },
      axisTick: {
        show: false
      },
      splitLine: {
        show: false
      }
    },
    yAxis: {
      type: 'value',
      min: 0,
      max: LDGComputedValue('max') + 30,
      axisLine: {
        show: true,
        lineStyle: {
          color: '#C7E7FF'
        }
      },
      axisTick: {
        show: false
      },
      splitLine: {
        show: false
      }
    },
    series: [
      // 真实数据(实线)
      {
        type: 'line',
        data: realData,
        smooth: true,
        symbol: 'none',
        lineStyle: { color: '#95E6FF' },
        markLine: {
          symbol: ['none', 'none'],
          label: {
            show: false
          },
          data: [{
            xAxis: splitTime,
            lineStyle: {
              type: 'solid',
              color: '#5DFF9E',
              width: 1
            }
          }]
        }
      },
      // 预测数据(虚线)
      {
        type: 'line',
        data: predictData,
        smooth: true,
        symbol: 'none',
        lineStyle: {
          type: 'dashed',
          color: '#E76666',
          dashOffset: 5
        }
      },
      // 上限填充
      {
        type: 'line',
        data: upperLimitData,
        lineStyle: { width: 0 },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: 'rgba(255,0,0,0.2)' },   // 顶部颜色
            { offset: 1, color: 'rgba(255,0,0,0.5)' }      // 底部颜色(到upperLimit)
          ]),
          origin: 'end' // 关键:从线条向上填充
        },
        silent: true
      },
      // 下限填充
      {
        type: 'line',
        data: lowerLimitData,
        lineStyle: { width: 0 },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
            { offset: 0, color: 'rgba(0,0,255,0.2)' },
            { offset: 1, color: 'rgba(0,0,255,0.5)' }
          ])
        },
        silent: true
      },
      // 辅助线系列
      {
        type: 'line',
        symbol: 'none', // 关闭辅助线系列自身的数据点
        markLine: {
          symbol: ['none', 'none'], // 全局隐藏标记点
          data: [
            // 上限辅助线配置部分
            {
              yAxis: upperLimit,
              symbol: 'none',
              label: { show: false },
              emphasis: {
                // 这里添加让高亮时标记点也不显示
                itemStyle: {
                  borderWidth: 0,
                  borderColor: 'transparent',
                  color: 'transparent'
                },
                label: { show: false }
              },
              lineStyle: { color: 'rgba(255,0,0)', width: 1, type: 'solid' }
            },
            // 下限辅助线配置部分
            {
              yAxis: lowerLimit,
              symbol: 'none',
              label: { show: false },
              emphasis: {
                // 这里添加让高亮时标记点也不显示
                itemStyle: {
                  borderWidth: 0,
                  borderColor: 'transparent',
                  color: 'transparent'
                },
                label: { show: false }
              },
              lineStyle: { color: 'rgba(0,0,255)', width: 1, type: 'solid' }
            },
            {
              yAxis: averageValue,
              label: { show: false },
              lineStyle: { type: 'dashed', color: 'rgba(0,194,255,0.52)', width: 1 }
            }
          ]
        }
      }
    ]
  };
  LDGGRQSYCChart.setOption(option);
}
const isFullscreen = ref(false);
// 核心:统一检测全屏状态
const updateFullscreenStatus = () => {
  // 同时检测 API 全屏和 F11 全屏(近似)
  isFullscreen.value = !!document.fullscreenElement || window.outerHeight === screen.height;
};
// 监听全屏 API 变化
const handleFullscreenChange = () => {
  updateFullscreenStatus();
};
// 监听 F11 按键(兜底)
const handleKeyPress = (e) => {
  if (e.key === 'F11') {
    e.preventDefault(); // 尝试阻止默认行为(部分浏览器允许)
    setTimeout(updateFullscreenStatus, 100); // 延迟确保状态更新
  }
};
// 监听窗口大小变化(F11 全屏会触发)
const handleResize = () => {
  updateFullscreenStatus();
};
// 切换全屏(API 方式)
const toggleFullscreen = async () => {
  if (!document.fullscreenElement) {
    await document.documentElement.requestFullscreen();
  } else {
    await document.exitFullscreen();
  }
};
//初始化全屏信息
const initFullscreen = async () => {
  // Fullscreen API 事件
  const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
  events.forEach(event => {
    document.addEventListener(event, handleFullscreenChange);
  });
  // 窗口变化 + 键盘事件兜底
  window.addEventListener('resize', handleResize);
  window.addEventListener('keydown', handleKeyPress);
  // 初始状态检测
  updateFullscreenStatus();
}
/** 初始化 **/
onMounted(async () => {
  await initFullscreen()
  // 如果有 conversationId 参数,则默认选中
  if (route.query.conversationId) {
    const id = route.query.conversationId as unknown as number
    activeConversationId.value = id
    await getConversation(id)
  }
  // 获取列表数据
  activeMessageListLoading.value = true
  await getMessageList()
})
// 清理监听
onUnmounted(() => {
  const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
  events.forEach(event => {
    document.removeEventListener(event, handleFullscreenChange);
  });
  window.removeEventListener('resize', handleResize);
  window.removeEventListener('keydown', handleKeyPress);
});
</script>
<style lang="scss" scoped>
.gas-scheduling-container {
  display: flex;
  font-family: Microsoft YaHei, Microsoft YaHei;
  .fullscreen-btn {
    background-color: transparent;
    border: 1px solid rgba(115, 196, 255, 0.5);
    position: fixed;
    margin-top: 3.5%;
    margin-left: 28%;
    color: rgba(115, 196, 255, 0.8);
    font-size: 12px;
  }
  /* 背景层容器 */
  &::before {
    content: '';
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: -1; /* 置于内容层下方 */
    background:
      url("@/assets/ai/zhuanlu/bg.png") center/cover no-repeat,
      linear-gradient(to bottom, #0a1633dd, #0a1633dd); /* 叠加深色遮罩 */
    pointer-events: none; /* 防止遮挡交互 */
  }
  .gas-scheduling-left {
    width: 23%;
    height: 89%;
    margin: 2rem 2rem 0 1.8rem;
    z-index: 1;
    background-color: rgba(0, 0, 0, 0); /* 透明背景 */
    .data1-item {
      height: 2.6rem;
      width: 42%;
      display: inline-block;
      margin: 8px 10px ;
      background: url("@/assets/ai/zhuanlu/data_bg1.png") center/cover no-repeat;
    }
    .data2-item {
      height: 30px;
      width: 42%;
      display: inline-block;
      margin: 6px 8px;
      background: url("@/assets/ai/zhuanlu/data_bg2.png") center/cover no-repeat;
    }
    .content {
      margin-left: 16px;
      .value {
        span:nth-child(1){
          height: 19px;
          font-weight: bold;
          font-size: 16px;
          color: #FFAE81;
          line-height: 19px;
        }
        span:nth-child(2) {
          height: 16px;
          font-weight: 400;
          font-size: 12px;
          color: #C7E7FF;
        }
      }
      .name {
        height: 16px;
        font-weight: 400;
        font-size: 12px;
        color: #C7E7FF;
      }
    }
    .content2 {
      display: flex;
      width: 11rem;
      margin-left: 10px;
      .name {
        width: 95px;
        height: 18px;
        font-weight: 400;
        font-size: 14px;
        color: #C7E7FF;
      }
      .value {
        margin-left: auto;
        margin-right: 5px;
        span:nth-child(1){
          height: 15px;
          font-weight: bold;
          font-size: 15px;
          color: #FFAE81;
          line-height: 15px;
        }
        span:nth-child(2) {
          height: 15px;
          font-weight: 400;
          font-size: 12px;
          color: #C7E7FF;
        }
      }
    }
    #mqhsssxx {
      .title {
        height: 2rem;
        background:
          url("@/assets/ai/zhuanlu/mqhsssxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
    }
    #tsxx {
      .title {
        margin-top: 5px;
        height: 2rem;
        background:
          url("@/assets/ai/zhuanlu/tsxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
    }
    #zlxx {
      .title {
        margin-top: 5px;
        height: 2rem;
        background:
          url("@/assets/ai/zhuanlu/zlxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
      :deep(.el-table) {
        font-weight: 400;
        font-size: 14px;
        color: #DBEEFF;
        text-align: left;
        background-color: transparent !important;
      }
      .transparent-table {
        margin-top: 14px;
      }
      /* 行样式 */
      :deep(.el-table .el-table__body tr) {
        border: 1px solid rgba(16, 198, 255, 0.3);
        margin-bottom: 4px;
        border-radius: 4px;
        overflow: hidden;
        background-color: transparent;
      }
      /* 行内容样式 */
      :deep(.el-table .el-table__body td) {
        padding: 4px 0;
        color: #FFAA5D;
      }
      /* 列头样式 */
      :deep(.el-table th) {
        color: #8FD6FE;
        background:
          url("@/assets/ai/zhuanlu/table_header_bg.png") center/cover no-repeat !important; /* 叠加深色遮罩 */
        border: none;
      }
      :deep(.el-table tr) {
        background: transparent;
      }
      /* 行头样式 */
      :deep(.el-table .el-table__body td:first-child) {
        color: #8FD6FE;
        font-weight: 500;
        background-color: transparent;
      }
      :deep(.el-table .el-table__body tr:nth-child(even) td) {
        background-color: rgba(0, 194, 255, 0.1);
      }
      :deep(.el-table .el-table__body tr:nth-child(odd) td) {
        background-color: rgba(0, 194, 255, 0.2);
      }
      /* 移除表格内部边框线 */
      :deep(.el-table td, .el-table th.is-leaf) {
        border-bottom: none;
      }
      :deep(.el-table .el-table__inner-wrapper:before) {
        background-color: transparent !important;
      }
    }
    #mqxhssxx {
      .title {
        height: 30px;
        margin-top: 15px;
        background:
          url("@/assets/ai/zhuanlu/mqxhssxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
    }
  }
  .gas-scheduling-center {
    margin-top: 2.6rem;
    width: 55.5rem !important;
    .mode-switch {
      margin-top: 20px;
      margin-left: 43rem;
      font-weight: 400;
      font-size: 14px;
      color: #73C4FF;
      width: 40%;
      /* 必须穿透到组件内部层级 */
      :deep(.custom-radio-group) {
        --el-color-primary: red !important; /* 强制修改主题色变量 */
      }
      /* 所有按钮基础样式 */
      :deep(.custom-radio-group .el-radio-button__inner) {
        background: black !important; /* 未选中黑色背景 */
        border: 1px solid rgba(173, 216, 230, 0.3) !important; /* 蓝色边框 */
        font-weight: 300;
        font-size: 14px;
        color: #DBEEFF;
        transition: all 0.3s;
      }
      /* 选中状态 */
      :deep(.custom-radio-group .el-radio-button.is-active .el-radio-button__inner) {
        background: #b92220 !important;
        color: gold !important;
        font-weight: bolder;
      }
      /* 强制覆盖原生选中状态 */
      :deep(.custom-radio-group .el-radio-button__orig-radio:checked + .el-radio-button__inner) {
        background: inherit !important; /* 继承上层样式 */
      }
    }
    // 头部
    .detail-container {
      margin-left: 5px;
      background-color: rgba(0, 0, 0, 0); /* 透明背景 */
      z-index: 1;
      .header {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        box-shadow: 0 0 0 0 #dcdfe6;
        .title {
          font-size: 18px;
          font-weight: bold;
        }
        .btns {
          display: flex;
          width: 300px;
          flex-direction: row;
          justify-content: flex-end;
          //justify-content: space-between;
          .btn {
            padding: 10px;
          }
        }
      }
    }
    // main 容器
    .main-container {
      margin: 0;
      padding: 0;
      position: relative;
      height: 30rem;
      .message-container {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        overflow-y: hidden;
        padding: 0;
        margin: 0;
      }
      .title {
        background: url("@/assets/ai/zhuanlu/think_bg.png") center/cover no-repeat;
        width: auto;
        height: 1.8rem;
        font-weight: 400;
        font-size: 14px;
        color: #8FD6FE;
        text-align: left;
        font-style: normal;
        text-transform: none;
        span {
          margin-left: 30px;
        }
      }
    }
    .result-container-title {
      margin-top: 15px;
      background: url("@/assets/ai/zhuanlu/ddtljl_result_title.png") center/cover no-repeat;
      width: auto;
      height: 1.8rem;
      font-weight: 400;
      font-size: 14px;
      color: #8FD6FE;
      text-align: left;
      font-style: normal;
      text-transform: none;
      span {
        margin-left: 30px;
      }
      .history-button {
        color: rgba(143, 214, 254);
        font-weight: bold;
        float: right;
        margin-right: 5px;
        background-color: rgba(0, 255, 255, 0.1);
        border-radius: 3px;
        padding: 0 5px;
        border: none;
        cursor: pointer
      }
      .history-button:hover {
        color: rgba(143, 214, 254, 0.5);
      }
    }
    // main 容器
    .result-container {
      padding: 0;
      position: relative;
      width: 100%;
      /* WebKit */
      &::-webkit-scrollbar {
        width: 6px;
        background: transparent;
      }
      &::-webkit-scrollbar-thumb {
        border-radius: 4px;
        background: rgba(0, 0, 0, 0.15);
        transition: background 0.3s;
        &:hover { background: rgba(0, 0, 0, 0.25); }
      }
      .result {
        margin-top: 10px;
        margin-left: 18px;
        border-left: 1px solid #73C4FF;
        .result-content {
          width: 53rem;
          height: 6rem;
          margin-left: 10px;
          font-weight: 400;
          font-size: 14px;
          background-color: rgba(219,238,255,0);
          line-height: 21px;
          text-align: left;
          font-style: normal;
          text-transform: none;
          border: 0;
          color: rgba(219,238,255,0.6);
        }
        .result-content:focus {
          outline: none;
        }
      }
    }
    // 输入框
    .input-container {
      display: flex;
      flex-direction: column;
      height: auto;
      margin: 0;
      padding: 0;
      overflow-y: auto;        /* 垂直方向溢出时显示滚动条 */
      overflow-x: hidden;      /* 水平方向隐藏滚动条 */
      /* Firefox */
      scrollbar-width: thin;
      scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
      /* WebKit */
      &::-webkit-scrollbar {
        width: 6px;
        background: transparent;
      }
      &::-webkit-scrollbar-thumb {
        border-radius: 4px;
        background: rgba(0, 0, 0, 0.15);
        transition: background 0.3s;
        &:hover { background: rgba(0, 0, 0, 0.25); }
      }
      .prompt-from {
        display: flex;
        flex-direction: column;
        margin: 10px 20px 20px 20px;
        padding: 9px 10px;
        width: 53.9rem;
        background: rgba(115,196,255,0.05);
        border-radius: 4px 4px 4px 4px;
        border: 1px solid #73C4FF;
      }
      .prompt-input {
        width: 53rem;
        height: 11rem;
        font-weight: 400;
        font-size: 14px;
        background-color: rgba(219,238,255,0);
        line-height: 21px;
        text-align: left;
        font-style: normal;
        text-transform: none;
        border: 0;
        color: rgba(219,238,255,0.6);
      }
      .prompt-input:focus {
        outline: none;
      }
      .prompt-btns {
        display: flex;
        justify-content: space-between;
        padding-bottom: 0;
        padding-top: 5px;
        .content {
          /* 默认状态 */
          .el-button {
            background: transparent !important;
            border-color: rgba(115, 196, 255, 0.5);
            color: #73C4FF;
            border-radius: 15px !important;
          }
          /* 上下文图标处理 */
          .content-icon {
            color: blue;  /* 图标颜色 */
            font-size: 18px;
            margin-right: 10px;
            background: url("@/assets/ai/zhuanlu/content.png");
            vertical-align: middle;
          }
          /* 选中状态 */
          .active-button {
            background: #409eff !important;
            border-color: #409eff !important;
            color: white !important;
            .content-icon {
              background: url("@/assets/ai/zhuanlu/content_select.png");
              vertical-align: middle;
            }
          }
          /* 按钮组间距处理 */
          .button-group .el-button {
            margin-left: 0;
            border-radius: 4px;
          }
          /* 悬停效果 */
          .el-button:not(.active-button):hover {
            border-color: rgba(115,196,255,0.5);
            color: #409eff;
          }
        }
        .message {
          /* 所有状态通用透明背景 */
          :deep(.el-button) {
            background: rgba(73, 254, 210, 0.8) !important;
            border-color: currentColor; /* 保持与文字同色 */
            font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
            font-weight: bold;
            font-size: 16px;
            color: #123C4E;
            clip-path: polygon(
                0 0,
                100% 0,
                100% 100%,
                10px 100%,          /* 右下方向留出10px */
                0 calc(100% - 10px) /* 左上方向留出10px */
            );
            position: relative;
            padding-left: 15px; /* 增加右侧留白 */
          }
          /* 悬停状态 */
          :deep(.el-button:hover) {
            background: rgba(73, 254, 210, 0.6) !important; /* 轻微悬停反馈 */
          }
          /* 点击状态 */
          :deep(.el-button:active) {
            background: rgba(73, 254, 210, 1) !important;
          }
          /* 禁用状态 */
          :deep(.el-button.is-disabled) {
            opacity: 0.6;
            background: transparent !important;
          }
          /* 核心样式覆盖 */
          :deep(.el-switch__core) {
            background: transparent !important;
            border-radius: 0 0 15px 0 !important;
            border: none !important;
            height: 40px !important;
          }
          /* 按钮内容容器 */
          .button-content {
            display: flex;
            align-items: center;
            padding: 0 15px;
            height: 100%;
          }
        }
      }
    }
  }
  .gas-scheduling-right {
    width: 22%;
    height: 89%;
    margin-left: 4.3rem;
    margin-top: 2.8rem;
    z-index: 1;
    background-color: rgba(0, 0, 0, 0); /* 透明背景 */
    #ldghslyc {
      .title {
        height: 1.8rem;
        background:
          url("@/assets/ai/zhuanlu/ldghslyc_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
    }
    #ldggrqsyc {
      .title {
        height: 1.8rem;
        background:
          url("@/assets/ai/zhuanlu/ldggrqsyc_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
    }
    #mqhsjhxx {
      .title {
        height: 1.8rem;
        background:
          url("@/assets/ai/zhuanlu/mqhsjhxx_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
      .time-content {
        display: inline-block;
        width: 32%;
        height: 2.9rem;
        margin: 10px 0 5px 5px;
        font-weight: 400;
        font-size: 12px;
        color: #C7E7FF;
        .time-content-item {
          display: flex;
          .name {
            width: 1.6rem;
            height: 3rem;
            padding: 8px 3px;
            background: linear-gradient( 180deg, rgba(115,196,255,0.1) 0%, rgba(255,136,69,0.1) 100%);
            border-radius: 2px 2px 2px 2px;
            border: 1px solid rgba(255,255,255,0.15);
            opacity: 0.9;
          }
          .name span {
            height: 1.8rem;
            font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
            font-weight: bold;
            font-size: 14px;
            color: #C7E7FF;
            line-height: 15px;
            text-align: left;
            font-style: normal;
            text-transform: none;
          }
          .time {
            width: 105px;
            height: 38px;
            margin: 4px;
            display: inline-block;
            .in-pot{
              width: 4px;
              height: 4px;
              background: #49FFD3;
              border-radius: 80px 80px 80px 80px;
              margin-top: 7px;
              margin-right: 3px;
            }
            .out-pot{
              width: 4px;
              height: 4px;
              background: #FFAE81;
              border-radius: 80px 80px 80px 80px;
              margin-top: 7px;
              margin-right: 3px;
            }
          }
          .time > div {
            float: left;
            height: 25px;
          }
        }
      }
      .data2-item {
        height: 2.8rem;
        width: 45%;
        display: inline-block;
        margin: 6px 8px;
        background: url("@/assets/ai/zhuanlu/data_bg3.png") no-repeat;
      }
      .content {
        display: flex;
        width: 192px;
        margin-left: 10px;
        .name {
          width: 95px;
          height: 18px;
          font-weight: 400;
          font-size: 14px;
          color: #C7E7FF;
        }
        .value {
          margin-top: 10px;
          margin-left: auto;
          margin-right: 5px;
          span:nth-child(1) {
            height: 15px;
            font-weight: bold;
            font-size: 15px;
            color: #FFAE81;
            line-height: 15px;
          }
          span:nth-child(2) {
            height: 15px;
            font-weight: 400;
            font-size: 12px;
            color: #C7E7FF;
          }
        }
      }
    }
    #scmbyyxzb {
      margin-top: 10px;
      .title {
        height: 1.8rem;
        background:
          url("@/assets/ai/zhuanlu/scmbyyxzb_title.png") center/cover no-repeat; /* 叠加深色遮罩 */
      }
      .little-title {
        font-size: 14px;
        color: #8FD6FE;
        margin: 10px;
      }
      .data3-item {
        height: 5.2rem;
        width: 30%;
        display: inline-block;
        margin: 0 6px 6px 6px;
        background: url("@/assets/ai/zhuanlu/data_bg4.png") center/cover no-repeat;
        .name {
          font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
          font-weight: bold;
          font-size: 16px;
          color: #FFFFFF;
          margin-left: 3px;
        }
        .value {
          color: #DBEEFF;
          font-size: 14px;
          margin-left: 3px;
        }
        .value-content {
          color: #8FD6FE;
          font-size: 12px;
          margin-left: 3px;
        }
      }
      .zb-content {
        display: inline-block;
        .item {
          float: left;
        }
        .data4-item {
          height: 2.2rem;
          width: 7.8rem;
          display: inline-block;
          margin: 5px 0;
          .content {
            margin-left: 16px;
            .value {
              text-align: center;
              span:nth-child(1){
                height: 19px;
                font-weight: bold;
                font-size: 16px;
                color: #FFAE81;
                line-height: 19px;
              }
              span:nth-child(2) {
                height: 16px;
                font-weight: 400;
                font-size: 12px;
                color: #C7E7FF;
              }
            }
            .name {
              font-weight: 400;
              font-size: 12px;
              color: #C7E7FF;
            }
          }
          .content div {
            height: 25px;
          }
        }
        .left-label {
          width: 1.2rem;
          height: 3.5rem;
          background: url("@/assets/ai/zhuanlu/left_label.png") center/cover no-repeat;
        }
        .right-label {
          width: 1.2rem;
          height: 3.5rem;
          background: url("@/assets/ai/zhuanlu/right_label.png") center/cover no-repeat;
        }
      }
    }
  }
}
/* 背景颜色修改 */
:deep(.el-progress .el-progress-bar .el-progress-bar__outer) {
  width: 90%;
  margin: 5px 0 2px 5px;
  background-color: rgba(64, 158, 255, 0.3) !important;
  border-radius: 2px;
}
/* 进度条填充颜色 */
:deep(.el-progress-bar__inner) {
  border-radius: 2px;
  transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1);
}
</style>
src/views/ai/questionparamsetting/QuestionParamSettingForm.vue
对比新文件
@@ -0,0 +1,117 @@
<template>
  <div class="app-container">
    <!-- 对话框(添加 / 修改) -->
    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag
               append-to-body>
      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading"
               label-width="100px">
        <el-form-item label="问题模板id" prop="templateId">
          <el-input v-model="formData.templateId" placeholder="请输入问题模板id"/>
        </el-form-item>
        <el-form-item label="key" prop="settingKey">
          <el-input v-model="formData.settingKey" placeholder="请输入key"/>
        </el-form-item>
        <el-form-item label="参数名称" prop="settingName">
          <el-input v-model="formData.settingName" placeholder="请输入参数名称"/>
        </el-form-item>
        <el-form-item label="参数默认值" prop="settingValue">
          <el-input v-model="formData.settingValue" placeholder="请输入参数默认值"/>
        </el-form-item>
        <el-form-item label="排序" prop="sort">
          <el-input v-model="formData.sort" placeholder="请输入排序"/>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
        <el-button @click="dialogVisible = false">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import * as QuestionParamSettingApi from '@/api/ai/questionparamsetting';
export default {
  name: "QuestionParamSettingForm",
  components: {},
  data() {
    return {
      // 弹出层标题
      dialogTitle: "",
      // 是否显示弹出层
      dialogVisible: false,
      // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
      formLoading: false,
      // 表单参数
      formData: {
        id: undefined,
        templateId: undefined,
        settingKey: undefined,
        settingName: undefined,
        settingValue: undefined,
        sort: undefined,
      },
      // 表单校验
      formRules: {
        templateId: [{required: true, message: '问题模板id不能为空', trigger: 'blur'}],
      },
    };
  },
  methods: {
    /** 打开弹窗 */
    async open(id) {
      this.dialogVisible = true;
      this.reset();
      // 修改时,设置数据
      if (id) {
        this.formLoading = true;
        try {
          const res = await QuestionParamSettingApi.getQuestionParamSetting(id);
          this.formData = res.data;
          this.title = "修改大模型问题设置参数";
        } finally {
          this.formLoading = false;
        }
      }
      this.title = "新增大模型问题设置参数";
    },
    /** 提交按钮 */
    async submitForm() {
      // 校验主表
      await this.$refs["formRef"].validate();
      this.formLoading = true;
      try {
        const data = this.formData;
        // 修改的提交
        if (data.id) {
          await QuestionParamSettingApi.updateQuestionParamSetting(data);
          this.$modal.msgSuccess("修改成功");
          this.dialogVisible = false;
          this.$emit('success');
          return;
        }
        // 添加的提交
        await QuestionParamSettingApi.createQuestionParamSetting(data);
        this.$modal.msgSuccess("新增成功");
        this.dialogVisible = false;
        this.$emit('success');
      } finally {
        this.formLoading = false;
      }
    },
    /** 表单重置 */
    reset() {
      this.formData = {
        id: undefined,
        templateId: undefined,
        settingKey: undefined,
        settingName: undefined,
        settingValue: undefined,
        sort: undefined,
      };
      this.resetForm("formRef");
    }
  }
};
</script>
src/views/ai/questionparamsetting/index.vue
对比新文件
@@ -0,0 +1,148 @@
<template>
  <div class="app-container">
    <!-- 搜索工作栏 -->
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch"
             label-width="68px">
      <el-form-item label="问题模板id" prop="templateId">
        <el-input v-model="queryParams.templateId" placeholder="请输入问题模板id" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="key" prop="settingKey">
        <el-input v-model="queryParams.settingKey" placeholder="请输入key" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="参数名称" prop="settingName">
        <el-input v-model="queryParams.settingName" placeholder="请输入参数名称" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="参数默认值" prop="settingValue">
        <el-input v-model="queryParams.settingValue" placeholder="请输入参数默认值" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="排序" prop="sort">
        <el-input v-model="queryParams.sort" placeholder="请输入排序" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- 操作工具栏 -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
                   v-hasPermi="['ai:question-param-setting:create']">新增
        </el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
      <el-table-column label="id" align="center" prop="id"/>
      <el-table-column label="问题模板id" align="center" prop="templateId"/>
      <el-table-column label="key" align="center" prop="settingKey"/>
      <el-table-column label="参数名称" align="center" prop="settingName"/>
      <el-table-column label="参数默认值" align="center" prop="settingValue"/>
      <el-table-column label="排序" align="center" prop="sort"/>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template v-slot="scope">
          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
                     v-hasPermi="['ai:question-param-setting:update']">修改
          </el-button>
          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
                     v-hasPermi="['ai:question-param-setting:delete']">删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页组件 -->
    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo"
                :limit.sync="queryParams.pageSize"
                @pagination="getList"/>
    <!-- 对话框(添加 / 修改) -->
    <QuestionParamSettingForm ref="formRef" @success="getList"/>
  </div>
</template>
<script>
import * as QuestionParamSettingApi from '@/api/ai/questionparamsetting';
import QuestionParamSettingForm from './QuestionParamSettingForm.vue';
export default {
  name: "QuestionParamSetting",
  components: {
    QuestionParamSettingForm,
  },
  data() {
    return {
      // 遮罩层
      loading: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 大模型问题设置参数列表
      list: [],
      // 是否展开,默认全部展开
      isExpandAll: true,
      // 重新渲染表格状态
      refreshTable: true,
      // 选中行
      currentRow: {},
      // 查询参数
      queryParams: {
        pageNo: 1,
        pageSize: 10,
        templateId: null,
        settingKey: null,
        settingName: null,
        settingValue: null,
        sort: null,
      },
    };
  },
  created() {
    this.getList();
  },
  methods: {
    /** 查询列表 */
    async getList() {
      try {
        this.loading = true;
        const res = await QuestionParamSettingApi.getQuestionParamSettingPage(this.queryParams);
        this.list = res.data.list;
        this.total = res.data.total;
      } finally {
        this.loading = false;
      }
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNo = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    /** 添加/修改操作 */
    openForm(id) {
      this.$refs["formRef"].open(id);
    },
    /** 删除按钮操作 */
    async handleDelete(row) {
      const id = row.id;
      await this.$modal.confirm('是否确认删除大模型问题设置参数编号为"' + id + '"的数据项?')
      try {
        await QuestionParamSettingApi.deleteQuestionParamSetting(id);
        await this.getList();
        this.$modal.msgSuccess("删除成功");
      } catch {
      }
    },
  }
};
</script>
src/views/ai/questiontemplate/QuestionTemplateForm.vue
对比新文件
@@ -0,0 +1,148 @@
<template>
  <div class="app-container">
    <!-- 对话框(添加 / 修改) -->
    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag
               append-to-body>
      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading"
               label-width="100px">
        <el-form-item label="模型id" prop="modelId">
          <el-input v-model="formData.modelId" placeholder="请输入模型id"/>
        </el-form-item>
        <el-form-item label="问题编号" prop="questionCode">
          <el-input v-model="formData.questionCode" placeholder="请输入问题编号"/>
        </el-form-item>
        <el-form-item label="问题名称" prop="questionName">
          <el-input v-model="formData.questionName" placeholder="请输入问题名称"/>
        </el-form-item>
        <el-form-item label="问题内容">
          <Editor v-model="formData.questionContent" :min-height="192"/>
        </el-form-item>
        <el-form-item label="输入个数" prop="dataLength">
          <el-input v-model="formData.dataLength" placeholder="请输入输入个数"/>
        </el-form-item>
        <el-form-item label="是否启用(0禁用 1启用)" prop="isEnable">
          <el-input v-model="formData.isEnable" placeholder="请输入是否启用(0禁用 1启用)"/>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formData.remark" placeholder="请输入备注"/>
        </el-form-item>
        <el-form-item label="创建时间" prop="createDate">
          <el-date-picker clearable v-model="formData.createDate" type="date"
                          value-format="timestamp" placeholder="选择创建时间"/>
        </el-form-item>
        <el-form-item label="更新者" prop="updator">
          <el-input v-model="formData.updator" placeholder="请输入更新者"/>
        </el-form-item>
        <el-form-item label="更新时间" prop="updateDate">
          <el-date-picker clearable v-model="formData.updateDate" type="date"
                          value-format="timestamp" placeholder="选择更新时间"/>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
        <el-button @click="dialogVisible = false">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import * as QuestionTemplateApi from '@/api/ai/questiontemplate';
import Editor from '@/components/Editor';
export default {
  name: "QuestionTemplateForm",
  components: {
    Editor,
  },
  data() {
    return {
      // 弹出层标题
      dialogTitle: "",
      // 是否显示弹出层
      dialogVisible: false,
      // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
      formLoading: false,
      // 表单参数
      formData: {
        id: undefined,
        modelId: undefined,
        questionCode: undefined,
        questionName: undefined,
        questionContent: undefined,
        dataLength: undefined,
        isEnable: undefined,
        remark: undefined,
        createDate: undefined,
        updator: undefined,
        updateDate: undefined,
      },
      // 表单校验
      formRules: {
        modelId: [{required: true, message: '模型id不能为空', trigger: 'blur'}],
        questionCode: [{required: true, message: '问题编号不能为空', trigger: 'blur'}],
      },
    };
  },
  methods: {
    /** 打开弹窗 */
    async open(id) {
      this.dialogVisible = true;
      this.reset();
      // 修改时,设置数据
      if (id) {
        this.formLoading = true;
        try {
          const res = await QuestionTemplateApi.getQuestionTemplate(id);
          this.formData = res.data;
          this.title = "修改大模型问题模板";
        } finally {
          this.formLoading = false;
        }
      }
      this.title = "新增大模型问题模板";
    },
    /** 提交按钮 */
    async submitForm() {
      // 校验主表
      await this.$refs["formRef"].validate();
      this.formLoading = true;
      try {
        const data = this.formData;
        // 修改的提交
        if (data.id) {
          await QuestionTemplateApi.updateQuestionTemplate(data);
          this.$modal.msgSuccess("修改成功");
          this.dialogVisible = false;
          this.$emit('success');
          return;
        }
        // 添加的提交
        await QuestionTemplateApi.createQuestionTemplate(data);
        this.$modal.msgSuccess("新增成功");
        this.dialogVisible = false;
        this.$emit('success');
      } finally {
        this.formLoading = false;
      }
    },
    /** 表单重置 */
    reset() {
      this.formData = {
        id: undefined,
        modelId: undefined,
        questionCode: undefined,
        questionName: undefined,
        questionContent: undefined,
        dataLength: undefined,
        isEnable: undefined,
        remark: undefined,
        createDate: undefined,
        updator: undefined,
        updateDate: undefined,
      };
      this.resetForm("formRef");
    }
  }
};
</script>
src/views/ai/questiontemplate/index.vue
对比新文件
@@ -0,0 +1,186 @@
<template>
  <div class="app-container">
    <!-- 搜索工作栏 -->
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch"
             label-width="68px">
      <el-form-item label="模型id" prop="modelId">
        <el-input v-model="queryParams.modelId" placeholder="请输入模型id" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="问题编号" prop="questionCode">
        <el-input v-model="queryParams.questionCode" placeholder="请输入问题编号" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="问题名称" prop="questionName">
        <el-input v-model="queryParams.questionName" placeholder="请输入问题名称" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="输入个数" prop="dataLength">
        <el-input v-model="queryParams.dataLength" placeholder="请输入输入个数" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="是否启用(0禁用 1启用)" prop="isEnable">
        <el-input v-model="queryParams.isEnable" placeholder="请输入是否启用(0禁用 1启用)" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input v-model="queryParams.remark" placeholder="请输入备注" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="创建时间" prop="createDate">
        <el-date-picker v-model="queryParams.createDate" style="width: 240px"
                        value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
                        :default-time="['00:00:00', '23:59:59']"/>
      </el-form-item>
      <el-form-item label="更新者" prop="updator">
        <el-input v-model="queryParams.updator" placeholder="请输入更新者" clearable
                  @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="更新时间" prop="updateDate">
        <el-date-picker v-model="queryParams.updateDate" style="width: 240px"
                        value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
                        :default-time="['00:00:00', '23:59:59']"/>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- 操作工具栏 -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
                   v-hasPermi="['ai:question-template:create']">新增
        </el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
      <el-table-column label="id" align="center" prop="id"/>
      <el-table-column label="模型id" align="center" prop="modelId"/>
      <el-table-column label="问题编号" align="center" prop="questionCode"/>
      <el-table-column label="问题名称" align="center" prop="questionName"/>
      <el-table-column label="问题内容" align="center" prop="questionContent"/>
      <el-table-column label="输入个数" align="center" prop="dataLength"/>
      <el-table-column label="是否启用(0禁用 1启用)" align="center" prop="isEnable"/>
      <el-table-column label="备注" align="center" prop="remark"/>
      <el-table-column label="创建时间" align="center" prop="createDate" width="180">
        <template v-slot="scope">
          <span>{{ parseTime(scope.row.createDate) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="更新者" align="center" prop="updator"/>
      <el-table-column label="更新时间" align="center" prop="updateDate" width="180">
        <template v-slot="scope">
          <span>{{ parseTime(scope.row.updateDate) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template v-slot="scope">
          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
                     v-hasPermi="['ai:question-template:update']">修改
          </el-button>
          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
                     v-hasPermi="['ai:question-template:delete']">删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页组件 -->
    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo"
                :limit.sync="queryParams.pageSize"
                @pagination="getList"/>
    <!-- 对话框(添加 / 修改) -->
    <QuestionTemplateForm ref="formRef" @success="getList"/>
  </div>
</template>
<script>
import * as QuestionTemplateApi from '@/api/ai/questiontemplate';
import QuestionTemplateForm from './QuestionTemplateForm.vue';
export default {
  name: "QuestionTemplate",
  components: {
    QuestionTemplateForm,
  },
  data() {
    return {
      // 遮罩层
      loading: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 大模型问题模板列表
      list: [],
      // 是否展开,默认全部展开
      isExpandAll: true,
      // 重新渲染表格状态
      refreshTable: true,
      // 选中行
      currentRow: {},
      // 查询参数
      queryParams: {
        pageNo: 1,
        pageSize: 10,
        modelId: null,
        questionCode: null,
        questionName: null,
        questionContent: null,
        dataLength: null,
        isEnable: null,
        remark: null,
        createDate: [],
        updator: null,
        updateDate: [],
      },
    };
  },
  created() {
    this.getList();
  },
  methods: {
    /** 查询列表 */
    async getList() {
      try {
        this.loading = true;
        const res = await QuestionTemplateApi.getQuestionTemplatePage(this.queryParams);
        this.list = res.data.list;
        this.total = res.data.total;
      } finally {
        this.loading = false;
      }
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNo = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    /** 添加/修改操作 */
    openForm(id) {
      this.$refs["formRef"].open(id);
    },
    /** 删除按钮操作 */
    async handleDelete(row) {
      const id = row.id;
      await this.$modal.confirm('是否确认删除大模型问题模板编号为"' + id + '"的数据项?')
      try {
        await QuestionTemplateApi.deleteQuestionTemplate(id);
        await this.getList();
        this.$modal.msgSuccess("删除成功");
      } catch {
      }
    },
  }
};
</script>