Răsfoiți Sursa

feat: 实现ai api log和 chat log页面

zhouyuhao 7 luni în urmă
părinte
comite
43d4e59128

+ 10 - 0
src/router/index.ts

@@ -96,6 +96,16 @@ const router = createRouter({
           name: 'Operationlog',
           component: () => import('../views/OperationLog')
         },
+        {
+          path: '/chat-log',
+          name: 'Chat Log',
+          component: () => import('../views/ChatLog')
+        },
+        {
+          path: '/ai-api-log',
+          name: 'AI API Log',
+          component: () => import('../views/AIApiLog')
+        },
         {
           path: '/system-message',
           name: 'System Message',

+ 1 - 0
src/views/AIApiLog/index.ts

@@ -0,0 +1 @@
+export { default } from './src/AIApiLog.vue'

+ 211 - 0
src/views/AIApiLog/src/AIApiLog.vue

@@ -0,0 +1,211 @@
+<script lang="ts" setup>
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import TableView from './components/TableView'
+
+const OperationSearch = ref()
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 290, [filterRef])
+const searchData = ref({
+  inputModel: '',
+  startDate: '',
+  endDate: '',
+  aiModel: '',
+  responseDuration: ''
+})
+
+const aiModelList = [
+  {
+    label: 'Deepseek-chat',
+    value: 'deepseekChat'
+  },
+  {
+    label: 'Deepseek-search',
+    value: 'deepseekSearch'
+  },
+  {
+    label: 'Claude 3.7 Sonnet',
+    value: 'claude'
+  }
+]
+const questionTypeList = [
+  {
+    label: 'Predefined Question',
+    value: 'predefinedQuestion'
+  },
+  {
+    label: 'free text',
+    value: 'freeText'
+  }
+]
+const AnswerTypeList = [
+  {
+    label: 'Suspend',
+    value: 'suspend'
+  },
+  {
+    label: 'Timeout',
+    value: 'timeout'
+  },
+  {
+    label: 'Predefined Template',
+    value: 'predefinedTemplate'
+  },
+  {
+    label: 'AI Answer',
+    value: 'AIAnswer'
+  }
+]
+const answerSatisfactionList = [
+  {
+    label: 'Null',
+    value: 'null'
+  },
+  {
+    label: 'Good',
+    value: 'good'
+  },
+  {
+    label: 'Not Good',
+    value: 'notGood'
+  }
+]
+
+const tableRef = ref()
+
+const Search = () => {
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+const DateChange = (date: any) => {
+  searchData.value.startDate = date[0]
+  searchData.value.endDate = date[1]
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+</script>
+<template>
+  <div class="dashboard">
+    <div class="Title">AI API Log</div>
+    <div class="display">
+      <div class="heaer_top">
+        <div class="input-tips_filter">
+          <el-input
+            placeholder="Search Request ID、Question ID"
+            v-model="OperationSearch"
+            class="log_input"
+          >
+            <template #prefix>
+              <span class="iconfont_icon">
+                <svg class="iconfont icon_dark" aria-hidden="true">
+                  <use xlink:href="#icon-icon_search_b"></use>
+                </svg>
+              </span>
+            </template>
+          </el-input>
+        </div>
+
+        <div class="date-tips_filter">
+          <CalendarDate @DateChange="DateChange"></CalendarDate>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.aiModel" placeholder="AI Model">
+            <el-option
+              v-for="item in aiModelList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.questionType" placeholder="Question Type">
+            <el-option
+              v-for="item in questionTypeList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.answerType" placeholder="Answer Type">
+            <el-option
+              v-for="item in AnswerTypeList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+        </div>
+
+        <el-button class="el-button--dark" @click="Search">Search</el-button>
+      </div>
+    </div>
+    <TableView :height="containerHeight" ref="tableRef"></TableView>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding-left: 23.32px;
+  align-items: center;
+}
+.heaer_top {
+  margin-top: 6.57px;
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+}
+
+.display {
+  border: 1px solid var(--color-border);
+  border-width: 0 0 1px 0;
+  padding-left: 23.52px;
+}
+:deep(.el-select__placeholder.is-transparent span) {
+  color: var(--tag-info-text-color) !important;
+}
+:deep(.ETD_title) {
+  margin-bottom: 0;
+}
+:deep(.ant-picker-range) {
+  width: 250px !important;
+  height: 32px;
+  background-color: var(--color-mode) !important;
+}
+.tips_filter {
+  flex: 1;
+  height: 30px;
+  max-width: 190px;
+  margin-right: 8px;
+}
+.input-tips_filter {
+  flex: 1;
+  max-width: 320px;
+  height: 32px;
+  margin-right: 8px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+  }
+}
+.date-tips_filter {
+  flex: 1;
+  max-width: 250px;
+  height: 32px;
+  margin-right: 8px;
+}
+.dashboard {
+  z-index: 2014;
+  position: relative;
+  background-color: var(--color-mode);
+}
+:deep(.log_input .el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-select-border);
+  border-radius: 6px;
+}
+</style>

+ 1 - 0
src/views/AIApiLog/src/components/TableView/index.ts

@@ -0,0 +1 @@
+export { default } from './src/TableView.vue'

+ 436 - 0
src/views/AIApiLog/src/components/TableView/src/TableView.vue

@@ -0,0 +1,436 @@
+<script setup lang="ts">
+import { ref, nextTick, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import DownloadDialog from './components/DownloadDialog.vue'
+// import { autoWidth } from '@/utils/table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import dayjs from 'dayjs'
+import { formatTimezone, formatNumber } from '@/utils/tools'
+
+const props = defineProps({
+  height: {
+    type: Number,
+    default: 440
+  }
+})
+
+const tableOriginColumnsField = ref()
+const handleColumns = (columns: any, status?: string) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      sortable: true,
+      minWidth: 120
+    }
+    // 设置插槽
+    if (item.type === 'status' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    } else if (item.type === 'link' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'link' }
+      }
+    } else if (item.type === 'mode' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'mode' },
+        formatter: ({ cellValue }: any) => {
+          return cellValue
+        }
+      }
+    }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+// 获取表格列
+const getTableColumns = async () => {
+  tableLoadingColumn.value = true
+  await $api.getOperationTableColumns().then((res: any) => {
+    if (res.code === 200) {
+      tableData.value.columns = [
+        { type: 'checkbox', width: 50, fixed: 'left' },
+        ...handleColumns(res.data.OperationTableColumns)
+      ]
+      tableOriginColumnsField.value = res.data.OperationTableColumns
+    }
+  })
+  nextTick(() => {
+    // tableRef.value && autoWidth(tableData.value, tableRef.value)
+    tableLoadingColumn.value = false
+    selectedNumber.value = 0
+    selectedTableData.value = []
+  })
+}
+
+const pageInfo = ref({ pageNo: 1, pageSize: 20, total: 0 })
+const tempSearch = ref()
+// 获得表格数据后赋值
+const assignTableData = (data: any) => {
+  tableData.value.data = data.searchData || []
+  pageInfo.value.total = Number(data.rc) || 0
+  tempSearch.value = data.tmp_search
+  // 拥有所有字段的表格
+  setTimeout(() => {
+    allTable.value.columns = handleColumns(tableData.value.columns, 'all')
+    allTable.value.data = data.searchData || []
+    // 为了让导出的表格列宽度自适应
+    nextTick(() => {
+      // allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+    })
+  }, 1000)
+}
+
+let searchdata: any = {}
+// 获取表格数据
+const getTableData = async (isPageChange?: boolean) => {
+  const rc = isPageChange ? pageInfo.value.total : -1
+  tableLoadingTableData.value = true
+  await $api
+    .SearchOperationLog({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc,
+      ...searchdata
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+const SearchOperationLog = (val: any) => {
+  searchdata = val
+  tableLoadingTableData.value = true
+  $api
+    .SearchOperationLog({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: -1,
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+onMounted(() => {
+  Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
+    nextTick(() => {
+      // tableRef.value && autoWidth(tableData.value, tableRef.value)
+    })
+  })
+})
+
+const tableRef = ref<VxeGridInstance>()
+const tableData = ref<VxeGridProps<any>>({
+  border: true,
+  round: true,
+  columns: [],
+  data: [],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  stripe: true,
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  sortConfig: {
+    sortMethod: (params) => {
+      const { data, sortList } = params
+
+      // 如果没有排序条件,直接返回原数据
+      if (sortList.length === 0) return data
+
+      // 对数据进行多重排序
+      const sortedData = [...data].sort((a, b) => {
+        for (const { field, order } of sortList) {
+          const curColumn = tableOriginColumnsField.value.find((item: any) => item.field === field)
+          if (!curColumn) continue
+
+          const typeName = curColumn.type
+          const aValue = a[field]
+          const bValue = b[field]
+
+          const compareResult = (aValue: any, bValue: any) => {
+            // 如果 aValue 或 bValue 是 null 或 undefined,优先处理这些值
+            if (aValue == null && bValue == null) {
+              return 0 // 如果两个值都为 null 或 undefined,视为相等
+            } else if (aValue == null) {
+              return -1 // 如果 aValue 是 null,bValue 不是,则将 aValue 视为较小值
+            } else if (bValue == null) {
+              return 1 // 如果 bValue 是 null,aValue 不是,则将 bValue 视为较小值
+            }
+
+            if (typeName === 'datetime' || typeName === 'date' || typeName === 'time') {
+              return dayjs(aValue).unix() - dayjs(bValue).unix()
+            } else if (isNaN(Number(aValue)) || isNaN(Number(bValue))) {
+              return aValue.localeCompare(bValue)
+            } else {
+              return Number(aValue) - Number(bValue)
+            }
+          }
+
+          const result = compareResult(aValue, bValue)
+          if (result !== 0) {
+            return order === 'asc' ? result : -result
+          }
+        }
+
+        return 0 // 如果所有字段都相等
+      })
+
+      return sortedData
+    }
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+const allTableRef = ref<VxeGridInstance>()
+const allTable = ref<VxeGridProps<any>>({
+  columns: [],
+  data: [],
+  showHeaderOverflow: true,
+  showOverflow: true,
+  scrollY: { enabled: true, oSize: 5, gt: 2 },
+  scrollX: { enabled: true, gt: 2 },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const downloadDialogRef = ref()
+const handleDownload = () => {
+  const curSelectedColumns: string[] = []
+  tableRef.value?.columns?.forEach((item: any) => {
+    if (item.field) {
+      curSelectedColumns.push(item.title)
+    }
+  })
+  downloadDialogRef.value.openDialog(
+    curSelectedColumns,
+    selectedNumber.value || pageInfo.value.total
+  )
+}
+
+const exportLoading = ref(false)
+// 获取导出表格数据
+const getExportTableData = (status: number) => {
+  // 如果有选中表格行数据,那么只到处选中的数据
+  if (selectedNumber.value > 0) {
+    exportTable(status)
+    return
+  }
+  exportLoading.value = true
+  const buildColumnString = (columns: any[]): string => {
+    return columns
+      .filter((item) => item.field)
+      .map((item) => item.title)
+      .join(',')
+  }
+
+  let column = ''
+  if (status === 1) {
+    column = buildColumnString(tableData.value.columns)
+  } else {
+    column = buildColumnString(allTable.value.columns)
+  }
+  $api
+    .OperationLogDownload({
+      selected_fields: column,
+      tmp_search: tempSearch.value
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allTable.value.data = res.data.Data || []
+        nextTick(() => {
+          exportLoading.value = false
+          // 导出数据
+          exportTable(status)
+        })
+      }
+    })
+    .finally(() => {
+      exportLoading.value = false
+    })
+}
+// 导出表格 status: 1 导出当前表格 2 导出所有字段表格
+const exportTable = (status: number) => {
+  const exportConfig: any = {
+    type: 'xlsx',
+    message: false,
+    filename: `Chat Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+  }
+  if (status === 1) {
+    exportConfig.columnFilterMethod = ({ column }: any) => {
+      const index = tableData.value.columns.findIndex((item: any) => item.field === column.field)
+      // 排除复选框列
+      return column.field && index !== -1
+    }
+    exportConfig.columns = tableData.value.columns
+  }
+  if (selectedNumber.value > 0) {
+    exportConfig.dataFilterMethod = ({ row }: any) => {
+      const index = selectedTableData.value.findIndex(
+        (item: any) => item._X_ROW_KEY === row._X_ROW_KEY
+      )
+      return index !== -1
+    }
+  }
+  allTableRef.value?.exportData(exportConfig)
+}
+
+const tableLoadingColumn = ref(false)
+const tableLoadingTableData = ref(false)
+
+const selectedNumber = ref(0)
+const selectedTableData = ref([])
+// 复选框选中事件
+const handleCheckboxChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+const handleCheckAllChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+defineExpose({
+  SearchOperationLog
+})
+</script>
+
+<template>
+  <div
+    style="padding: 0px 20px"
+    class="table-box"
+    v-loading.fullscreen.lock="exportLoading"
+    element-loading-text="Loading..."
+    element-loading-custom-class="element-loading"
+    element-loading-background="rgb(43, 47, 54, 0.7)"
+  >
+    <div class="table-tools">
+      <div class="left-total-records">{{ selectedNumber }} Selected</div>
+      <div class="right-tools-btn">
+        <el-button class="el-button--main el-button--pain-theme" @click="handleDownload">
+          <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+          Download
+        </el-button>
+      </div>
+    </div>
+    <vxe-grid
+      ref="tableRef"
+      v-vloading="tableLoadingTableData || tableLoadingColumn"
+      :height="props.height"
+      :style="{ border: 'none' }"
+      v-bind="tableData"
+      @checkbox-change="handleCheckboxChange"
+      @checkbox-all="handleCheckAllChange"
+    >
+      <!-- 空数据时的插槽 -->
+      <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
+        <VEmpty></VEmpty>
+      </template>
+    </vxe-grid>
+    <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
+    <div class="bottom-pagination">
+      <div class="left-total-records">Total {{ formatNumber(pageInfo.total) }}</div>
+      <div class="right-pagination">
+        <el-pagination
+          v-model:current-page="pageInfo.pageNo"
+          v-model:page-size="pageInfo.pageSize"
+          :page-sizes="[20, 50, 100, 150]"
+          :pager-count="3"
+          background
+          layout="sizes, prev, pager, next"
+          :total="pageInfo.total"
+          @size-change="getTableData(true)"
+          @current-change="getTableData(true)"
+        />
+      </div>
+    </div>
+    <DownloadDialog @export="getExportTableData" ref="downloadDialogRef" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.table-tools {
+  position: relative;
+  display: flex;
+  justify-content: space-between;
+  height: 48px;
+  padding: 8px 0;
+
+  .left-total-records {
+    font-size: 16px;
+    font-weight: 700;
+    line-height: 32px;
+  }
+}
+
+.bottom-pagination {
+  display: flex;
+  justify-content: space-between;
+  height: 40px;
+  margin-top: -1px;
+  padding: 4px 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 0 0 12px 12px;
+
+  .left-total-records {
+    line-height: 32px;
+  }
+
+  .right-pagination {
+    display: flex;
+    align-items: center;
+  }
+}
+.table-box {
+  position: relative;
+  overflow: hidden;
+
+  .all-table {
+    position: absolute;
+    top: -100000px;
+    width: 20px;
+  }
+}
+</style>

+ 190 - 0
src/views/AIApiLog/src/components/TableView/src/components/DownloadDialog.vue

@@ -0,0 +1,190 @@
+<script setup lang="ts">
+const dialogVisible = ref(false)
+
+const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
+  selectedDataNumber.value = slectedDataNumber
+  columns.value = selectedColumns
+  dialogVisible.value = true
+}
+
+const isShowSelectColumn = ref(false)
+
+const downloadFilter = ref(1)
+const selectedDataNumber = ref(0)
+
+const columns = ref()
+
+const emits = defineEmits<{ export: [number] }>()
+const handleDownload = () => {
+  emits('export', downloadFilter.value)
+}
+
+const clearData = () => {
+  isShowSelectColumn.value = false
+  downloadFilter.value = 1
+}
+
+defineExpose({
+  openDialog,
+  handleDownload
+})
+</script>
+
+<template>
+  <div>
+    <el-dialog @close="clearData" v-model="dialogVisible" title="Download File" width="540">
+      <div class="download-dialog">
+        <div class="select-data">
+          <div style="display: inline-block">
+            Select data on your Opeartion Log list:<span style="color: var(--color-theme)">{{
+              selectedDataNumber
+            }}</span>
+          </div>
+        </div>
+        <div class="download-filter">
+          <el-radio-group v-model="downloadFilter">
+            <el-radio :value="1"
+              >Download with selected columns
+              <span class="column-number">{{ columns.length }}</span>
+              <SeeAllIcon v-model="isShowSelectColumn" />
+            </el-radio>
+            <div
+              v-if="isShowSelectColumn"
+              class="select-columns"
+              :class="{ show: isShowSelectColumn }"
+            >
+              <div class="title">Selected columns</div>
+              <div class="content">
+                <div class="column-item" v-for="item in columns" :key="item">{{ item }}</div>
+              </div>
+            </div>
+            <el-radio :value="2">Download with all columns</el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button class="cancel-btn" type="default" @click="dialogVisible = false"
+            >Cancel</el-button
+          >
+          <el-button class="download-btn el-button--dark" @click="handleDownload"
+            ><span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+            Download</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.download-dialog {
+  color: var(--color-neutral-1);
+}
+
+.select-data {
+  font-weight: 700;
+}
+
+.data-filter {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  max-height: 120px;
+  margin-top: 8px;
+  overflow: auto;
+
+  .filter-item {
+    height: 22px;
+    padding: 0px 8px;
+    background-color: var(--color-download-file-filter-tag-bg);
+    border-radius: 12px;
+    line-height: 22px;
+    font-size: 12px;
+  }
+}
+
+.download-filter {
+  margin-top: 16px;
+
+  .el-radio-group {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .el-radio {
+      height: 40px;
+      align-items: center;
+    }
+
+    :deep(.el-radio__label) {
+      margin-top: 2px;
+      font-weight: 700;
+      color: var(--color-neutral-1);
+    }
+
+    .column-number {
+      padding: 3px 5px;
+      background-color: var(--color-theme);
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 700;
+      color: #fff;
+    }
+
+    .see-all-btn {
+      margin-left: 8px;
+      color: var(--color-theme);
+      font-size: 12px;
+    }
+
+    .select-columns {
+      max-height: 350px;
+      padding: 8px;
+      margin-top: 8px;
+      background-color: var(--color-dialog-header-bg);
+      border-radius: 6px;
+      overflow: hidden;
+
+      &.show {
+        max-height: 500px;
+      }
+
+      .title {
+        font-size: 12px;
+        font-weight: 700;
+      }
+
+      .content {
+        display: flex;
+        flex-wrap: wrap;
+        margin-top: 8px;
+        gap: 8px;
+
+        .column-item {
+          height: 22px;
+          padding: 0px 8px;
+          background-color: var(--color-download-file-selected-column-tag-bg);
+          line-height: 22px;
+          border-radius: 12px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  .el-button {
+    height: 40px;
+  }
+
+  .cancel-btn {
+    width: 115px;
+  }
+
+  .download-btn {
+    width: 136px;
+  }
+}
+</style>

+ 1 - 0
src/views/ChatLog/index.ts

@@ -0,0 +1 @@
+export { default } from './src/ChatLog.vue'

+ 219 - 0
src/views/ChatLog/src/ChatLog.vue

@@ -0,0 +1,219 @@
+<script lang="ts" setup>
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import TableView from './components/TableView'
+
+const OperationSearch = ref()
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 290, [filterRef])
+const searchData = ref({
+  inputModel: '',
+  startDate: '',
+  endDate: '',
+  userType: '',
+  questionType: '',
+  answerType: '',
+  answerSatisfaction: ''
+})
+
+const userTypeList = [
+  {
+    label: 'Customer',
+    value: 'customer'
+  },
+  {
+    label: 'Employee',
+    value: 'employee'
+  }
+]
+const questionTypeList = [
+  {
+    label: 'Predefined Question',
+    value: 'predefinedQuestion'
+  },
+  {
+    label: 'free text',
+    value: 'freeText'
+  }
+]
+const AnswerTypeList = [
+  {
+    label: 'Suspend',
+    value: 'suspend'
+  },
+  {
+    label: 'Timeout',
+    value: 'timeout'
+  },
+  {
+    label: 'Predefined Template',
+    value: 'predefinedTemplate'
+  },
+  {
+    label: 'AI Answer',
+    value: 'AIAnswer'
+  }
+]
+const answerSatisfactionList = [
+  {
+    label: 'Null',
+    value: 'null'
+  },
+  {
+    label: 'Good',
+    value: 'good'
+  },
+  {
+    label: 'Not Good',
+    value: 'notGood'
+  }
+]
+
+const tableRef = ref()
+
+const Search = () => {
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+const DateChange = (date: any) => {
+  searchData.value.startDate = date[0]
+  searchData.value.endDate = date[1]
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+</script>
+<template>
+  <div class="dashboard">
+    <div class="Title">Chat Log</div>
+    <div class="display">
+      <div class="heaer_top">
+        <div class="input-tips_filter">
+          <el-input
+            placeholder="Search Question ID、User"
+            v-model="OperationSearch"
+            class="log_input"
+          >
+            <template #prefix>
+              <span class="iconfont_icon">
+                <svg class="iconfont icon_dark" aria-hidden="true">
+                  <use xlink:href="#icon-icon_search_b"></use>
+                </svg>
+              </span>
+            </template>
+          </el-input>
+        </div>
+
+        <div class="date-tips_filter">
+          <CalendarDate @DateChange="DateChange"></CalendarDate>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.userType" placeholder="User Type">
+            <el-option
+              v-for="item in userTypeList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.questionType" placeholder="Question Type">
+            <el-option
+              v-for="item in questionTypeList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.answerType" placeholder="Answer Type">
+            <el-option
+              v-for="item in AnswerTypeList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.answerSatisfaction" placeholder="Answer Satisfaction">
+            <el-option
+              v-for="item in answerSatisfactionList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+        </div>
+        <el-button class="el-button--dark" @click="Search">Search</el-button>
+      </div>
+    </div>
+    <TableView :height="containerHeight" ref="tableRef"></TableView>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding-left: 23.32px;
+  align-items: center;
+}
+.heaer_top {
+  margin-top: 6.57px;
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+}
+
+.display {
+  border: 1px solid var(--color-border);
+  border-width: 0 0 1px 0;
+  padding-left: 23.52px;
+}
+:deep(.el-select__placeholder.is-transparent span) {
+  color: var(--tag-info-text-color) !important;
+}
+:deep(.ETD_title) {
+  margin-bottom: 0;
+}
+:deep(.ant-picker-range) {
+  width: 250px !important;
+  height: 32px;
+  background-color: var(--color-mode) !important;
+}
+.tips_filter {
+  flex: 1;
+  height: 30px;
+  max-width: 190px;
+  margin-right: 8px;
+}
+.input-tips_filter {
+  flex: 1;
+  max-width: 320px;
+  height: 32px;
+  margin-right: 8px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+  }
+}
+.date-tips_filter {
+  flex: 1;
+  max-width: 250px;
+  height: 32px;
+  margin-right: 8px;
+}
+.dashboard {
+  z-index: 2014;
+  position: relative;
+  background-color: var(--color-mode);
+}
+:deep(.log_input .el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-select-border);
+  border-radius: 6px;
+}
+</style>

+ 1 - 0
src/views/ChatLog/src/components/TableView/index.ts

@@ -0,0 +1 @@
+export { default } from './src/TableView.vue'

+ 436 - 0
src/views/ChatLog/src/components/TableView/src/TableView.vue

@@ -0,0 +1,436 @@
+<script setup lang="ts">
+import { ref, nextTick, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import DownloadDialog from './components/DownloadDialog.vue'
+// import { autoWidth } from '@/utils/table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import dayjs from 'dayjs'
+import { formatTimezone, formatNumber } from '@/utils/tools'
+
+const props = defineProps({
+  height: {
+    type: Number,
+    default: 440
+  }
+})
+
+const tableOriginColumnsField = ref()
+const handleColumns = (columns: any, status?: string) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      sortable: true,
+      minWidth: 120
+    }
+    // 设置插槽
+    if (item.type === 'status' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    } else if (item.type === 'link' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'link' }
+      }
+    } else if (item.type === 'mode' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'mode' },
+        formatter: ({ cellValue }: any) => {
+          return cellValue
+        }
+      }
+    }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+// 获取表格列
+const getTableColumns = async () => {
+  tableLoadingColumn.value = true
+  await $api.getOperationTableColumns().then((res: any) => {
+    if (res.code === 200) {
+      tableData.value.columns = [
+        { type: 'checkbox', width: 50, fixed: 'left' },
+        ...handleColumns(res.data.OperationTableColumns)
+      ]
+      tableOriginColumnsField.value = res.data.OperationTableColumns
+    }
+  })
+  nextTick(() => {
+    // tableRef.value && autoWidth(tableData.value, tableRef.value)
+    tableLoadingColumn.value = false
+    selectedNumber.value = 0
+    selectedTableData.value = []
+  })
+}
+
+const pageInfo = ref({ pageNo: 1, pageSize: 20, total: 0 })
+const tempSearch = ref()
+// 获得表格数据后赋值
+const assignTableData = (data: any) => {
+  tableData.value.data = data.searchData || []
+  pageInfo.value.total = Number(data.rc) || 0
+  tempSearch.value = data.tmp_search
+  // 拥有所有字段的表格
+  setTimeout(() => {
+    allTable.value.columns = handleColumns(tableData.value.columns, 'all')
+    allTable.value.data = data.searchData || []
+    // 为了让导出的表格列宽度自适应
+    nextTick(() => {
+      // allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+    })
+  }, 1000)
+}
+
+let searchdata: any = {}
+// 获取表格数据
+const getTableData = async (isPageChange?: boolean) => {
+  const rc = isPageChange ? pageInfo.value.total : -1
+  tableLoadingTableData.value = true
+  await $api
+    .SearchOperationLog({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc,
+      ...searchdata
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+const SearchOperationLog = (val: any) => {
+  searchdata = val
+  tableLoadingTableData.value = true
+  $api
+    .SearchOperationLog({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: -1,
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+onMounted(() => {
+  Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
+    nextTick(() => {
+      // tableRef.value && autoWidth(tableData.value, tableRef.value)
+    })
+  })
+})
+
+const tableRef = ref<VxeGridInstance>()
+const tableData = ref<VxeGridProps<any>>({
+  border: true,
+  round: true,
+  columns: [],
+  data: [],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  stripe: true,
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  sortConfig: {
+    sortMethod: (params) => {
+      const { data, sortList } = params
+
+      // 如果没有排序条件,直接返回原数据
+      if (sortList.length === 0) return data
+
+      // 对数据进行多重排序
+      const sortedData = [...data].sort((a, b) => {
+        for (const { field, order } of sortList) {
+          const curColumn = tableOriginColumnsField.value.find((item: any) => item.field === field)
+          if (!curColumn) continue
+
+          const typeName = curColumn.type
+          const aValue = a[field]
+          const bValue = b[field]
+
+          const compareResult = (aValue: any, bValue: any) => {
+            // 如果 aValue 或 bValue 是 null 或 undefined,优先处理这些值
+            if (aValue == null && bValue == null) {
+              return 0 // 如果两个值都为 null 或 undefined,视为相等
+            } else if (aValue == null) {
+              return -1 // 如果 aValue 是 null,bValue 不是,则将 aValue 视为较小值
+            } else if (bValue == null) {
+              return 1 // 如果 bValue 是 null,aValue 不是,则将 bValue 视为较小值
+            }
+
+            if (typeName === 'datetime' || typeName === 'date' || typeName === 'time') {
+              return dayjs(aValue).unix() - dayjs(bValue).unix()
+            } else if (isNaN(Number(aValue)) || isNaN(Number(bValue))) {
+              return aValue.localeCompare(bValue)
+            } else {
+              return Number(aValue) - Number(bValue)
+            }
+          }
+
+          const result = compareResult(aValue, bValue)
+          if (result !== 0) {
+            return order === 'asc' ? result : -result
+          }
+        }
+
+        return 0 // 如果所有字段都相等
+      })
+
+      return sortedData
+    }
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+const allTableRef = ref<VxeGridInstance>()
+const allTable = ref<VxeGridProps<any>>({
+  columns: [],
+  data: [],
+  showHeaderOverflow: true,
+  showOverflow: true,
+  scrollY: { enabled: true, oSize: 5, gt: 2 },
+  scrollX: { enabled: true, gt: 2 },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const downloadDialogRef = ref()
+const handleDownload = () => {
+  const curSelectedColumns: string[] = []
+  tableRef.value?.columns?.forEach((item: any) => {
+    if (item.field) {
+      curSelectedColumns.push(item.title)
+    }
+  })
+  downloadDialogRef.value.openDialog(
+    curSelectedColumns,
+    selectedNumber.value || pageInfo.value.total
+  )
+}
+
+const exportLoading = ref(false)
+// 获取导出表格数据
+const getExportTableData = (status: number) => {
+  // 如果有选中表格行数据,那么只到处选中的数据
+  if (selectedNumber.value > 0) {
+    exportTable(status)
+    return
+  }
+  exportLoading.value = true
+  const buildColumnString = (columns: any[]): string => {
+    return columns
+      .filter((item) => item.field)
+      .map((item) => item.title)
+      .join(',')
+  }
+
+  let column = ''
+  if (status === 1) {
+    column = buildColumnString(tableData.value.columns)
+  } else {
+    column = buildColumnString(allTable.value.columns)
+  }
+  $api
+    .OperationLogDownload({
+      selected_fields: column,
+      tmp_search: tempSearch.value
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allTable.value.data = res.data.Data || []
+        nextTick(() => {
+          exportLoading.value = false
+          // 导出数据
+          exportTable(status)
+        })
+      }
+    })
+    .finally(() => {
+      exportLoading.value = false
+    })
+}
+// 导出表格 status: 1 导出当前表格 2 导出所有字段表格
+const exportTable = (status: number) => {
+  const exportConfig: any = {
+    type: 'xlsx',
+    message: false,
+    filename: `Chat Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+  }
+  if (status === 1) {
+    exportConfig.columnFilterMethod = ({ column }: any) => {
+      const index = tableData.value.columns.findIndex((item: any) => item.field === column.field)
+      // 排除复选框列
+      return column.field && index !== -1
+    }
+    exportConfig.columns = tableData.value.columns
+  }
+  if (selectedNumber.value > 0) {
+    exportConfig.dataFilterMethod = ({ row }: any) => {
+      const index = selectedTableData.value.findIndex(
+        (item: any) => item._X_ROW_KEY === row._X_ROW_KEY
+      )
+      return index !== -1
+    }
+  }
+  allTableRef.value?.exportData(exportConfig)
+}
+
+const tableLoadingColumn = ref(false)
+const tableLoadingTableData = ref(false)
+
+const selectedNumber = ref(0)
+const selectedTableData = ref([])
+// 复选框选中事件
+const handleCheckboxChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+const handleCheckAllChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+defineExpose({
+  SearchOperationLog
+})
+</script>
+
+<template>
+  <div
+    style="padding: 0px 20px"
+    class="table-box"
+    v-loading.fullscreen.lock="exportLoading"
+    element-loading-text="Loading..."
+    element-loading-custom-class="element-loading"
+    element-loading-background="rgb(43, 47, 54, 0.7)"
+  >
+    <div class="table-tools">
+      <div class="left-total-records">{{ selectedNumber }} Selected</div>
+      <div class="right-tools-btn">
+        <el-button class="el-button--main el-button--pain-theme" @click="handleDownload">
+          <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+          Download
+        </el-button>
+      </div>
+    </div>
+    <vxe-grid
+      ref="tableRef"
+      v-vloading="tableLoadingTableData || tableLoadingColumn"
+      :height="props.height"
+      :style="{ border: 'none' }"
+      v-bind="tableData"
+      @checkbox-change="handleCheckboxChange"
+      @checkbox-all="handleCheckAllChange"
+    >
+      <!-- 空数据时的插槽 -->
+      <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
+        <VEmpty> </VEmpty>
+      </template>
+    </vxe-grid>
+    <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
+    <div class="bottom-pagination">
+      <div class="left-total-records">Total {{ formatNumber(pageInfo.total) }}</div>
+      <div class="right-pagination">
+        <el-pagination
+          v-model:current-page="pageInfo.pageNo"
+          v-model:page-size="pageInfo.pageSize"
+          :page-sizes="[20, 50, 100, 150]"
+          :pager-count="3"
+          background
+          layout="sizes, prev, pager, next"
+          :total="pageInfo.total"
+          @size-change="getTableData(true)"
+          @current-change="getTableData(true)"
+        />
+      </div>
+    </div>
+    <DownloadDialog @export="getExportTableData" ref="downloadDialogRef" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.table-tools {
+  position: relative;
+  display: flex;
+  justify-content: space-between;
+  height: 48px;
+  padding: 8px 0;
+
+  .left-total-records {
+    font-size: 16px;
+    font-weight: 700;
+    line-height: 32px;
+  }
+}
+
+.bottom-pagination {
+  display: flex;
+  justify-content: space-between;
+  height: 40px;
+  margin-top: -1px;
+  padding: 4px 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 0 0 12px 12px;
+
+  .left-total-records {
+    line-height: 32px;
+  }
+
+  .right-pagination {
+    display: flex;
+    align-items: center;
+  }
+}
+.table-box {
+  position: relative;
+  overflow: hidden;
+
+  .all-table {
+    position: absolute;
+    top: -100000px;
+    width: 20px;
+  }
+}
+</style>

+ 190 - 0
src/views/ChatLog/src/components/TableView/src/components/DownloadDialog.vue

@@ -0,0 +1,190 @@
+<script setup lang="ts">
+const dialogVisible = ref(false)
+
+const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
+  selectedDataNumber.value = slectedDataNumber
+  columns.value = selectedColumns
+  dialogVisible.value = true
+}
+
+const isShowSelectColumn = ref(false)
+
+const downloadFilter = ref(1)
+const selectedDataNumber = ref(0)
+
+const columns = ref()
+
+const emits = defineEmits<{ export: [number] }>()
+const handleDownload = () => {
+  emits('export', downloadFilter.value)
+}
+
+const clearData = () => {
+  isShowSelectColumn.value = false
+  downloadFilter.value = 1
+}
+
+defineExpose({
+  openDialog,
+  handleDownload
+})
+</script>
+
+<template>
+  <div>
+    <el-dialog @close="clearData" v-model="dialogVisible" title="Download File" width="540">
+      <div class="download-dialog">
+        <div class="select-data">
+          <div style="display: inline-block">
+            Select data on your Opeartion Log list:<span style="color: var(--color-theme)">{{
+              selectedDataNumber
+            }}</span>
+          </div>
+        </div>
+        <div class="download-filter">
+          <el-radio-group v-model="downloadFilter">
+            <el-radio :value="1"
+              >Download with selected columns
+              <span class="column-number">{{ columns.length }}</span>
+              <SeeAllIcon v-model="isShowSelectColumn" />
+            </el-radio>
+            <div
+              v-if="isShowSelectColumn"
+              class="select-columns"
+              :class="{ show: isShowSelectColumn }"
+            >
+              <div class="title">Selected columns</div>
+              <div class="content">
+                <div class="column-item" v-for="item in columns" :key="item">{{ item }}</div>
+              </div>
+            </div>
+            <el-radio :value="2">Download with all columns</el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button class="cancel-btn" type="default" @click="dialogVisible = false"
+            >Cancel</el-button
+          >
+          <el-button class="download-btn el-button--dark" @click="handleDownload"
+            ><span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+            Download</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.download-dialog {
+  color: var(--color-neutral-1);
+}
+
+.select-data {
+  font-weight: 700;
+}
+
+.data-filter {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  max-height: 120px;
+  margin-top: 8px;
+  overflow: auto;
+
+  .filter-item {
+    height: 22px;
+    padding: 0px 8px;
+    background-color: var(--color-download-file-filter-tag-bg);
+    border-radius: 12px;
+    line-height: 22px;
+    font-size: 12px;
+  }
+}
+
+.download-filter {
+  margin-top: 16px;
+
+  .el-radio-group {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .el-radio {
+      height: 40px;
+      align-items: center;
+    }
+
+    :deep(.el-radio__label) {
+      margin-top: 2px;
+      font-weight: 700;
+      color: var(--color-neutral-1);
+    }
+
+    .column-number {
+      padding: 3px 5px;
+      background-color: var(--color-theme);
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 700;
+      color: #fff;
+    }
+
+    .see-all-btn {
+      margin-left: 8px;
+      color: var(--color-theme);
+      font-size: 12px;
+    }
+
+    .select-columns {
+      max-height: 350px;
+      padding: 8px;
+      margin-top: 8px;
+      background-color: var(--color-dialog-header-bg);
+      border-radius: 6px;
+      overflow: hidden;
+
+      &.show {
+        max-height: 500px;
+      }
+
+      .title {
+        font-size: 12px;
+        font-weight: 700;
+      }
+
+      .content {
+        display: flex;
+        flex-wrap: wrap;
+        margin-top: 8px;
+        gap: 8px;
+
+        .column-item {
+          height: 22px;
+          padding: 0px 8px;
+          background-color: var(--color-download-file-selected-column-tag-bg);
+          line-height: 22px;
+          border-radius: 12px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  .el-button {
+    height: 40px;
+  }
+
+  .cancel-btn {
+    width: 115px;
+  }
+
+  .download-btn {
+    width: 136px;
+  }
+}
+</style>

+ 55 - 3
src/views/Layout/src/components/Menu/MenuView.vue

@@ -10,7 +10,59 @@ const userStore = useUserStore()
 
 const isCollapse = defineModel<boolean>()
 
-const menuList = ref()
+const menuList = ref([
+  {
+    index: '1',
+    label: 'Dashboard',
+    icon: 'icon_data_fill_b',
+    path: '/dashboard'
+  },
+  {
+    index: '2',
+    label: 'Booking',
+    icon: 'icon_booking__fill_b',
+    path: '/booking'
+  },
+  {
+    index: '3',
+    label: 'Tracking',
+    icon: 'icon_tracking__fill_b',
+    path: '/tracking'
+  },
+  {
+    index: '4',
+    label: 'System Management',
+    icon: 'icon_system__management_fill_b',
+    type: 'list',
+    children: [
+      {
+        index: '4-1',
+        label: 'System Message',
+        path: '/system-message'
+      },
+      {
+        index: '4-2',
+        label: 'System Settings',
+        path: '/SystemSettings'
+      },
+      {
+        index: '4-3',
+        label: 'Operation Log',
+        path: '/Operationlog'
+      },
+      {
+        index: '4-4',
+        label: 'Chat Log',
+        path: '/chat-log'
+      },
+      {
+        index: '4-5',
+        label: 'AI API Log',
+        path: '/ai-api-log'
+      }
+    ]
+  }
+])
 watch(
   () => userStore.userInfo?.uname,
   () => {
@@ -24,7 +76,7 @@ const getMenuList = () => {
     }
   })
 }
-getMenuList()
+// getMenuList()
 //监听窗口大小
 const handler = () => {
   return (() => {
@@ -93,7 +145,7 @@ const changeRouter = (path: any) => {
   sessionStorage.removeItem('bookingTablePageInfo')
   if (localStorage.getItem('loginAI')) {
     localStorage.removeItem('loginAI')
-    emitter.emit('login-success');
+    emitter.emit('login-success')
   }
   isVisible.value = false
   emits('changeVisible', isVisible.value)