src/views/ai/dashboard/components/suggest/ScheduleSuggestDialog.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/ai/dashboard/components/suggest/ScheduleSuggestList.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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> 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>