houzhongjian
10 天以前 bec94315ad2c0932bd655d8947276abd18dbf6a0
转炉大模型功能完善
已添加2个文件
724 ■■■■■ 文件已修改
src/views/ai/dashboard/components/suggest/ScheduleSuggestDialog.vue 417 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ai/dashboard/components/suggest/ScheduleSuggestList.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | 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>