已修改12个文件
693 ■■■■■ 文件已修改
src/api/ai/chat/message/index.ts 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/src/DialogHistory.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/CommonConversation.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/conversation/CommonConversationList.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/HistoryMessageDialog.vue 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/HistoryMessageList.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/message/MessageList.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/zhuanlu/index.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/model/template/index.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/pre/analysis/index.vue 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/suggest/suggestOperationRecord.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/suggest/suggestSnapshot.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ai/chat/message/index.ts
@@ -2,7 +2,6 @@
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 {
@@ -36,6 +35,14 @@
  getChatMessageListByConversationId: async (conversationId: number | null) => {
    return await request.get({
      url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`
    })
  },
  // 消息列表
  getChatMessagePageListByConversationId: async (params: number | null) => {
    return await request.get({
      url: `/ai/chat/message/page-list-by-conversation-id`,
      params: params
    })
  },
@@ -76,35 +83,6 @@
      signal: ctrl.signal
    })
  },
  // 发送 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
  //   })
  // },
  // 删除消息
  deleteChatMessage: async (id: string) => {
@@ -115,18 +93,6 @@
  deleteByConversationId: async (conversationId: number) => {
    return await request.delete({
      url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`
    })
  },
  // // 删除消息【工业大模型专用】
  // deleteEnergyChatMessage: async (id: string) => {
  //   return await request.delete({ url: `/ai/chat/message/delete-energy?id=${id}` })
  // },
  // 删除指定对话的消息【工业大模型专用】
  deleteEnergyByConversationId: async (conversationId: number) => {
    return await request.delete({
      url: `/ai/chat/message/delete-energy-by-conversation-id?conversationId=${conversationId}`
    })
  },
src/components/Dialog/src/DialogHistory.vue
@@ -73,14 +73,6 @@
          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)"
@@ -101,9 +93,8 @@
<style lang="scss">
.history-dialog {
  height: 90vh;
  margin-top: 30px;
  color: #73C4FF;
  overflow: hidden; /* 防止内容溢出 */
  margin-top: 30px;
  background: rgba(3,29,76,0.79);
  border-radius: 4px 4px 4px 4px;
  border: 1px solid;
@@ -115,7 +106,6 @@
  .#{$elNamespace}-dialog {
    margin: 0 !important;
    &__header {
      height: 40px;
      padding: 0;
src/views/ai/dashboard/components/conversation/CommonConversation.vue
@@ -18,6 +18,9 @@
      ref="sidebarRef">
      <ConversationList
        :active-id="activeConversationId"
        :quick-access="quickAccessFlag"
        :model-name="modelName"
        :default-message="defaultMessage"
        ref="conversationListRef"
        @on-conversation-create="handleConversationCreateSuccess"
        @on-conversation-click="handleConversationClick"
@@ -55,9 +58,8 @@
          <!-- 情况一:消息加载中 -->
          <MessageLoading v-if="activeMessageListLoading" />
          <!-- 情况二:无聊天对话时 -->
          <MessageNewConversation
          <MessageListEmpty
            v-if="!activeConversation"
            @on-new-conversation="handleConversationCreate"
          />
          <!-- 情况三:消息列表为空 -->
          <MessageListEmpty
@@ -89,7 +91,7 @@
              @input="handlePromptInput"
              @compositionstart="onCompositionstart"
              @compositionend="onCompositionend"
              placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
              placeholder="请问我问题...(Shift+Enter 换行,按下 Enter 发送)"
            ></textarea>
            <div class="prompt-btns">
              <div class="content">
@@ -141,13 +143,21 @@
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";
import {formatToDateTime} from "@/utils/dateUtil";
import {ElLoading} from "element-plus";
/** AI 聊天对话 列表 */
defineOptions({ name: 'NormalConversation' })
const props = defineProps({
  data: {
    type: Object,
    default: () => null
  }
})
const route = useRoute() // 路由
const message = useMessage() // 消息弹窗
@@ -175,8 +185,12 @@
  isCollapsed.value = !isCollapsed.value
}
const modelName = ref<string>('common') // 对话搜索
// 聊天对话
const conversationListRef = ref()
const quickAccessFlag = ref(false)
const defaultMessage = ref<ChatMessageVO>()
const activeConversationId = ref<number | null>(null) // 选中的对话编号
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
@@ -199,7 +213,6 @@
// 接收 Stream 消息
const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('')
// =========== 【聊天对话】相关 ===========
@@ -298,7 +311,6 @@
    activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
      activeConversationId.value
    )
    // 滚动到最下面
    await nextTick()
    await scrollToBottom()
@@ -335,19 +347,37 @@
  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]
// //处理调度推理结论(deepSeek)
// 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]
//     }
//   })
// }
//处理调度推理结论(微调大模型)
const dealResult = (messages: any) => {
  messages.forEach((message) => {
    if(message.type === 'assistant') {
      const spliceText = message.content.includes("总结:") ? "总结:" : "结论:";
      // 创建同时捕获前后内容的正则表达式
      const regex = new RegExp(`^([\\s\\S]*?)${spliceText}([\\s\\S]*)$`);
      const match = message.content.match(regex);
      if(match) {
        message.thinking = match[1];
        message.conclusion = match[2]
      } else {
        message.thinking = message.content
      }
    }
  })
}
@@ -455,19 +485,20 @@
    message.error('发送失败,原因:内容为空!')
    return
  }
  // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
  authUtil.setToken(await refreshToken())
  if (activeConversationId.value == null) {
    message.error('还没创建对话,不能发送!')
    return
    await conversationListRef.value.createConversation(props.data?formatToDateTime(new Date(props.data.createTime)):null)
  }
  // 清空输入框
  prompt.value = ''
  // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
  authUtil.setToken(await refreshToken())
  // 执行发送
  await doSendMessageStream({
    conversationId: activeConversationId.value,
    content: content
  } as ChatMessageVO)
  setTimeout(() => {
    // 执行发送
    doSendMessageStream({
      conversationId: activeConversationId.value,
      content: content
    } as ChatMessageVO)
  }, 400)
}
/** 真正执行【发送】消息操作 */
@@ -478,7 +509,6 @@
  conversationInProgress.value = true
  // 设置为空
  receiveMessageFullText.value = ''
  try {
    // 1.1 先添加两个假数据,等 stream 返回再替换
    activeMessageList.value.push({
@@ -499,8 +529,7 @@
    await nextTick()
    await scrollToBottom() // 底部
    // 1.3 开始滚动
    textRoll()
    await textRoll()
    // 2. 发送 event stream
    let isFirstChunk = true // 是否是第一个 chunk 消息段
    await ChatMessageApi.sendChatMessageStream(
@@ -542,7 +571,9 @@
        stopStream()
      }
    )
  } catch {}
  } catch {
    console.log('sendStream Exception')
  }
}
/** 停止 stream 流式调用 */
@@ -632,16 +663,19 @@
/** 初始化 **/
onMounted(async () => {
  // 如果有 conversationId 参数,则默认选中
  if (route.query.conversationId) {
    const id = route.query.conversationId as unknown as number
    activeConversationId.value = id
    await getConversation(id)
  defaultMessage.value = props.data
  if(defaultMessage.value) {
    prompt.value = defaultMessage.value.content
    quickAccessFlag.value = true
  } else {
    // 获取列表数据
    activeMessageListLoading.value = true
    await getMessageList()
  }
})
  // 获取列表数据
  activeMessageListLoading.value = true
  await getMessageList()
onUnmounted(() => {
  stopStream()
})
</script>
@@ -655,7 +689,7 @@
.sidebar-toggle {
  position: absolute;
  left: 300px;  // 初始展开位置
  left: 320px;  // 初始展开位置
  top: 40%;
  z-index: 1000;
  width: 20px;
@@ -682,12 +716,12 @@
  left: 0;
  top: 0;
  bottom: 0;
  width: 300px;
  width: 320px;
  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;
  overflow-x: hidden;
  &.collapsed {
    transform: translateX(-100%);
@@ -699,7 +733,7 @@
// 头部
.detail-container {
  width: 100%;
  height: 885px;
  height: 910px;
  margin-left: 5px;
  background-color: rgba(0, 0, 0, 0); /* 透明背景 */
  transition: margin 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -796,7 +830,7 @@
.footer-container {
  display: flex;
  flex-direction: column;
  height: 114px;
  height: 205px;
  margin-left: 10px;
  padding: 0;
@@ -831,7 +865,7 @@
      flex-direction: column;
      padding: 9px 10px;
      width: 876px;
      height: 114px;
      height: 205px;
      background: rgba(115,196,255,0.05);
      border-radius: 4px 4px 4px 4px;
      border: 1px solid #73C4FF;
@@ -842,8 +876,8 @@
    }
    .prompt-input {
      width: 876px;
      height: 113.55px;
      width: 860px;
      height: 203px;
      font-weight: 400;
      font-size: 14px;
      background-color: rgba(219,238,255,0);
src/views/ai/dashboard/components/conversation/CommonConversationList.vue
@@ -1,8 +1,8 @@
<!--  AI 对话  -->
<template>
  <el-aside width="260px" class="conversation-container h-100%">
  <el-aside width="280px" class="conversation-container h-100%">
    <!-- 左顶部:对话 -->
    <div class="h-100%">
    <div class="h-80%">
      <div class="conversation-title">
        <img
          src="@/assets/ai/zhuanlu/conversation_big.png"
@@ -12,7 +12,7 @@
        对话列表
      </div>
<!--      <hr class="line"/>-->
      <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
      <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createNewConversation">
        <img
          src="@/assets/ai/zhuanlu/conversation_big.png"
          class="mr-8px w-[1.5em] h-[1.5em]"
@@ -82,8 +82,6 @@
            </div>
          </div>
        </div>
        <!-- 底部占位  -->
        <div class="h-160px w-100%"></div>
      </div>
    </div>
@@ -102,12 +100,13 @@
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { Bottom, Top } from '@element-plus/icons-vue'
import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png'
import {ChatMessageVO} from "@/api/ai/chat/message";
import {formatToDate, formatToDateTime} from "@/utils/dateUtil";
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[]) // 对话列表
@@ -120,7 +119,16 @@
  activeId: {
    type: String || null,
    required: true
  }
  },
  modelName: {
    type: String || null,
    required: true
  },
  quickAccess: {
    type: Boolean || null,
    required: true
  },
  defaultMessage: {}
})
// 定义钩子
@@ -169,10 +177,7 @@
    }, 50)
    // 1.1 获取 对话数据
    conversationList.value = await ChatConversationApi.getChatConversationEnergyList(modelName.value)
    if(conversationList.value.length == 0) {
      await createConversation()
    }
    conversationList.value = await ChatConversationApi.getChatConversationEnergyList(props.modelName)
    // 1.2 排序
    conversationList.value.sort((a, b) => {
      return b.createTime - a.createTime
@@ -184,7 +189,7 @@
      return
    }
    // 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前)
    // 2. 对话根据时间分组(置顶、今天、昨天、三天前、七天前、30 天前)
    conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
  } finally {
    // 清理定时器
@@ -203,7 +208,7 @@
  const groupMap = {
    置顶: [],
    今天: [],
    一天前: [],
    昨天: [],
    三天前: [],
    七天前: [],
    三十天前: []
@@ -212,9 +217,11 @@
  const now = Date.now()
  // 定义时间间隔常量(单位:毫秒)
  const oneDay = 24 * 60 * 60 * 1000
  const threeDays = 3 * oneDay
  const sevenDays = 7 * oneDay
  const thirtyDays = 30 * oneDay
  //今天
  const today = formatToDate(new Date())
  const yesterday = formatToDate(new Date().setDate(new Date().getDate() - 1))
  for (const conversation of list) {
    // 置顶
    if (conversation.pinned) {
@@ -223,11 +230,13 @@
    }
    // 计算时间差(单位:毫秒)
    const diff = now - conversation.createTime
    let conversationDate = formatToDate(conversation.createTime)
    let titleDate = conversation.title.split(' ')[0]
    // 根据时间间隔判断
    if (diff < oneDay) {
    if (titleDate == today) {
      groupMap['今天'].push(conversation)
    } else if (diff < threeDays) {
      groupMap['一天前'].push(conversation)
    } else if (titleDate == yesterday) {
      groupMap['昨天'].push(conversation)
    } else if (diff < sevenDays) {
      groupMap['三天前'].push(conversation)
    } else if (diff < thirtyDays) {
@@ -240,17 +249,22 @@
}
/** 新建对话 */
const createConversation = async () => {
const createNewConversation = async () => {
  await createConversation(null)
}
/** 新建对话 */
const createConversation = async (title: String) => {
  // 1. 新建对话
  const conversationId = await ChatConversationApi.createChatConversationEnergy(
    {modelName: modelName.value} as unknown as ChatConversationVO
    {modelName: props.modelName, title: title} as unknown as ChatConversationVO
  )
  // 2. 获取对话内容
  await getChatConversationList()
  // 3. 选中对话
  await handleConversationClick(conversationId)
  // 4. 回调
  emits('onConversationCreate')
  // // 4. 回调
  // emits('onConversationCreate')
}
/** 修改对话的标题 */
@@ -344,15 +358,28 @@
onMounted(async () => {
  // 获取 对话列表
  await getChatConversationList()
  // 默认选中
  if (props.activeId) {
    activeConversationId.value = props.activeId
  } else {
    // 首次默认选中第一个
  if(!props.quickAccess) {
    // 默认选中
    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])
      }
    }
  } else if(props.defaultMessage) {
    let tempTitle = formatToDateTime(new Date(props.defaultMessage.createTime))
    if (conversationList.value.length) {
      activeConversationId.value = conversationList.value[0].id
      // 回调 onConversationClick
      await emits('onConversationClick', conversationList.value[0])
      conversationList.value.forEach((item) => {
        if(item.title === tempTitle) {
          activeConversationId.value = item.id
          // 回调 onConversationClick
          emits('onConversationClick', item)
        }
      })
    }
  }
})
@@ -395,16 +422,30 @@
  }
  .conversation-list {
    overflow: auto;
    overflow-y: auto;
    height: 100%;
    margin-top: 10px;
    /* 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); }
    }
    .classify-title {
      padding-top: 10px;
      b {
        color: white;
      }
    }
    .conversation-item {
      margin-top: 5px;
    }
@@ -443,10 +484,9 @@
      }
      .title {
        padding: 2px 10px;
        padding: 2px 5px;
        max-width: 220px;
        font-size: 14px;
        font-weight: 400;
        font-size: 13px;
        color: rgba(0, 0, 0, 0.77);
        overflow: hidden;
        white-space: nowrap;
@@ -478,17 +518,19 @@
    }
  }
  // 角色仓库、清空未设置对话
  // 清空未设置对话
  .tool-box {
    bottom: 0;
    display: flex;
    padding: 0 20px;
    width: 90%;
    margin-left: 5%;
    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);
    border-radius: 2px;
    div {
      display: flex;
      margin-left: 20%;
src/views/ai/dashboard/components/message/HistoryMessageDialog.vue
@@ -1,17 +1,41 @@
<template>
  <DialogHistory title="历史建议" v-model="dialogVisible" width="1200">
    <!-- 左侧:对话列表 -->
    <ConversationList
      v-show="false"
      :active-id="activeConversationId"
      ref="conversationListRef"
    />
    <!-- 右侧:对话详情 -->
  <DialogHistory title="历史建议" v-model="dialogVisible" width="1200" custom-class="transparent-dialog">
    <!-- 搜索工作栏 -->
    <el-form
      class="-mb-15px query-area"
      :model="queryParams"
      ref="queryFormRef"
      :inline="true"
      label-width="68px"
    >
      <el-form-item label="对话时间" prop="createTime">
        <el-date-picker
          v-model="queryParams.createTime"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="datetimerange"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
          class="!w-360px transparent-date-picker-popper"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery">
          <Icon icon="ep:search" class="mr-5px"/>
          搜索
        </el-button>
        <el-button @click="resetQuery">
          <Icon icon="ep:refresh" class="mr-5px"/>
          重置
        </el-button>
      </el-form-item>
    </el-form>
    <!-- 对话详情 -->
    <el-container class="detail-container">
      <el-header class="header">
        <div class="title">
          {{ activeConversation?.title ? activeConversation?.title : '' }}
          <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
          <span v-if="total">({{ total }})</span>
        </div>
        <div class="btns" v-if="activeConversation">
          <el-button size="small" class="btn" @click="handlerMessageClear">
@@ -43,32 +67,55 @@
            ref="messageRef"
            :conversation="activeConversation"
            :list="activeMessageList"
            :gotoManualMethod="gotoManual"
          />
        </div>
      </el-main>
    </el-container>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="handleQuery"
    />
  </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'
import {formatDate} from "@vueuse/core";
import {ref} from "vue";
/** AI 聊天对话 列表 */
defineOptions({ name: 'HistoryMessageDialog' })
const route = useRoute() // 路由
// 接收父组件传递的方法
const props = defineProps({
  parentMethod: Function,
  gotoManualMethod: Function
});
// 定义发射事件
const emit = defineEmits(['gotoManualMethod'])
const message = useMessage() // 消息弹窗
const total = ref(0) // 历史建议列表
const dialogVisible = ref(false) // 弹窗的是否展示
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  createTime: [],
})
// 聊天对话
const conversationListRef = ref()
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
// 消息列表
@@ -77,14 +124,24 @@
/** 打开弹窗 */
const open = async (messages: ChatMessageVO[], conversation: ChatConversationVO) => {
const open = async (messages: ChatMessageVO[], conversation: ChatConversationVO, activeHistoryMessageTotal: number) => {
  dialogVisible.value = true
  total.value = activeHistoryMessageTotal
  await nextTick() // 等待弹窗DOM挂载
  activeMessageList.value = messages
  activeConversation.value = conversation
}
defineExpose({ open }) // 提供方法给 parent 调用
/** 处理查询时间段 */
const dealDate = async () => {
  const currentDate = new Date();
  const previousDate = new Date(currentDate.getTime() -  2 * 60 * 60 * 1000);
  queryParams.createTime[0] = formatDate(previousDate, 'YYYY-MM-DD HH:mm:ss');
  queryParams.createTime[1] = formatDate(currentDate, 'YYYY-MM-DD HH:mm:ss');
  return queryParams;
}
defineExpose({ open, dealDate }) // 提供方法给 parent 调用
/** 回到 message 列表的顶部 */
const handleGoTopMessage = () => {
@@ -111,19 +168,77 @@
  } catch {}
}
const gotoManual = async (item: ChatMessageVO) => {
  emit('gotoManualMethod', item) // 发送数据给父组件
}
/** 搜索按钮操作 */
const handleQuery = async () => {
  if (props.parentMethod) {
    // props.parentMethod({ data: queryParams }); // 可传递参数
    let pageResult = await props.parentMethod(queryParams)
    activeMessageList.value = pageResult.list
    total.value = pageResult.total
  }
}
/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  queryParams.pageNo = 1
  handleQuery()
}
/** 初始化 **/
onMounted(async () => {
  // await dealDate()
})
</script>
<style lang="scss" scoped>
.query-area {
  margin-top: 10px;
  float: right;
  :deep(.el-form-item__label) {
    color: #73C4FF;
  }
  :deep(.el-date-editor .el-icon) {
    color: #DBEEFF;
  }
  :deep(.el-date-editor .el-range-input) {
    color: rgba(219, 238, 255, 0.5);
  }
  /* 移除所有输入框边框 */
  :deep(.el-form-item .el-input__wrapper) {
    border: none !important;
    box-shadow: none !important;
    background: rgba(255,255,255,0.1) !important; /* 保留浅色背景 */
  }
  /* 所有状态通用透明背景 */
  :deep(.el-button) {
    background: transparent !important;
    border-color: currentColor; /* 保持与文字同色 */
    color: #409EFF; /* 蓝色文字 */
  }
  /* 悬停状态 */
  :deep(.el-button:hover) {
    background: rgba(0, 0, 0, 0.5) !important; /* 轻微悬停反馈 */
  }
  /* 点击状态 */
  :deep(.el-button:active) {
    background: rgba(0, 0, 0, 0.8) !important;
  }
}
// 头部
.detail-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 820px;
  height: 75vh;
  background-color: rgba(0, 0, 0, 0); /* 透明背景 */
  z-index: 1;
  .header {
@@ -165,7 +280,6 @@
      :deep(.el-button:active) {
        background: rgba(0, 0, 0, 0.1) !important;
      }
      /* 禁用状态 */
      :deep(.el-button.is-disabled) {
        opacity: 0.6;
@@ -370,6 +484,45 @@
        }
      }
    }
    /* 下拉组件 */
    :deep(.el-select) {
      /* 下拉箭头 */
      .el-select__caret {
        color: #73C4FF !important; /* 匹配图中的浅蓝箭头 */
        font-size: 16px !important;
      }
    }
    /* 深度选择器调整边框细节 */
    :deep(.el-select__wrapper) {
      background-color: transparent !important;
      border-radius: 6px;        /* 圆角大小 */
      border-width: 1.5px;        /* 边框粗细 */
      box-shadow: 0 0 0 1px #1E5A86 !important; /* 聚焦阴影 */
    }
  }
}
.el-pagination {
  //--el-pagination-button-bg-color: transparent;
  opacity: 0.6;
  :deep(.el-pagination__total) {
    color: white;
  }
  :deep(.el-pager) {
    color: rgba(3,27,21);
    font-weight: bold;
  }
  :deep(.el-pagination__jump) {
    color: white;
  }
  :deep(.el-select__popper) {
    background-color: transparent;
  }
  :deep(.el-scrollbar) {
    --el-scrollbar-opacity: 0.8;
    --el-scrollbar-bg-color: transparent;
  }
}
</style>
src/views/ai/dashboard/components/message/HistoryMessageList.vue
@@ -43,7 +43,7 @@
          <div>
            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
          </div>
          <div class="right-text-container">
          <div class="right-text-container question" @click="gotoManual(item)">
            <div class="right-text">{{ item.content }}</div>
          </div>
          <div class="right-btns">
@@ -71,15 +71,12 @@
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)
@@ -87,6 +84,7 @@
const userAvatar = computed(() => userAvatarDefaultImg)
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
// 定义 props
const props = defineProps({
@@ -97,7 +95,9 @@
  list: {
    type: Array as PropType<ChatMessageVO[]>,
    required: true
  }
  },
  gotoManualMethod: Function
})
const { list } = toRefs(props) // 消息列表
@@ -146,6 +146,12 @@
// ============ 处理消息操作 ==============
const gotoManual = async (item: ChatMessageVO) => {
  if(props.gotoManualMethod) {
    props.gotoManualMethod(item)
  }
}
/** 复制 */
const copyContent = async (content) => {
  await copy(content)
@@ -192,6 +198,11 @@
    flex-direction: column;
    text-align: left;
    margin: 0 15px;
    .question:hover {
      cursor: pointer;
      background: rgba(40, 139, 255, 0.3);
    }
    .time {
      text-align: left;
@@ -282,9 +293,15 @@
  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);
    background: rgba(255,255,255,0.1);
    border: solid 1px rgba(255,215,0,0.6);
    color: rgba(255,215,0,0.5);
  }
  .el-button:hover {
    cursor: pointer;
    background-color: rgba(255,255,255,0.4);
    border: solid 2px rgba(255,215,0);
    color: rgba(255,215,0);
  }
}
</style>
src/views/ai/dashboard/components/message/MessageList.vue
@@ -240,7 +240,7 @@
      overflow-wrap: break-word;
      background: rgba(115,196,255,0);
      border-radius: 4px 4px 4px 4px;
      padding: 20px 10px 5px 0;
      padding: 0 10px 0 0;
      .left-text {
        color: rgba(219,238,255,0.8);
        font-size: 1rem;
@@ -295,11 +295,23 @@
  }
}
// 回到底部
.to-bottom {
  position: absolute;
  z-index: 1000;
  bottom: 0;
  right: 50%;
  .el-button {
    background: rgba(255, 255, 255, 0.1);
    border: solid 1px rgba(255, 215, 0, 0.6);
    color: rgba(255, 215, 0, 0.5);
  }
  .el-button:hover {
    cursor: pointer;
    background-color: rgba(255, 255, 255, 0.4);
    border: solid 2px rgba(255, 215, 0);
    color: rgba(255, 215, 0);
  }
}
</style>
src/views/ai/dashboard/zhuanlu/index.vue
@@ -62,7 +62,7 @@
    <div class="gas-scheduling-center">
      <div class="mode-switch">
        <el-radio-group v-model="tabPosition" class="custom-radio-group">
        <el-radio-group v-model="tabPosition" @change="handleChange" class="custom-radio-group">
          <el-radio-button label="model">大模型模式</el-radio-button>
          <el-radio-button label="conversation">对话模式</el-radio-button>
        </el-radio-group>
@@ -164,12 +164,15 @@
        <!-- 历史建议 -->
        <HistoryMessageDialog
          ref="historyMessageRef"
          :conversation="activeConversation"
          :parentMethod="queryHistoryMessage"
          @gotoManualMethod="gotoManual"
        />
      </div>
      <div v-else>
        <NormalConversation />
        <NormalConversation
          :data="defaultMessage"
        />
      </div>
    </div>
@@ -265,6 +268,7 @@
import {round} from "lodash-es";
import {ArrowUpBold} from "@element-plus/icons-vue";
import * as authUtil from "@/utils/auth";
import HistoryMessageList from "@/views/ai/dashboard/components/message/HistoryMessageList.vue";
const mqhsList = ref([
  {
@@ -548,6 +552,7 @@
const messageRef = ref()
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表
const activeHistoryMessageList = ref<ChatMessageVO[]>([]) // 历史建议列表
const activeHistoryMessageTotal = ref(0) // 历史建议总数
const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中
const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中
// 消息滚动
@@ -573,8 +578,30 @@
const historyMessageRef = ref()
const openHistoryMessage = async () => {
  // 刷新 message 列表
  await getHistoryMessageList()
  historyMessageRef.value.open(activeHistoryMessageList.value, activeConversation.value)
  let resDate = await historyMessageRef.value.dealDate()
  await getHistoryMessageList(resDate)
  historyMessageRef.value.open(activeHistoryMessageList.value, activeConversation.value, activeHistoryMessageTotal.value)
}
const queryHistoryMessage = async (queryParams: ChatMessageVO) => {
  return await getHistoryMessageList(queryParams)
}
//切换对话模式判断
const handleChange = async () => {
  // 对话进行中,不允许切换
  if (conversationInProgress.value) {
    message.alert('对话中,不允许切换!')
    return false
  }
}
// 默认选中消息
const defaultMessage = ref<ChatMessageVO>()
const gotoManual = async (item: ChatMessageVO) => {
  defaultMessage.value = item
  tabPosition.value = 'conversation'
}
// =========== 【聊天对话】相关 ===========
@@ -671,22 +698,23 @@
}
/** 获取消息 message 列表 */
const getHistoryMessageList = async () => {
const getHistoryMessageList = async (params: any) => {
  if (activeConversationId.value === null) {
    return
  }
  params.conversationId = activeConversationId.value
  // 获取消息列表
  activeHistoryMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
    activeConversationId.value
  )
  if (activeHistoryMessageList.value.length > 0) {
  let pageResult = await ChatMessageApi.getChatMessagePageListByConversationId(params)
  activeHistoryMessageList.value = pageResult.list
  activeHistoryMessageTotal.value = pageResult.total
  if (activeHistoryMessageList.value != null && activeHistoryMessageList.value.length > 0) {
    activeHistoryMessageList.value.forEach((message: ChatMessageVO) => {
      if(message.type != 'user') {
        dealResult(message)
      }
    })
    return activeHistoryMessageList.value
  }
  return pageResult
}
//处理调度推理结论
const dealResult = (message: any) => {
@@ -709,7 +737,6 @@
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 则展示它
@@ -1143,11 +1170,11 @@
  let returnValue = 0;
  if(type == 'max') {
    returnValue = computed(() => {
      return Math.max(...tank) + 20
      return Number((Math.max(...tank) + 20).toFixed(0))
    })
  } else if(type == 'min') {
    returnValue = computed(() => {
      return Math.min(...tank) - 60
      return Number((Math.min(...tank) - 60).toFixed(0))
    })
  } else if(type == 'average') {
    returnValue = computed(() => {
@@ -1155,7 +1182,7 @@
      tank.forEach((item) => {
        sum += item[0]
      })
      return (sum / tank.length).toFixed(0);
      return Number((sum / tank.length).toFixed(0));
    })
  }
  return returnValue.value
@@ -1582,6 +1609,7 @@
// 清理监听
onUnmounted(() => {
  console.log('stopStream')
  const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
  events.forEach(event => {
    document.removeEventListener(event, handleFullscreenChange);
src/views/ai/model/template/index.vue
@@ -109,9 +109,7 @@
</template>
<script lang="ts" setup>
  import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
  import {dateFormatter} from '@/utils/formatTime'
  import download from '@/utils/download'
  import { DICT_TYPE } from '@/utils/dict'
  import * as AiQuestionTemplateApi from '@/api/ai/questiontemplate'
  import TemplateForm from './templateForm.vue'
  import * as AiModelApi from "@/api/ai/model/model";
src/views/model/pre/analysis/index.vue
@@ -379,7 +379,7 @@
  return data.label.includes(value)
}
// let xAxisData = []
let xAxisData = []
/** 查询列表 */
const getList = async (isClear = true) => {
@@ -407,26 +407,6 @@
    formData.value.predictTime = data.predictTime;
    formData.value.startTime = data.startTime
    formData.value.endTime = data.endTime
    // 默认影响时间
    changeInfluenceFactorTime(data.predictTime);
    // 获取影响因素结果列表
    influenceFactorResultList.value = await influenceFactorApi.getResultList({
      outIds: outIds,
      startTime: data.startTime,
      endTime: data.endTime
    })
    // 获取影响因素结果列表
    influenceFactorList.value = await influenceFactorApi.getListByOutId(formData.value.checkedItemData.id)
    if (influenceFactorList.value && influenceFactorList.value.length > 0) {
      // 根据factorOutputId去重,因为不同的统计规则会有重复的影响因素
      influenceFactorList.value = Array.from(new Map(influenceFactorList.value.map(item => [item.factorOutputId, item])).values());
      // 默认选中第一个影响因素
      influenceFactor.value = influenceFactorList.value?.[0]?.factorOutputId
      getInfluenceFactorChart(influenceFactorList.value?.[0]?.factorOutputId)
    }
    const paramsAlarm = reactive({
@@ -827,7 +807,6 @@
        symbol: ['circle', 'none'],
      },
    });
    itemDataObject.value = {}
    yAxisData.push({
      type: 'value',
      name: "累计值",
@@ -845,7 +824,6 @@
    })
    for (let i = 0; i < data.dataViewList.length; i++) {
      let dataView = data.dataViewList[i]
      itemDataObject.value[dataView.outId] = dataView;
      let maxValue = dataView.maxValue;
      let minValue = dataView.minValue;
      yAxisIndex = (formData.value.isMultipleY ? i : 0) + 1;
@@ -1053,7 +1031,7 @@
function changeInfluenceFactorTime(time) {
  if (time && new Date(time)?.getTime()) {
    influenceFactorResultTime.value = time
    influenceFactorResult.value = influenceFactorResultList.value?.[formData.value.checkedItemData?.id]?.filter(e => e.time === new Date(time).getTime()).sort((a, b) => b.value - a.value) || [];
    influenceFactorResult.value = influenceFactorResultList.value?.[calRateForm.value.calItem]?.filter(e => e.time === new Date(time).getTime()).sort((a, b) => b.value - a.value) || [];
  }
}// 选择影响因素
function changeInfluenceFactor(value) {
@@ -1143,7 +1121,7 @@
  if (checked.checkedNodes) {
    let cns = [...checked.checkedNodes]
    for (let i = 0; i < cns.length; i++) {
      if (cns[i].id.indexOf('-') !== -1) {
      if (cns[i].disabled) {
        continue
      }
      formData.value.checkedItemData.push(cns[i])
@@ -1163,7 +1141,7 @@
  }, wait)
}
function calItemBaseVale() {
const calItemBaseVale = async () => {
  if (!calRateForm.value.calItem) {
    calRateForm.value.itemPreMax = 0;
    calRateForm.value.itemPreMin = 0;
@@ -1190,11 +1168,30 @@
    calDeviation(dataView.cumulantRealData,dataView.cumulantPreData,'deviationCumulant')
    calAccuracyRate()
    // 影响因素
    // 获取影响因素结果列表
    influenceFactorList.value = influenceFactorApi.getListByOutId(formData.value.checkedItemData.id)
    // 根据factorOutputId去重,因为不同的统计规则会有重复的影响因素
    influenceFactorList.value = Array.from(new Map(influenceFactorList.value.map(item => [item.factorOutputId, item])).values());
    const outPutId = calRateForm.value.calItem
    influenceFactorResultList.value = await influenceFactorApi.getResultList({
      outIds: [outPutId],
      startTime: formData.value.startTime,
      endTime: formData.value.endTime
    })
    // 默认影响时间
    changeInfluenceFactorTime(formData.value.predictTime);
    // 获取影响因素列表
    influenceFactorList.value = await influenceFactorApi.getListByOutId(outPutId)
    if (influenceFactorList.value && influenceFactorList.value.length > 0) {
      // 根据factorOutputId去重,因为不同的统计规则会有重复的影响因素
      influenceFactorList.value = Array.from(new Map(influenceFactorList.value.map(item => [item.factorOutputId, item])).values());
      // 默认选中第一个影响因素
      influenceFactor.value = influenceFactorList.value?.[0]?.factorOutputId
      getInfluenceFactorChart(influenceFactorList.value?.[0]?.factorOutputId)
    }else {
      // 清除历史
      influenceFactor.value = undefined
      myInfluenceFactorChart.clear()
    }
  }
}
src/views/model/sche/suggest/suggestOperationRecord.vue
@@ -21,12 +21,13 @@
          label="结果code"
          header-align="center"
          align="left"
          min-width="150"
          min-width="50"
        />
        <el-table-column
          prop="resultData"
          label="结果数据"
          header-align="center"
          min-width="150"
          align="center"
        />
        <el-table-column
@@ -34,21 +35,29 @@
          label="操作"
          header-align="center"
          align="center"
          min-width="150"
          min-width="50"
        />
        <el-table-column
          prop="reason"
          label="原因"
          header-align="center"
          align="center"
          min-width="100"
        />
        <el-table-column
          prop="handler"
          label="处理人"
          header-align="center"
          align="center"
          min-width="150"
          min-width="100"
        />
        <el-table-column
          prop="handleTime"
          label="处理时间"
          :formatter="dateFormatter"
          header-align="center"
          align="center"
          min-width="150"
          min-width="100"
        />
      </el-table>
      <!-- 分页 -->
@@ -67,6 +76,7 @@
  import type {DrawerProps} from 'element-plus'
  import { getSuggestOperationRecordPage } from '@/api/model/sche/suggest/suggestOperationRecord';
  import SuggestSnapshot from './suggestSnapshot.vue'
  import {dateFormatter} from '@/utils/formatTime'
  import {ref} from "vue";
  defineOptions({name: 'SuggestOperationRecord'})
src/views/model/sche/suggest/suggestSnapshot.vue
@@ -16,10 +16,10 @@
    </el-checkbox-group>
    <div
      v-for="(chart, index) in charts"
      v-for="chart in charts"
      :key="chart.id"
      class="chart-container"
      :ref="el => chartDoms[index] = el"
      :ref="el => chartDoms[chart.id] = el"
      v-loading="loading"
    ></div>
  </el-dialog>
@@ -34,8 +34,8 @@
  const visible = ref(false)
  const dataList = ref([])
  const selectedData = ref([])
  const charts = ref([])
  const chartDoms = ref([])
  const charts = ref()
  const chartDoms = ref({})
  const chartInstances = ref([])
  const loading = ref(false)
@@ -106,10 +106,10 @@
  /** 渲染图表 */
  const renderCharts = () => {
    chartInstances.value = chartDoms.value.map((dom, index) => {
    chartInstances.value = charts.value.map((chartInfo, index) => {
      const dom = chartDoms.value[chartInfo.id]
      if (!dom) return null
      const chart = echarts.init(dom)
      const chartInfo = charts.value[index]
      if (!chartInfo) return chart