From e295922209fb87c6dcd68ea1560fd16c3e6d808c Mon Sep 17 00:00:00 2001 From: dongyukun <1208714201@qq.com> Date: 星期五, 27 六月 2025 09:36:51 +0800 Subject: [PATCH] Merge remote-tracking branch 'origin/feature/ai' --- src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue | 2 src/assets/ai/zhuanlu/history_title.png | 0 src/views/ai/utils/utils.ts | 17 src/views/ai/dashboard/components/conversation/ConversationUpdateForm.vue | 2 src/assets/ai/zhuanlu/suggest_title.png | 0 src/views/ai/suggest/index.vue | 223 ++++++++ src/views/ai/model/template/templateForm.vue | 3 src/components/Dialog/src/DialogSuggest.vue | 51 + src/views/ai/dashboard/components/conversation/CommonConversation.vue | 23 src/views/ai/suggest/ScheduleSuggestForm.vue | 117 ++++ src/views/ai/dashboard/components/suggest/ScheduleSuggestDialog.vue | 417 +++++++++++++++ src/views/ai/model/template/index.vue | 3 src/views/ai/dashboard/components/conversation/CommonConversationList.vue | 4 src/views/ai/dashboard/components/message/MessageList.vue | 2 src/views/ai/dashboard/components/message/ModelMessageList.vue | 22 src/api/ai/schedulesuggest/index.ts | 55 ++ src/views/ai/dashboard/components/message/HistoryMessageDialog.vue | 4 src/views/ai/utils/constants.ts | 12 src/api/ai/model/model/index.ts | 2 src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue | 2 src/components/Dialog/src/DialogAi.vue | 4 src/views/ai/dashboard/components/suggest/ScheduleSuggestList.vue | 307 +++++++++++ src/views/ai/dashboard/zhuanlu/index.vue | 338 ++++++++++-- 23 files changed, 1,477 insertions(+), 133 deletions(-) diff --git a/src/api/ai/model/model/index.ts b/src/api/ai/model/model/index.ts index 7c485a0..44c898a 100644 --- a/src/api/ai/model/model/index.ts +++ b/src/api/ai/model/model/index.ts @@ -23,7 +23,7 @@ }, // 获得模型列表 - getModelSimpleList: async (type?: number) => { + getModelSimpleList: async (type?: string) => { return await request.get({ url: `/ai/model/simple-list`, params: { diff --git a/src/api/ai/schedulesuggest/index.ts b/src/api/ai/schedulesuggest/index.ts new file mode 100644 index 0000000..19c5d1e --- /dev/null +++ b/src/api/ai/schedulesuggest/index.ts @@ -0,0 +1,55 @@ +import request from '@/config/axios' + +// 大模型调度建议 VO +export interface ScheduleSuggestVO { + id: number // id + modelId: number // 模型id + conversationId: number // 会话id + messageId: number // 消息id + content: string // 调度建议 + status: number // 状态(0-未处理 1-已采纳 2-已忽略) + createTime: Date // 创建时间 +} + +// 大模型调度建议 API +export const ScheduleSuggestApi = { + // 查询大模型调度建议分页 + getScheduleSuggestPage: async (params: any) => { + return await request.get({ url: `/ai/schedule-suggest/page`, params }) + }, + + // 查询大模型调度建议详情 + getScheduleSuggest: async (id: number) => { + return await request.get({ url: `/ai/schedule-suggest/get?id=` + id }) + }, + + // 查询大模型调度建议详情 + getTopScheduleSuggests: async (top: number) => { + return await request.get({ url: `/ai/schedule-suggest/simple-list?top=` + top }) + }, + + // 新增大模型调度建议 + createScheduleSuggest: async (data: ScheduleSuggestVO) => { + return await request.post({ url: `/ai/schedule-suggest/create`, data }) + }, + + // 修改大模型调度建议 + updateScheduleSuggest: async (data: ScheduleSuggestVO) => { + return await request.put({ url: `/ai/schedule-suggest/update`, data }) + }, + + // 采纳忽略取消采纳 + operateScheduleSuggest: async (data: ScheduleSuggestVO) => { + return await request.put({ url: `/ai/schedule-suggest/operate-suggest`, data}) + }, + + // 删除大模型调度建议 + deleteScheduleSuggest: async (id: number) => { + return await request.delete({ url: `/ai/schedule-suggest/delete?id=` + id }) + }, + + // 导出大模型调度建议 Excel + exportScheduleSuggest: async (params) => { + return await request.download({ url: `/ai/schedule-suggest/export-excel`, params }) + }, +} diff --git a/src/assets/ai/zhuanlu/history_title.png b/src/assets/ai/zhuanlu/history_title.png new file mode 100644 index 0000000..c0350b6 --- /dev/null +++ b/src/assets/ai/zhuanlu/history_title.png Binary files differ diff --git a/src/assets/ai/zhuanlu/suggest_title.png b/src/assets/ai/zhuanlu/suggest_title.png new file mode 100644 index 0000000..16c716e --- /dev/null +++ b/src/assets/ai/zhuanlu/suggest_title.png Binary files differ diff --git a/src/components/Dialog/src/DialogHistory.vue b/src/components/Dialog/src/DialogAi.vue similarity index 96% rename from src/components/Dialog/src/DialogHistory.vue rename to src/components/Dialog/src/DialogAi.vue index 02dc867..07abfcf 100644 --- a/src/components/Dialog/src/DialogHistory.vue +++ b/src/components/Dialog/src/DialogAi.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> import { propTypes } from '@/utils/propTypes' import { isNumber } from '@/utils/is' -defineOptions({ name: 'DialogHistory' }) +defineOptions({ name: 'DialogAi' }) const slots = useSlots() @@ -111,7 +111,7 @@ padding: 0; margin-right: 0 !important; background: - url("@/assets/ai/zhuanlu/common_title.png") left no-repeat, + url("@/assets/ai/zhuanlu/history_title.png") left no-repeat, linear-gradient(to bottom, #0a1633dd, #0a1633dd); /* 叠加深色遮罩 */ div { color: #73C4FF; diff --git a/src/components/Dialog/src/DialogHistory.vue b/src/components/Dialog/src/DialogSuggest.vue similarity index 71% copy from src/components/Dialog/src/DialogHistory.vue copy to src/components/Dialog/src/DialogSuggest.vue index 02dc867..6d1723d 100644 --- a/src/components/Dialog/src/DialogHistory.vue +++ b/src/components/Dialog/src/DialogSuggest.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> import { propTypes } from '@/utils/propTypes' import { isNumber } from '@/utils/is' -defineOptions({ name: 'DialogHistory' }) +defineOptions({ name: 'Dialog' }) const slots = useSlots() @@ -9,7 +9,7 @@ modelValue: propTypes.bool.def(false), title: propTypes.string.def('Dialog'), fullscreen: propTypes.bool.def(true), - width: propTypes.oneOfType([String, Number]).def('30%'), + width: propTypes.oneOfType([String, Number]).def('40%'), scroll: propTypes.bool.def(false), // 是否开启滚动条。如果是的话,按照 maxHeight 设置最大高度 maxHeight: propTypes.oneOfType([String, Number]).def('400px') }) @@ -50,40 +50,56 @@ } ) +const dialogStyle = computed(() => { + return { + height: unref(dialogHeight) + } +}) </script> <template> <ElDialog v-bind="getBindValue" - :fullscreen="isFullscreen" :close-on-click-modal="true" + :fullscreen="isFullscreen" :width="width" destroy-on-close lock-scroll draggable - class="history-dialog" + class="com-dialog" :show-close="false" > <template #header="{ close }"> - <div class="relative h-30px flex items-center justify-between pl-15px pr-15px"> + <div class="relative h-54px 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" + class="absolute right-15px top-[50%] h-54px 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="var(--el-color-info)" + 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" + color="var(--el-color-info)" @click="close" /> </div> </div> </template> - <slot></slot> + <ElScrollbar v-if="scroll" :style="dialogStyle"> + <slot></slot> + </ElScrollbar> + <slot v-else></slot> <template v-if="slots.footer" #footer> <slot name="footer"></slot> </template> @@ -91,10 +107,9 @@ </template> <style lang="scss"> -.history-dialog { - height: 90vh; +.com-dialog { + height: 62vh; color: #73C4FF; - margin-top: 30px; background: rgba(3,29,76,0.79); border-radius: 4px 4px 4px 4px; border: 1px solid; @@ -111,7 +126,7 @@ padding: 0; margin-right: 0 !important; background: - url("@/assets/ai/zhuanlu/common_title.png") left no-repeat, + url("@/assets/ai/zhuanlu/suggest_title.png") left no-repeat, linear-gradient(to bottom, #0a1633dd, #0a1633dd); /* 叠加深色遮罩 */ div { color: #73C4FF; @@ -123,6 +138,18 @@ color: #73C4FF; top: 0; } + + &__body { + padding: 15px !important; + } + + &__footer { + border-top: 1px solid var(--el-border-color); + } + + &__headerbtn { + top: 0; + } } } </style> diff --git a/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue b/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue index 90f68c6..bba2a10 100644 --- a/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue +++ b/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue @@ -110,7 +110,7 @@ } } // 获得下拉数据 - models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT) + models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT + "," + AiModelTypeEnum.LLM) } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 diff --git a/src/views/ai/dashboard/components/conversation/CommonConversation.vue b/src/views/ai/dashboard/components/conversation/CommonConversation.vue index 04137e7..5d41a42 100644 --- a/src/views/ai/dashboard/components/conversation/CommonConversation.vue +++ b/src/views/ai/dashboard/components/conversation/CommonConversation.vue @@ -147,7 +147,7 @@ import * as authUtil from "@/utils/auth"; import {refreshToken} from "@/api/login"; import {formatToDateTime} from "@/utils/dateUtil"; -import {ElLoading} from "element-plus"; +import { formatReasoningContent } from '@/views/ai/utils/utils' /** AI 聊天对话 列表 */ defineOptions({ name: 'NormalConversation' }) @@ -347,23 +347,6 @@ return [] }) -// //处理调度推理结论(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) => { @@ -378,6 +361,8 @@ } else { message.thinking = message.content } + // 处理推理思路内容 + message.thinking = formatReasoningContent(message.thinking); } }) } @@ -691,7 +676,7 @@ position: absolute; left: 320px; // 初始展开位置 top: 40%; - z-index: 1000; + z-index: 1; width: 20px; height: 80px; background: rgba(115, 196, 255, 0.5); diff --git a/src/views/ai/dashboard/components/conversation/CommonConversationList.vue b/src/views/ai/dashboard/components/conversation/CommonConversationList.vue index 590e88c..fc32a5a 100644 --- a/src/views/ai/dashboard/components/conversation/CommonConversationList.vue +++ b/src/views/ai/dashboard/components/conversation/CommonConversationList.vue @@ -128,7 +128,9 @@ type: Boolean || null, required: true }, - defaultMessage: {} + defaultMessage: { + type: Object as PropType<ChatMessageVO> + } }) // 定义钩子 diff --git a/src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue b/src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue index afa5279..5882bce 100644 --- a/src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue +++ b/src/views/ai/dashboard/components/conversation/CommonConversationUpdateForm.vue @@ -102,7 +102,7 @@ } } // 获得下拉数据 - models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT) + models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.LLM) } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 diff --git a/src/views/ai/dashboard/components/conversation/ConversationUpdateForm.vue b/src/views/ai/dashboard/components/conversation/ConversationUpdateForm.vue index 90f68c6..af25f2f 100644 --- a/src/views/ai/dashboard/components/conversation/ConversationUpdateForm.vue +++ b/src/views/ai/dashboard/components/conversation/ConversationUpdateForm.vue @@ -110,7 +110,7 @@ } } // 获得下拉数据 - models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT) + models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.LLM) } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 diff --git a/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue b/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue index 51999c2..2d58542 100644 --- a/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue +++ b/src/views/ai/dashboard/components/message/HistoryMessageDialog.vue @@ -1,5 +1,5 @@ <template> - <DialogHistory title="历史建议" v-model="dialogVisible" width="1200" custom-class="transparent-dialog"> + <DialogAi title="" v-model="dialogVisible" width="1200" custom-class="transparent-dialog"> <!-- 搜索工作栏 --> <el-form class="-mb-15px query-area" @@ -79,7 +79,7 @@ v-model:limit="queryParams.pageSize" @pagination="handleQuery" /> - </DialogHistory> + </DialogAi> </template> <script setup lang="ts"> diff --git a/src/views/ai/dashboard/components/message/MessageList.vue b/src/views/ai/dashboard/components/message/MessageList.vue index 5ecf1bc..7635873 100644 --- a/src/views/ai/dashboard/components/message/MessageList.vue +++ b/src/views/ai/dashboard/components/message/MessageList.vue @@ -134,14 +134,12 @@ /** 回到底部 */ 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 } diff --git a/src/views/ai/dashboard/components/message/ModelMessageList.vue b/src/views/ai/dashboard/components/message/ModelMessageList.vue index f46b93e..4728658 100644 --- a/src/views/ai/dashboard/components/message/ModelMessageList.vue +++ b/src/views/ai/dashboard/components/message/ModelMessageList.vue @@ -1,5 +1,5 @@ <template> - <div ref="messageContainer" class="h-100%"> + <div ref="messageContainer" class="h-100% 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'"> @@ -21,10 +21,9 @@ 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 { 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 到粘贴板 @@ -88,24 +87,9 @@ scrollContainer.scrollTop = 0 } -defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用 +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') -} /** 刷新 */ const onRefresh = async (message: ChatMessageVO) => { diff --git a/src/views/ai/dashboard/components/suggest/ScheduleSuggestDialog.vue b/src/views/ai/dashboard/components/suggest/ScheduleSuggestDialog.vue new file mode 100644 index 0000000..48224a0 --- /dev/null +++ b/src/views/ai/dashboard/components/suggest/ScheduleSuggestDialog.vue @@ -0,0 +1,417 @@ +<template> + <DialogSuggest title="" v-model="dialogVisible" width="1300"> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px query-area" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="采纳状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择" + size="large" + class="!w-200px" + clearable + @change="getList" + > + <el-option + v-for="item in suggestStatus" + :key="item.key" + :label="item.name" + :value="item.key" + /> + </el-select> + </el-form-item> + <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-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column + label="调度时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="调度建议" align="center" prop="content" width="750"/> + <el-table-column label="采纳状态" align="center" prop="status" width="90"> + <template #default="scope"> + <template v-if="scope.row.status === 0"> + 未处理 + </template> + <template v-else-if="scope.row.status === 1"> + <span style="color: var(--el-color-success)">已采纳</span> + </template> + <template v-else> + <span style="color: var(--el-color-danger)">已忽略</span> + </template> + </template> + </el-table-column> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <template v-if="scope.row.status === 0"> + <el-button + link + type="success" + @click="operateSuggest(scope.row.id, 1)" + > + 采纳建议 + </el-button> + <el-button + link + type="primary" + @click="operateSuggest(scope.row.id, 2)" + > + 忽略建议 + </el-button> + </template> + <template v-else-if="scope.row.status === 1"> + <el-button + link + type="primary" + @click="operateSuggest(scope.row.id, 2)" + > + 取消采纳 + </el-button> + </template> + <template v-else> + <el-button + link + type="primary" + > + 已忽略 + </el-button> + </template> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['ai:schedule-suggest:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + </el-container> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="handleQuery" + /> + </DialogSuggest> +</template> + +<script setup lang="ts"> +import {ChatMessageVO} from '@/api/ai/chat/message' +import {ref} from "vue"; +import {dateFormatter} from "@/utils/formatTime"; +import {ScheduleSuggestApi, ScheduleSuggestVO} from "@/api/ai/schedulesuggest"; +import {OtherPlatformEnum} from "@/views/ai/utils/constants"; + +/** AI 聊天对话 列表 */ +defineOptions({ name: 'HistoryMessageDialog' }) + +// 接收父组件传递的方法 +// const props = defineProps({ +// parentMethod: Function, +// gotoManualMethod: Function +// }); + +// 定义发射事件 +// const emit = defineEmits(['gotoManualMethod']) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const loading = ref(true) // 列表的加载中 +const list = ref<ScheduleSuggestVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + modelId: undefined, + conversationId: undefined, + messageId: undefined, + content: undefined, + status: undefined, + createTime: [], +}) +const operateData = ref({ + id: undefined, + status: undefined, +}) +const queryFormRef = ref() // 搜索的表单 + +const suggestStatus = ref([ + { + key: 0, + name: '未处理' + }, + { + key: 1, + name: '已采纳' + }, + { + key: 2, + name: '已忽略' + } +]) + +/** 打开弹窗 */ +const open = async () => { + dialogVisible.value = true + await nextTick() // 等待弹窗DOM挂载 + await getList() +} + +defineExpose({ open }) // 提供方法给 parent 调用 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ScheduleSuggestApi.getScheduleSuggestPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 采纳与取消采纳建议 */ +const operateSuggest = async (id: number, status: number) => { + const data = operateData.value as unknown as ScheduleSuggestVO + data.id = id + data.status = status + await ScheduleSuggestApi.operateScheduleSuggest(data) + message.success(t('common.updateSuccess')) + // 刷新列表 + await getList() +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ScheduleSuggestApi.deleteScheduleSuggest(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + queryParams.status = undefined + handleQuery() +} + +// const gotoManual = async (item: ChatMessageVO) => { +// emit('gotoManualMethod', item) // 发送数据给父组件 +// } + + +/** 初始化 **/ +onMounted(async () => {}) +</script> + +<style lang="scss" scoped> + +.query-area { + margin-top: 2px; + margin-bottom: 2px; + float: left; + :deep(.el-select__wrapper) { + background: rgba(255,255,255,0.1) !important; /* 保留浅色背景 */ + min-height: 30px; + box-shadow: none !important; + } + :deep(.el-select__placeholder) { + color: #DBEEFF; + } + :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: 42vh; + background-color: rgba(0, 0, 0, 0); /* 透明背景 */ + /* 表格透明背景 */ + :deep(.el-table) { + background-color: transparent !important; + border: 1px solid rgba(255, 255, 255, 0.3) !important; + border-radius: 4px; + } + + :deep(.el-table tr) { + color: #00b4ff; /* 蓝色文字 */ + background: transparent !important; + } + + /* 表头单元格边框 */ + :deep(.el-table th.el-table__cell) { + border-bottom: 1px solid rgba(115,196,255,0.14) !important; + border-right: 1px solid rgba(115,196,255,0.14) !important; + } + + /* 表格内容单元格边框 */ + :deep(.el-table td.el-table__cell) { + border-bottom: 1px solid rgba(115,196,255,0.14) !important; + border-right: 1px solid rgba(115,196,255,0.14) !important; + } + + /* 表头 */ + :deep(.el-table__header thead tr th) { + background-color: rgba(0, 194, 255, 0.2); + border-bottom: none; + } + + /* 行头样式 */ + :deep(.el-table .el-table__body td:first-child) { + color: #8FD6FE; + font-weight: 500; + } + + :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.1); + } + + /* 移除表格内部边框线 */ + :deep(.el-table td, .el-table th.is-leaf) { + border-bottom: none; + } + + :deep(.el-table .el-table__inner-wrapper:before) { + background-color: 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); } + } + } +} + +.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/suggest/ScheduleSuggestList.vue b/src/views/ai/dashboard/components/suggest/ScheduleSuggestList.vue new file mode 100644 index 0000000..b4ebf44 --- /dev/null +++ b/src/views/ai/dashboard/components/suggest/ScheduleSuggestList.vue @@ -0,0 +1,307 @@ +<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 question" @click="gotoManual(item)"> + <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 userAvatarDefaultImg from '@/assets/ai/zhuanlu/user.png' +import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png' + + +const message = useMessage() // 消息弹窗 +const { copy } = useClipboard() // 初始化 copy 到粘贴板 + +// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方) +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 + }, + + gotoManualMethod: Function +}) + +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 gotoManual = async (item: ChatMessageVO) => { + if(props.gotoManualMethod) { + props.gotoManualMethod(item) + } +} + +/** 复制 */ +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; + + .question:hover { + cursor: pointer; + background: rgba(40, 139, 255, 0.3); + } + + .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,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 9feb5d0..16e7643 100644 --- a/src/views/ai/dashboard/zhuanlu/index.vue +++ b/src/views/ai/dashboard/zhuanlu/index.vue @@ -26,9 +26,15 @@ <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 class="content1"> + <div class="value" v-if="item.type == 1"> + <div class="item" v-for="(list, i) in item.lists" :key="`dynamics-${i}`"> + <span>{{list.no}}</span><span>{{list.value}}</span> + </div> + </div> + <div class="value" v-else> + <span v-if="item.value == '进行'" style="color: #49FFD3; font-size: 14px; font-weight: bold;">{{item.value}}</span> + <span v-else style="color: #FFAE81; font-size: 14px; font-weight: bold;">{{item.value}}</span> </div> <div class="name"> {{item.name}} @@ -179,11 +185,11 @@ <div class="gas-scheduling-right"> <div id="ldghslyc"> <div class="title"></div> - <div ref="LDGHSLYCEhartContainer" style="width: 100%; height: 180px"></div> + <div ref="LDGHSLYCEhartContainer" style="width: 100%; height: 140px"></div> </div> <div id="ldggrqsyc"> <div class="title"></div> - <div ref="LDGGRYCEhartContainer" style="width: 100%; height: 180px"></div> + <div ref="LDGGRYCEhartContainer" style="width: 100%; height: 140px"></div> </div> <div id="mqhsjhxx"> <div class="title"></div> @@ -248,6 +254,31 @@ <div class="item right-label"></div> </div> </div> + <div class="schedule-suggest"> + <div class="result-title"> + <span>推理结论</span><el-button @click="openSuggest" size="small" class="result-button" :icon="ArrowRight">查看更多</el-button> + </div> + <div class="result-content"> + <div class="content-item" v-for="(item, index) in topSuggests" :key="`dynamics-${index}`"> + <div class="time"> + <span>{{formatDate(item.createTime, 'MM-DD HH:mm')}}</span> + </div> + <el-tooltip + effect="dark" + :content="item.content" + placement="top" + :disabled="!isOverflow" + > + <div class="content" ref="contentRef"> + {{ item.content }} + </div> + </el-tooltip> + </div> + </div> + <!-- 推理结论 --> + <ScheduleSuggestDialog + ref="scheduleSuggestRef" /> + </div> </div> </div> </template> @@ -262,13 +293,16 @@ import MessageLoading from '../components/message/MessageLoading.vue' import ConversationList from "../components/conversation/ConversationList.vue"; import HistoryMessageDialog from "../components/message/HistoryMessageDialog.vue" +import ScheduleSuggestDialog from "../components/suggest/ScheduleSuggestDialog.vue" import * as echarts from "echarts"; import {formatToDateTime} from "@/utils/dateUtil"; +import { formatReasoningContent } from '@/views/ai/utils/utils' import {refreshToken} from "@/api/login"; import {round} from "lodash-es"; -import {ArrowUpBold} from "@element-plus/icons-vue"; +import {ArrowRight, ArrowUpBold} from "@element-plus/icons-vue"; import * as authUtil from "@/utils/auth"; -import HistoryMessageList from "@/views/ai/dashboard/components/message/HistoryMessageList.vue"; +import {ScheduleSuggestApi, ScheduleSuggestVO} from "@/api/ai/schedulesuggest"; +import {formatDate} from "@/utils/formatTime"; const mqhsList = ref([ { @@ -278,7 +312,7 @@ }, { name: '转炉煤气 O 含量', - value: 618, + value: 10, unit: '%' }, { @@ -306,33 +340,60 @@ const tsxxList = ref([ { name: '各高炉出铁水信号', - value: '进行', - unit: '' + type: 1, + lists: [ + { + no: '1#', + value: '不进行', + }, + { + no: '2#', + value: '不进行', + } + ] }, { name: '各高炉出铁量', - value: 5000, - unit: '吨' + type: 1, + lists: [ + { + no: '1#', + value: '500t', + }, + { + no: '2#', + value: '600t', + } + ] }, { name: '各高炉铁水装入鱼雷罐车信号', - value: '进行', - unit: 'm³/h' + type: 1, + lists: [ + { + no: '1#', + value: '不进行', + }, + { + no: '2#', + value: '不进行', + } + ] }, { name: '鱼雷罐车等待信号', - value: '进行', - unit: 'm³/h' + type: 2, + value: '进行' }, { name: '铁水倒入铁水包信号', - value: '不进行', - unit: 'm³/h' + type: 2, + value: '不进行' }, { name: '铁产量计划', - value: 6000, - unit: '吨' + type: 3, + value: '6000t', }, ]) @@ -440,36 +501,24 @@ const mqhsjhxxList = ref([ { - name: '转炉总炉数\n' + - '日计划', - value: 567, + name: '转炉总炉数日计划', + value: 123, unit: '炉' }, { - name: '转炉入炉铁水量\n' + - '日计划', - value: 200, - unit: '吨' - }, - { name: '转炉检修计划', - value: '未进行', + value: '0', unit: '' }, { name: '钢产量日计划', - value: 300, - unit: '吨' - }, - { - name: '转炉加入废钢总量', - value: 500, - unit: '吨' + value: 20000, + unit: 't' }, { name: '转炉实绩钢产量', - value: 100, - unit: '吨' + value: 20929, + unit: 't' } ]) @@ -495,20 +544,20 @@ { id: 1, name: '1#转炉', - current: 20, - total: 30 + current: 4, + total: 29 }, { id: 2, name: '2#转炉', - current: 25, - total: 100 + current: 5, + total: 42 }, { id: 3, name: '3#转炉', - current: 4, - total: 29 + current: 6, + total: 42 } ]) @@ -536,6 +585,11 @@ } ]) + +const topSuggests = ref<ScheduleSuggestVO[]>([]) + +const contentRef = ref([]); +const isOverflow = ref([]); const ddtlResult = ref('') @@ -725,10 +779,14 @@ message.thinking = match[2]; message.conclusion = match[4] } + message.thinking = formatReasoningContent(message.thinking) return message } - - +/** 调度建议 */ +const scheduleSuggestRef = ref() +const openSuggest = async () => { + scheduleSuggestRef.value.open() +} /** * 消息列表 * @@ -736,8 +794,22 @@ */ const messageList = computed(() => { if (activeMessageList.value.length > 0) { - activeMessageList.value[1].thinking = dealResultAndData(activeMessageList.value[1].content) - return activeMessageList.value + // 对AI返回的消息进行格式化处理 + const formattedList = activeMessageList.value.map(msg => { + if (msg.type === 'assistant') { + // 复制消息对象以避免修改原始数据 + const formattedMsg = {...msg}; + // 处理推理思路内容 + formattedMsg.content = formatReasoningContent(msg.content); + return formattedMsg; + } + return msg; + }); + + // 处理调度推理结论及数据 + formattedList[1].thinking = dealResultAndData(formattedList[1].content); + + return formattedList; } // 没有消息时,如果有 systemMessage 则展示它 if (activeConversation.value?.systemMessage) { @@ -773,6 +845,14 @@ } initLDGGRQSYCChart() return content +} + +const getScheduleResult = (content: string) => { + const spliceText = content.includes("总结:") ? "总结:" : "结论:"; + const regex = new RegExp(`^([\\s\\S]*?)${spliceText}([\\s\\S]*)$`); + const match = content.match(regex); + const result = match ? match[2].trim() : ''; + return result } const extractRecoveryDetails = (text, consume, gui, totalMinutes = 60) => { @@ -945,6 +1025,10 @@ conversationId: activeConversationId.value, content: content } as ChatMessageVO) + // 保存调度建议 + setTimeout(async () => { + await createSuggest() + }, 1000) } /** 真正执行【发送】消息操作 */ @@ -1108,6 +1192,31 @@ } catch {} } +const suggestData = ref({ + id: undefined, + modelId: undefined, + conversationId: undefined, + messageId: undefined, + content: undefined, + status: undefined, +}) + +const createSuggest = async () => { + const suggestParam = suggestData.value as unknown as ScheduleSuggestVO + let assistantMessage = activeMessageList.value[1] + suggestParam.content = getScheduleResult(assistantMessage.content) + if(suggestParam.content != '') { + suggestParam.modelId = activeConversation.value.modelId + suggestParam.conversationId = activeConversation.value.id + suggestParam.messageId = assistantMessage.id + suggestParam.createTime = assistantMessage.createTime + suggestParam.status = 0 + await ScheduleSuggestApi.createScheduleSuggest(suggestParam) + // 刷新首页推理结果列表 + await getTopSuggest() + } +} + const LDGHSLYCEhartContainer = ref(); // 生成未来60秒的时间标签(LDG回收量预测) @@ -1148,7 +1257,7 @@ const max = LDGMaxTotalValue() const schedule = modelData.value.schedule[type] // 返回对象格式数据,包含原始值和基准值 - const baseline = round(max, 0) + (6 - 2 * type) + const baseline = round(max, 0) + (4.5 - 2 * type) return schedule.map(item => ({ value: item + baseline, // 显示值 = 原始值 + 基准值 original: item // 原始值 @@ -1232,15 +1341,15 @@ }, grid: { left: 25, - right: 25, + right: 5, bottom: 10, top: 30, containLabel: true }, legend: { - top: 10, + top: 5, right: 10, - data: ['1#转炉', '2#转炉', '3#转炉', '总回收量'], + data: ['1#转炉', '2#转炉', '3#转炉'], textStyle: { color: '#8FD6FE' }, @@ -1293,6 +1402,7 @@ focus: 'series' }, lineStyle: { + width: 1, color: '#FF7686' // 粉色 } }, @@ -1306,6 +1416,7 @@ focus: 'series' }, lineStyle: { + width: 1, color: '#49FFD3' // 绿色 } }, @@ -1320,6 +1431,7 @@ focus: 'series' }, lineStyle: { + width: 1, color: '#FFAE81' // 橙色 }, }, @@ -1333,6 +1445,7 @@ focus: 'series' }, lineStyle: { + width: 1, color: 'white' }, } @@ -1383,7 +1496,7 @@ left: 0, right: 0, bottom: 10, - top: 20, + top: 10, containLabel: true }, tooltip: { @@ -1434,7 +1547,10 @@ data: realData, smooth: true, symbol: 'none', - lineStyle: { color: '#95E6FF' }, + lineStyle: { + color: '#95E6FF', + width: 1 + }, markLine: { symbol: ['none', 'none'], label: { @@ -1459,6 +1575,7 @@ lineStyle: { type: 'dashed', color: '#E76666', + width: 1, dashOffset: 5 } }, @@ -1591,6 +1708,11 @@ // 初始状态检测 updateFullscreenStatus(); } +/** 查询列表 */ +const getTopSuggest = async () => { + const data = await ScheduleSuggestApi.getTopScheduleSuggests(5) + topSuggests.value = data +} /** 初始化 **/ onMounted(async () => { @@ -1605,11 +1727,11 @@ // 获取列表数据 activeMessageListLoading.value = true await getMessageList() + await getTopSuggest() }) // 清理监听 onUnmounted(() => { - console.log('stopStream') const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange']; events.forEach(event => { document.removeEventListener(event, handleFullscreenChange); @@ -1682,6 +1804,35 @@ font-weight: 400; font-size: 12px; color: #C7E7FF; + } + } + .name { + height: 16px; + font-weight: 400; + font-size: 12px; + color: #C7E7FF; + } + } + .content1 { + margin-left: 16px; + .value { + span:nth-child(1) { + height: 16px; + font-weight: 400; + font-size: 12px; + color: #C7E7FF; + } + span:nth-child(2){ + padding-left: 3px; + height: 19px; + font-weight: bold; + font-size: 14px; + color: #FFAE81; + line-height: 19px; + } + .item { + display: inline-block; + width: 45%; } } .name { @@ -2229,10 +2380,10 @@ } } .data2-item { - height: 2.8rem; - width: 45%; + height: 1.4rem; + width: 46%; display: inline-block; - margin: 6px 8px; + margin: 8px 8px; background: url("@/assets/ai/zhuanlu/data_bg3.png") no-repeat; } .content { @@ -2241,15 +2392,13 @@ margin-left: 10px; .name { - width: 95px; - height: 18px; + width: 130px; font-weight: 400; font-size: 14px; color: #C7E7FF; } .value { - margin-top: 10px; margin-left: auto; margin-right: 5px; span:nth-child(1) { @@ -2279,13 +2428,13 @@ .little-title { font-size: 14px; color: #8FD6FE; - margin: 10px; + margin: 5px 10px 5px 10px; } .data3-item { height: 5.2rem; width: 30%; display: inline-block; - margin: 0 6px 6px 6px; + margin: 0 6px 0 6px; background: url("@/assets/ai/zhuanlu/data_bg4.png") center/cover no-repeat; .name { font-family: Alimama ShuHeiTi, Alimama ShuHeiTi; @@ -2355,6 +2504,67 @@ } } } + .schedule-suggest { + .result-title { + margin-top: 10px; + background: url("@/assets/ai/zhuanlu/ddtljl_result_title.png") no-repeat; + 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-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); + } + } + .result-content { + margin-top: 5px; + display: inline-block; + font-weight: 400; + font-size: 14px; + color: rgba(130,202,255,0.89); + text-align: left; + font-style: normal; + text-transform: none; + .content-item { + height: 28px; + background: rgba(69,133,255,0.2); + border-radius: 2px 2px 2px 2px; + padding-left: 3px; + margin: 3px 0; + display: flex; + align-items: center; + overflow: hidden; + } + .time { + flex-shrink: 0; + margin-right: 12px; + } + .content { + width: 350px; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } } } diff --git a/src/views/ai/model/template/index.vue b/src/views/ai/model/template/index.vue index b6de43b..9d51204 100644 --- a/src/views/ai/model/template/index.vue +++ b/src/views/ai/model/template/index.vue @@ -113,6 +113,7 @@ import * as AiQuestionTemplateApi from '@/api/ai/questiontemplate' import TemplateForm from './templateForm.vue' import * as AiModelApi from "@/api/ai/model/model"; + import {AiModelTypeEnum} from "@/views/ai/utils/constants"; defineOptions({name: 'AiTemplate'}) @@ -179,7 +180,7 @@ /** 初始化 **/ onMounted(async () => { - aiModelList.value = await AiModelApi.ModelApi.getModelSimpleList(1) + aiModelList.value = await AiModelApi.ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT + "," + AiModelTypeEnum.LLM) await getList() }) </script> diff --git a/src/views/ai/model/template/templateForm.vue b/src/views/ai/model/template/templateForm.vue index fde0ed7..da11743 100644 --- a/src/views/ai/model/template/templateForm.vue +++ b/src/views/ai/model/template/templateForm.vue @@ -148,6 +148,7 @@ import {CommonStatusEnum} from '@/utils/constants' import {ElMessage} from 'element-plus' import * as AiModelApi from "@/api/ai/model/model"; + import {AiModelTypeEnum} from "@/views/ai/utils/constants"; const aiModelList = ref([] as AiModelApi.ModelVO[]) defineOptions({name: 'AiTemplateForm'}) @@ -203,7 +204,7 @@ formType.value = type resetForm() // 加载调度模型列表 - aiModelList.value = await AiModelApi.ModelApi.getModelSimpleList(1) + aiModelList.value = await AiModelApi.ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT + "," + AiModelTypeEnum.LLM) if (id) { formLoading.value = true try { diff --git a/src/views/ai/suggest/ScheduleSuggestForm.vue b/src/views/ai/suggest/ScheduleSuggestForm.vue new file mode 100644 index 0000000..1329936 --- /dev/null +++ b/src/views/ai/suggest/ScheduleSuggestForm.vue @@ -0,0 +1,117 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="模型id" prop="modelId"> + <el-input v-model="formData.modelId" placeholder="请输入模型id" /> + </el-form-item> + <el-form-item label="会话id" prop="conversationId"> + <el-input v-model="formData.conversationId" placeholder="请输入会话id" /> + </el-form-item> + <el-form-item label="消息id" prop="messageId"> + <el-input v-model="formData.messageId" placeholder="请输入消息id" /> + </el-form-item> + <el-form-item label="调度建议" prop="content"> + <Editor v-model="formData.content" height="150px" /> + </el-form-item> + <el-form-item label="状态(0-未处理 1-已采纳 2-已忽略)" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio label="1">请选择字典生成</el-radio> + </el-radio-group> + </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> + </Dialog> +</template> +<script setup lang="ts"> +import { ScheduleSuggestApi, ScheduleSuggestVO } from '@/api/ai/schedulesuggest' + +/** 大模型调度建议 表单 */ +defineOptions({ name: 'ScheduleSuggestForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + modelId: undefined, + conversationId: undefined, + messageId: undefined, + content: undefined, + status: undefined, +}) +const formRules = reactive({ + modelId: [{ required: true, message: '模型id不能为空', trigger: 'blur' }], + conversationId: [{ required: true, message: '会话id不能为空', trigger: 'blur' }], + messageId: [{ required: true, message: '消息id不能为空', trigger: 'blur' }], + status: [{ required: true, message: '状态(0-未处理 1-已采纳 2-已忽略)不能为空', trigger: 'blur' }], +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await ScheduleSuggestApi.getScheduleSuggest(id) + } finally { + formLoading.value = false + } + } +} +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 ScheduleSuggestVO + if (formType.value === 'create') { + await ScheduleSuggestApi.createScheduleSuggest(data) + message.success(t('common.createSuccess')) + } else { + await ScheduleSuggestApi.updateScheduleSuggest(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + modelId: undefined, + conversationId: undefined, + messageId: undefined, + content: undefined, + status: undefined, + } + formRef.value?.resetFields() +} +</script> \ No newline at end of file diff --git a/src/views/ai/suggest/index.vue b/src/views/ai/suggest/index.vue new file mode 100644 index 0000000..b241a18 --- /dev/null +++ b/src/views/ai/suggest/index.vue @@ -0,0 +1,223 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="模型id" prop="modelId"> + <el-input + v-model="queryParams.modelId" + placeholder="请输入模型id" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="会话id" prop="conversationId"> + <el-input + v-model="queryParams.conversationId" + placeholder="请输入会话id" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="消息id" prop="messageId"> + <el-input + v-model="queryParams.messageId" + placeholder="请输入消息id" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="状态(0-未处理 1-已采纳 2-已忽略)" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择状态(0-未处理 1-已采纳 2-已忽略)" + clearable + class="!w-240px" + > + <el-option label="请选择字典生成" value="" /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </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-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['ai:schedule-suggest:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['ai:schedule-suggest:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <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="会话id" align="center" prop="conversationId" /> + <el-table-column label="消息id" align="center" prop="messageId" /> + <el-table-column label="调度建议" align="center" prop="content" /> + <el-table-column label="状态(0-未处理 1-已采纳 2-已忽略)" align="center" prop="status" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['ai:schedule-suggest:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['ai:schedule-suggest:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <ScheduleSuggestForm ref="formRef" @success="getList" /> +</template> + +<script setup lang="ts"> +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' +import { ScheduleSuggestApi, ScheduleSuggestVO } from '@/api/ai/schedulesuggest' +import ScheduleSuggestForm from './ScheduleSuggestForm.vue' + +/** 大模型调度建议 列表 */ +defineOptions({ name: 'ScheduleSuggest' }) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const list = ref<ScheduleSuggestVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + modelId: undefined, + conversationId: undefined, + messageId: undefined, + content: undefined, + status: undefined, + createTime: [], +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ScheduleSuggestApi.getScheduleSuggestPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ScheduleSuggestApi.deleteScheduleSuggest(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await ScheduleSuggestApi.exportScheduleSuggest(queryParams) + download.excel(data, '大模型调度建议.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> \ No newline at end of file diff --git a/src/views/ai/utils/constants.ts b/src/views/ai/utils/constants.ts index 580c676..713862a 100644 --- a/src/views/ai/utils/constants.ts +++ b/src/views/ai/utils/constants.ts @@ -25,12 +25,12 @@ } export const AiModelTypeEnum = { - CHAT: 1, // 聊天 - IMAGE: 2, // 图像 - VOICE: 3, // 音频 - VIDEO: 4, // 视频 - EMBEDDING: 5, // 向量 - RERANK: 6 // 重排 + CHAT: '1', // 聊天 + IMAGE: '2', // 图像 + VOICE: '3', // 音频 + VIDEO: '4', // 视频 + EMBEDDING: '5', // 向量 + LLM: '6' // 重排 } export const OtherPlatformEnum: ImageModelVO[] = [ diff --git a/src/views/ai/utils/utils.ts b/src/views/ai/utils/utils.ts index ef36350..2688533 100644 --- a/src/views/ai/utils/utils.ts +++ b/src/views/ai/utils/utils.ts @@ -11,3 +11,20 @@ export const hasChinese = (str: string) => { return /[\u4e00-\u9fa5]/.test(str) } + +export const formatReasoningContent = (content: string) => { + // 匹配 "数字" + "." + ("中文"或"空格") + "其他内容" + ":" + const stepRegex = /(\d+\.(?:[\u4e00-\u9fa5]|\s)[^:]*:)(\s*)/g; + + // 替换逻辑: + // - 如果标题后没有换行(即 `$2` 是空或只有空格),则添加 `<br>` + // - 如果标题后已有换行(如 `\n` 或 `<br>`),则不额外添加 + return content.replace( + stepRegex, + (match, title, whitespace) => { + const hasNewline = whitespace.includes('\\n') || whitespace.includes('<br>'); + const lineBreak = hasNewline ? '' : '<br>'; + return `<strong style="font-size: 16px; line-height: 32px; color: #FFFFFF;">${title}</strong>${lineBreak}`; + } + ); +} -- Gitblit v1.9.3