From 124f894f4e08fd63eae8c7a85babbc19f2cc1829 Mon Sep 17 00:00:00 2001 From: 潘志宝 <979469083@qq.com> Date: 星期五, 13 六月 2025 09:39:36 +0800 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- src/views/model/sche/suggest/suggestOperationRecord.vue | 18 + src/views/ai/dashboard/components/message/HistoryMessageList.vue | 33 ++ src/views/ai/model/template/index.vue | 4 src/views/ai/dashboard/components/conversation/CommonConversationList.vue | 116 +++++++--- src/views/ai/dashboard/components/message/MessageList.vue | 16 + src/api/ai/chat/message/index.ts | 50 --- src/views/ai/dashboard/components/message/HistoryMessageDialog.vue | 187 +++++++++++++++- src/views/model/sche/suggest/suggestSnapshot.vue | 12 src/components/Dialog/src/DialogHistory.vue | 12 - src/views/ai/dashboard/zhuanlu/index.vue | 58 +++- src/views/ai/dashboard/components/conversation/CommonConversation.vue | 130 +++++++---- 11 files changed, 443 insertions(+), 193 deletions(-) diff --git a/src/api/ai/chat/message/index.ts b/src/api/ai/chat/message/index.ts index 1123524..9ec9dcd 100644 --- a/src/api/ai/chat/message/index.ts +++ b/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}` }) }, diff --git a/src/components/Dialog/src/DialogHistory.vue b/src/components/Dialog/src/DialogHistory.vue index 0b3dd5a..02dc867 100644 --- a/src/components/Dialog/src/DialogHistory.vue +++ b/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; diff --git a/src/views/ai/dashboard/components/conversation/CommonConversation.vue b/src/views/ai/dashboard/components/conversation/CommonConversation.vue index 2de4e77..04137e7 100644 --- a/src/views/ai/dashboard/components/conversation/CommonConversation.vue +++ b/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); diff --git a/src/views/ai/dashboard/components/conversation/CommonConversationList.vue b/src/views/ai/dashboard/components/conversation/CommonConversationList.vue index df1c641..590e88c 100644 --- a/src/views/ai/dashboard/components/conversation/CommonConversationList.vue +++ b/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%; diff --git a/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue b/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue index 68b1a73..51999c2 100644 --- a/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue +++ b/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> diff --git a/src/views/ai/dashboard/components/message/HistoryMessageList.vue b/src/views/ai/dashboard/components/message/HistoryMessageList.vue index 91371e1..b4ebf44 100644 --- a/src/views/ai/dashboard/components/message/HistoryMessageList.vue +++ b/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> diff --git a/src/views/ai/dashboard/components/message/MessageList.vue b/src/views/ai/dashboard/components/message/MessageList.vue index 616b9f4..5ecf1bc 100644 --- a/src/views/ai/dashboard/components/message/MessageList.vue +++ b/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> diff --git a/src/views/ai/dashboard/zhuanlu/index.vue b/src/views/ai/dashboard/zhuanlu/index.vue index 0d0be46..9feb5d0 100644 --- a/src/views/ai/dashboard/zhuanlu/index.vue +++ b/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); diff --git a/src/views/ai/model/template/index.vue b/src/views/ai/model/template/index.vue index fa84bdb..b6de43b 100644 --- a/src/views/ai/model/template/index.vue +++ b/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"; diff --git a/src/views/model/sche/suggest/suggestOperationRecord.vue b/src/views/model/sche/suggest/suggestOperationRecord.vue index b0ff44b..c66c2aa 100644 --- a/src/views/model/sche/suggest/suggestOperationRecord.vue +++ b/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'}) diff --git a/src/views/model/sche/suggest/suggestSnapshot.vue b/src/views/model/sche/suggest/suggestSnapshot.vue index fd8272f..801a654 100644 --- a/src/views/model/sche/suggest/suggestSnapshot.vue +++ b/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 -- Gitblit v1.9.3