瀏覽代碼

feat:1.新增日志管理模块

wangkun 1 周之前
父節點
當前提交
7aafb7eee0

+ 122 - 0
src/api/sys/sysLog.js

@@ -0,0 +1,122 @@
+import service from '@/utils/request'
+
+/**
+ * 获取日志列表
+ * @param {*} data
+ * @returns
+ */
+export const getLogList = (data) => {
+  return service({
+    url: '/v1/systemLog/list',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 审计日志
+ * @param {*} data
+ * @returns
+ */
+export const auditLogList = (data) => {
+  return service({
+    url: '/v1/systemLog/audit',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 归档日志
+ * @param {*} data
+ * @returns
+ *
+ */
+export const archiveLogList = (data) => {
+  return service({
+    url: '/v1/systemLog/archive',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 获取日志归档列表
+ * @param {*} data
+ * @returns
+ *
+ */
+export const getArchiveLogList = (data) => {
+  return service({
+    url: '/v1/logArchive/list',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 获取日志恢复列表
+ * @param {*} data
+ * @returns
+ *
+ */
+export const getRecoveryLogList = (data) => {
+  return service({
+    url: '/v1/logRestore/list',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 恢复日志
+ * @param {*} data
+ * @returns
+ *
+ */
+export const recoveryLog = (data) => {
+  return service({
+    url: '/v1/logArchive/restore',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 下载日志
+ * @param {*} data
+ * @returns
+ *
+ */
+export const downloadLog = (data) => {
+  return service({
+    url: '/v1/logArchive/download',
+    method: 'post',
+    data,
+    responseType: 'blob' // 关键:指定响应类型为二进制流
+  })
+}
+
+/**
+ * 查看完整链路
+ * @param {*} data
+ * @returns
+ *
+ */
+export const getTraceLog = (data) => {
+  return service({
+    url: '/v1/systemLog/link',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 获取日志详情
+ * @param {*} data
+ * @returns
+ *
+ */
+
+export const getLogDetail = (data) => {
+  return service({
+    url: '/v1/systemLog/info',
+    method: 'post',
+    data
+  })
+}

+ 7 - 1
src/utils/request.js

@@ -53,7 +53,7 @@ service.interceptors.request.use(
       请求数据: config.data
     })
 
-    const isCmsEndpoint = config.url && config.url.includes('cms');
+    const isCmsEndpoint = config.url && config.url.includes('v1');
     // 统一处理 headers
     const userStore = useUserStore()
     config.headers = {
@@ -107,6 +107,12 @@ service.interceptors.response.use(
       userStore.setToken(response.headers['new-token'])
     }
 
+    // 文件下载(blob)不做 JSON 信封判断:原样返回 axios response(含 headers),由调用方自行处理文件流,
+    // 避免 Blob 走 code 判断弹 "undefined" 的错误提示
+    if (response.config.responseType === 'blob') {
+      return response
+    }
+
     if (response.data.code === 100000 || response.headers.success === 'true') {
       if (response.headers.message) {
         response.data.message = decodeURI(response.headers.message)

+ 3 - 13
src/view/ude/access/access.vue

@@ -57,10 +57,10 @@
         style="width: 100%"
         tooltip-effect="dark"
       >
-        <el-table-column align="left" label="服务名称" prop="name" min-width="140" />
+        <el-table-column align="left" label="服务名称" prop="name" width="140" />
         <el-table-column align="left" label="外挂服务IP" prop="ip" width="150" />
         <el-table-column align="left" label="端口" prop="port" width="90" />
-        <el-table-column align="left" label="保护目录" min-width="200">
+<!--        <el-table-column align="left" label="保护目录" min-width="200">
           <template #default="scope">
             <el-tooltip
               v-if="scope.row.directoryText"
@@ -71,7 +71,7 @@
             </el-tooltip>
             <span v-else>-</span>
           </template>
-        </el-table-column>
+        </el-table-column>-->
         <el-table-column align="left" label="接入标识" prop="iden" min-width="140" />
         <el-table-column align="left" label="密钥标识" prop="keyIden" min-width="140" />
         <el-table-column align="left" label="密钥算法" prop="keyAlgo" width="120" />
@@ -82,8 +82,6 @@
               active-value="启用"
               inactive-value="停用"
               inline-prompt
-              active-text="启用"
-              inactive-text="停用"
               @change="changeStatus(scope.row)"
             />
           </template>
@@ -96,32 +94,24 @@
         <el-table-column align="left" label="操作" fixed="right" width="260">
           <template #default="scope">
             <el-button
-              v-preReClick
-              icon="view"
               link
               type="primary"
               @click="viewService(scope.row)"
               >详情
             </el-button>
             <el-button
-              v-preReClick
-              icon="edit"
               link
               type="primary"
               @click="updateServiceInfo(scope.row)"
               >编辑
             </el-button>
             <el-button
-              v-preReClick
-              icon="delete"
               link
               type="primary"
               @click="deleteServiceInfo(scope.row)"
               >删除
             </el-button>
             <el-button
-              v-preReClick
-              icon="refresh"
               link
               type="primary"
               @click="brushServiceInfo(scope.row)"

+ 300 - 0
src/view/ude/logManager/logArchiveList/index.vue

@@ -0,0 +1,300 @@
+<script setup>
+import { ref } from 'vue'
+import { downloadLog, getArchiveLogList, recoveryLog } from '@/api/sys/sysLog'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+defineOptions({
+  name: 'MaskLogArchiveList'
+})
+
+const searchInfo = ref({
+  archiveName: '',
+  archivedBy: '',
+  archivedStartTime: '',
+  archivedEndTime: ''
+})
+const tableData = ref([])
+const elTableRef = ref(null)
+const elSearchFormRef = ref(null)
+const page = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+
+const handleCurrentChange = val => {
+  page.value = val
+  getTableData()
+}
+const handleSizeChange = val => {
+  pageSize.value = val
+  getTableData()
+}
+const onSubmit = () => {
+  getTableData()
+}
+const onReset = () => {
+  searchInfo.value = {
+    archiveName: '',
+    archivedBy: '',
+    archivedStartTime: '',
+    archivedEndTime: ''
+  }
+}
+const getTableData = async () => {
+  const res = await getArchiveLogList({
+    page: page.value,
+    pageSize: pageSize.value,
+    ...searchInfo.value
+  })
+  if (res.code === 100000) {
+    tableData.value = res.data.logArchives
+    total.value = res.data.total
+  }
+}
+getTableData()
+
+const multipleSelection = ref([])
+const handleSelectable = (row, index) => {
+  // 返回false禁止选中该行
+  return true
+}
+const handleSelectionChange = val => {
+  multipleSelection.value = val
+}
+
+// 通用下载工具函数
+const downloadBlob = (blob, fileName) => {
+  const url = URL.createObjectURL(blob)
+  const link = document.createElement('a')
+
+  link.href = url
+  link.download = fileName
+  document.body.appendChild(link)
+  link.click()
+
+  // 清理资源
+  setTimeout(() => {
+    document.body.removeChild(link)
+    URL.revokeObjectURL(url)
+  }, 100)
+}
+
+// 提取文件名
+const getFileNameFromResponse = (response) => {
+  const contentDisposition = response.headers['content-disposition']
+  return contentDisposition
+    ? contentDisposition.split('filename=')[1].replace(/"/g, '')
+    : '日志归档.zip'
+}
+
+// 处理单个或批量下载
+const handleDownload = async(ids, defaultFileName = '日志归档.zip') => {
+
+    const response = await downloadLog({ ids })
+    const fileName = getFileNameFromResponse(response)
+
+    const blob = new Blob([response.data], { type: 'application/zip' })
+    downloadBlob(blob, fileName)
+
+    ElMessage.success('下载开始')
+
+}
+
+// 单个下载
+const singleDownload = (row) => {
+  handleDownload([row.id])
+}
+
+// 批量下载
+const multipleDownload = () => {
+  const selectedRows = multipleSelection.value
+  if (selectedRows.length === 0) {
+    ElMessage.error('请选择要下载的日志!')
+    return
+  }
+  const ids = selectedRows.map(row => row.id)
+  handleDownload(ids)
+}
+const handleRestore = (row) => {
+  ElMessageBox.confirm('是否恢复该归档记录下的日志?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(async() => {
+    await recoveryLog({
+      ids: [row.id]
+    }).then(res => {
+      if (res.code === 100000) {
+        ElMessage.success('归档恢复任务已启动,正在后台处理中,请稍候刷新恢复记录查看进度!')
+        getTableData()
+      }
+    })
+  }).catch(() => {})
+}
+</script>
+
+<template>
+  <div>
+    <div class="gva-search-box flex">
+      <el-form
+        ref="elSearchFormRef"
+        :inline="true"
+        :model="searchInfo"
+        class="demo-form-inline "
+        @keyup.enter="onSubmit"
+      >
+        <el-form-item label="归档任务名称">
+          <el-input
+            v-model="searchInfo.archiveName"
+            placeholder="搜索条件"
+          />
+        </el-form-item>
+        <el-form-item label="归档人">
+          <el-input
+            v-model="searchInfo.archivedBy"
+            placeholder="搜索条件"
+          />
+        </el-form-item>
+        <el-form-item label="归档开始时间">
+          <el-date-picker
+            v-model="searchInfo.archivedStartTime"
+            type="date"
+            value-format="YYYY-MM-DD"
+            placeholder="选择日期"
+          />
+        </el-form-item>
+        <el-form-item label="归档结束时间">
+          <el-date-picker
+            v-model="searchInfo.archivedEndTime"
+            type="date"
+            value-format="YYYY-MM-DD"
+            placeholder="选择日期"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            v-preReClick
+            icon="search"
+            type="primary"
+            @click="onSubmit"
+          >查询
+          </el-button>
+          <el-button
+            icon="refresh"
+            @click="onReset"
+          >重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="gva-table-box">
+      <div class="gva-btn-list">
+        <el-button
+          icon="plus"
+          type="primary"
+          @click="multipleDownload"
+        >
+          批量下载
+        </el-button>
+      </div>
+      <!-- 表格 -->
+      <el-table
+        ref="elTableRef"
+        :data="tableData"
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          :selectable="handleSelectable"
+          type="selection"
+          width="55"
+        />
+        <el-table-column
+          align="center"
+          fixed
+          header-align="center"
+          label="归档任务名称"
+          prop="archiveName"
+          width="200"
+        />
+        <el-table-column
+          align="center"
+          fixed
+          header-align="center"
+          label="归档记录数"
+          prop="recordCount"
+          width="120"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="归档文件地址"
+          prop="archiveFileKey"
+          width="500"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="归档模式"
+          prop="archiveMode"
+          width="100"
+        >
+          <template #default="scope">
+            {{ scope.row.archiveMode === 1 ? '手动' : '自动' }}
+          </template>
+        </el-table-column>
+
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="归档人"
+          prop="archivedBy"
+          width="120"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="归档时间"
+          prop="archivedAt"
+          width="160"
+        />
+        <el-table-column
+          align="center"
+          fixed="right"
+          header-align="center"
+          label="操作"
+        >
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="singleDownload(scope.row)"
+            >下载
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="handleRestore(scope.row)"
+            >恢复
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <!-- 分页栏 -->
+    <div class="gva-pagination">
+      <el-pagination
+        :current-page="page"
+        :page-size="pageSize"
+        :page-sizes="[10, 30, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 696 - 0
src/view/ude/logManager/logList/index.vue

@@ -0,0 +1,696 @@
+<template>
+  <div>
+    <div class="gva-search-box flex">
+      <el-form
+        ref="elSearchFormRef"
+        :inline="true"
+        :model="searchInfo"
+        class="demo-form-inline"
+        @keyup.enter="onSubmit"
+      >
+        <el-form-item label="日志类型">
+          <el-select
+            v-model="logType"
+            placeholder="请选择日志类型"
+            value-key="id"
+            style="width: 150px"
+            @change="onLogTypeChange"
+          >
+            <el-option
+              v-for="item in logTypeOptions"
+              :key="item.id"
+              :label="item.label"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="用户名称">
+          <el-input
+            v-model="searchInfo.userName"
+            placeholder="搜索条件"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            v-preReClick
+            icon="search"
+            type="primary"
+            @click="onSubmit"
+          >查询
+          </el-button>
+          <el-button
+            icon="refresh"
+            @click="onReset"
+          >重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="gva-table-box">
+      <!-- table-header-button -->
+      <!-- 新增批量操作按钮 -->
+      <div class="gva-btn-list">
+        <el-button
+          type="primary"
+          @click="multipleAudit"
+        >
+          批量审计
+        </el-button>
+        <el-button
+          type="primary"
+          @click="multipleArchive"
+        >
+          归档
+        </el-button>
+        <el-button
+          type="primary"
+          @click="archiveByDate"
+        >
+          按日期归档
+        </el-button>
+      </div>
+      <!-- 表格 -->
+      <el-table
+        v-show="logType === 1"
+        ref="elTableRef"
+        :data="tableData"
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          :selectable="handleSelectable"
+          type="selection"
+          width="55"
+        />
+        <el-table-column
+          align="center"
+          fixed
+          header-align="center"
+          label="接口请求路径"
+          prop="uriPath"
+          width="200"
+        />
+
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="用户IP"
+          prop="clientIp"
+          width="200"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="请求耗时"
+          prop="durationMs"
+          width="200"
+          >
+          <template #default="scope">
+            {{ scope.row.durationMs }} ms
+          </template>
+        </el-table-column>
+        <el-table-column
+            align="center"
+            header-align="center"
+            label="操作时间"
+            prop="createdAt"
+            width="300"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="审计状态"
+          prop="auditStatus"
+          width="180"
+        >
+          <template #default="scope">
+            <span :style="getStatusClass(scope.row.auditStatus)">
+              {{ statusMap[scope.row.auditStatus] }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          fixed="right"
+          header-align="center"
+          label="操作"
+        >
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="getDetail(scope.row)"
+            >查看详情
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="audit(scope.row)"
+            >审计
+            </el-button>
+
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-table
+        v-show="logType === 2"
+        ref="elTableRef"
+        :data="tableData"
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          :selectable="handleSelectable"
+          type="selection"
+          width="55"
+        />
+        <el-table-column
+          align="center"
+          fixed
+          header-align="center"
+          label="应用名称"
+          prop="serviceName"
+          width="120"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="请求方式"
+          prop="method"
+          width="150"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="请求者IP"
+          prop="clientIp"
+          width="160"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="响应时间"
+          prop="durationMs"
+          width="80"
+        >
+          <template #default="scope">
+            {{ scope.row.durationMs }} ms
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="错误信息"
+          prop="errorMsg"
+          width="300"
+        />
+        <el-table-column
+          align="center"
+          fixed
+          header-align="center"
+          label="操作时间"
+          prop="createdAt"
+          width="250"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="审计状态"
+          prop="auditStatus"
+          width="80"
+        >
+          <template  #default="scope">
+           <span :style="getStatusClass(scope.row.auditStatus)">
+              {{ statusMap[scope.row.auditStatus] }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          fixed="right"
+          header-align="center"
+          label="操作"
+        >
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="getDetail(scope.row)"
+            >查看详情
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="audit(scope.row)"
+            >审计
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="viewLink(scope.row)"
+            >查看完整链路
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <!-- 分页栏 -->
+    <div class="gva-pagination">
+      <el-pagination
+        :current-page="page"
+        :page-size="pageSize"
+        :page-sizes="[10, 30, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </div>
+    <!-- 详情弹窗 -->
+    <el-dialog
+      v-model="detailDialogVisible"
+      width="70%"
+    >
+      <el-descriptions
+        title="操作日志详情"
+        border
+        :column="1"
+      >
+        <el-descriptions-item label="日志标识">{{ detailData.iden }}</el-descriptions-item>
+        <el-descriptions-item label="全链路TraceId">{{ detailData.traceId }}</el-descriptions-item>
+        <el-descriptions-item label="子链路SpanId">{{ detailData.spanId }}</el-descriptions-item>
+        <el-descriptions-item label="父SpanId">{{ detailData.parentSpanId }}</el-descriptions-item>
+        <el-descriptions-item label="日志类型">{{ detailData.type === 1 ? '操作日志':'接口日志' }}</el-descriptions-item>
+        <el-descriptions-item label="所属服务名称">{{ detailData.serviceName }}</el-descriptions-item>
+        <el-descriptions-item label="接口请求路径">{{ detailData.uriPath }}</el-descriptions-item>
+        <el-descriptions-item label="请求方法">{{ detailData.httpMethod }}</el-descriptions-item>
+        <el-descriptions-item label="业务方法名">{{ detailData.method }}</el-descriptions-item>
+        <el-descriptions-item label="日志级别">{{ detailData.level }}</el-descriptions-item>
+        <el-descriptions-item label="调用接口应用标识">{{ detailData.appKey }}</el-descriptions-item>
+        <el-descriptions-item label="用户ID">{{ detailData.userId }}</el-descriptions-item>
+        <el-descriptions-item label="用户名称">{{ detailData.userName }}</el-descriptions-item>
+        <el-descriptions-item label="用户IP">{{ detailData.clientIp }}</el-descriptions-item>
+        <el-descriptions-item label="请求头">{{ detailData.requestHeader }}</el-descriptions-item>
+        <el-descriptions-item label="请求体摘要">{{ detailData.requestBodyDigest }}</el-descriptions-item>
+        <el-descriptions-item label="请求体FileKey">{{ detailData.requestBodyFileKey }}</el-descriptions-item>
+        <el-descriptions-item label="响应码">{{ detailData.responseCode }}</el-descriptions-item>
+        <el-descriptions-item label="响应体摘要">{{ detailData.responseBodyDigest }}</el-descriptions-item>
+        <el-descriptions-item label="响应体FileKey">{{ detailData.responseBodyFileKey }}</el-descriptions-item>
+        <el-descriptions-item label="错误信息">{{ detailData.errorMsg }}</el-descriptions-item>
+        <el-descriptions-item label="请求耗时">{{ detailData.durationMs }}</el-descriptions-item>
+        <el-descriptions-item label="操作时间">{{ detailData.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="Hmac值">{{ detailData.hmac }}</el-descriptions-item>
+        <el-descriptions-item label="审计状态">{{ statusMap[detailData.auditStatus] }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+    <!-- 按日期归档弹窗 -->
+    <el-dialog
+      v-model="archiveDialogVisible"
+      width="30%"
+    >
+      <el-form
+        ref="elArchiveFormRef"
+        :model="archiveInfo"
+        label-width="100px"
+        :rules="archiveRules"
+        class="demo-form-inline"
+      >
+        <el-form-item label="归档任务名称">
+          <el-input
+            v-model="archiveInfo.name"
+            :placeholder="`${userStore.userInfo.userName}_manual_archive`"
+          />
+        </el-form-item>
+        <el-form-item label="归档起始日期" prop="startDate">
+          <el-date-picker
+            v-model="archiveInfo.startDate"
+            type="date"
+            placeholder="选择日期"
+          />
+        </el-form-item>
+        <el-form-item label="归档结束日期" prop="endDate">
+          <el-date-picker
+            v-model="archiveInfo.endDate"
+            type="date"
+            placeholder="选择日期"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            v-preReClick
+            type="primary"
+            @click="onArchiveSubmit"
+          >归档
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-dialog>
+    <!-- 查看完整链路弹窗 -->
+    <el-dialog
+      v-model="linkDialogVisible"
+      title="日志完整链路"
+      width="70%"
+    >
+      <el-steps direction="horizontal"
+        align-center>
+        <el-step
+            v-for="(activity, index) in activities"
+            :key="activity.id || index"
+            :title="activity.method"
+            :status="activeStep === index + 1 ? 'finish' : 'wait'"
+        >
+          <template #description>
+            <div
+                class="step-description text-sm mb-1 space-y-2"
+                :class="activity.errorMsg ? 'text-red-500' : 'text-gray-500'"
+            >
+              <div class="step-description-wrapper">
+                <div>{{ activity.timestamp }}</div>
+                <div class="text-xs">{{ activity.type === 1 ? '操作日志':'接口日志' }}</div>
+                <div class="text-xs">服务: {{ activity.serviceName }}</div>
+                <div class="text-xs">路径: {{ activity.uriPath }}</div>
+                <div class="text-xs">耗时: {{ activity.durationMs }}ms</div>
+                <div v-if="activity.errorMsg" class="text-xs mt-1">
+                  错误信息: {{ activity.errorMsg }}
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-step>
+      </el-steps>
+
+
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { archiveLogList, auditLogList, getLogDetail, getLogList, getTraceLog } from '@/api/sys/sysLog'
+import { useUserStore } from '@/pinia/modules/user'
+
+defineOptions({
+  name: 'MaskLogList'
+})
+
+const userStore = useUserStore()
+
+const logType = ref(1)
+const statusMap = {
+  1: '未审计',
+  2: '审计通过',
+  3: '审计异常'
+}
+const getStatusClass = (status) => {
+  switch (status) {
+    case 1:
+      return { color: '#f56c6c' }
+    case 2:
+      return { color: '#67C23A' }
+    case 3:
+      return { color: '#E6A23C' }
+    default:
+      return ''
+  }
+}
+const logTypeOptions = ref([
+  { id: 1,
+    label: '操作日志',
+    value: '1'
+  },
+  { id: 2,
+    label: '接口日志',
+    value: '2'
+  }])
+const page = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const handleCurrentChange = val => {
+  page.value = val
+  getTableData()
+}
+const handleSizeChange = val => {
+  pageSize.value = val
+  getTableData()
+}
+
+const tableData = ref([])
+const multipleSelection = ref([])
+const elSearchFormRef = ref(null)
+const searchInfo = ref({
+  userName: ''
+})
+const onSubmit = () => {
+  getTableData()
+}
+const onReset = () => {
+  searchInfo.value = {
+    userName: ''
+  }
+}
+const getTableData = async() => {
+  const res = await getLogList({
+    type: logType.value,
+    page: page.value,
+    pageSize: pageSize.value,
+    ...searchInfo.value,
+  })
+  if (res.code === 100000) {
+    tableData.value = res.data.logs
+    total.value = res.data.total
+  }
+  // 发送请求到后端
+}
+getTableData()
+
+// 切换日志类型时清空上一列表的勾选,避免批量操作作用到不可见行
+const onLogTypeChange = () => {
+  multipleSelection.value = []
+  getTableData()
+}
+
+const detailDialogVisible = ref(false)
+const detailData = ref([])
+const getDetail = async(row) => {
+  detailDialogVisible.value = true
+  detailData.value = row
+}
+const getDetailIn = async(activity) => {
+  detailDialogVisible.value = true
+  await getLogDetail({
+    iden: activity.iden
+  }).then(res => {
+    if (res.code === 100000) {
+      detailData.value = res.data
+    }
+  })
+}
+const multipleAudit = () => {
+  // 检查 multipleSelection 是否为空
+  if (multipleSelection.value.length === 0) {
+    ElMessage.error('请选择要审计的日志!')
+    return
+  }
+  const count = multipleSelection.value.length
+  ElMessageBox.confirm(`确定要审计选中的 ${count} 条日志吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(async() => {
+    const idens = multipleSelection.value.map(item => item.iden)
+    const res = await auditLogList({
+      idens: idens
+    })
+    if (res.code === 100000) {
+      const totalCount = res.data.totalCount
+      const validCount = res.data.validCount
+      const inValidCount = res.data.inValidCount
+      await ElMessageBox.alert(`审计 ${totalCount} 条日志记录完成,其中审计通过: ${validCount} 条,审计异常: ${inValidCount} 条`, '提示', {
+        confirmButtonText: '确定',
+        type: 'success'
+      })
+      getTableData()
+    }
+  }).catch(() => {
+    ElMessage.info('已取消审计操作')
+  })
+}
+const multipleArchive = () => {
+  // 检查 multipleSelection 是否为空
+  if (multipleSelection.value.length === 0) {
+    ElMessage.error('请选择要归档的日志!')
+    return
+  }
+
+  // 获取选中记录的数量
+  const count = multipleSelection.value.length
+  const idens = multipleSelection.value.map(item => item.id)
+
+  // 显示确认对话框
+  ElMessageBox.prompt(`将归档 ${count} 条日志记录。`, '确认归档', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    inputPlaceholder: `${userStore.userInfo.userName}_manual_archive`,
+  }).then(({ value: name }) => {
+    // 用户点击确定且输入有效时执行
+    archiveLogList({
+      ids: idens,
+      name: name || `${userStore.userInfo.userName}_manual_archive`,
+      startDate: '',
+      endDate: ''
+    }).then(res => {
+      if (res.code === 100000) {
+        ElMessage.success(`成功归档${res.data.count}条日志记录`)
+        getTableData()
+      }
+    })
+  }).catch(() => {
+    // 用户取消操作
+    ElMessage.info('已取消归档操作')
+  })
+}
+const archiveInfo = ref({})
+const archiveDialogVisible = ref(false)
+const elArchiveFormRef = ref(null)
+const archiveRules = {
+  startDate: [
+    { required: true, message: '请选择起始日期', trigger: 'change' }
+  ],
+  endDate: [
+    { required: true, message: '请选择结束日期', trigger: 'change' },
+    {
+      validator: (rule, value, callback) => {
+        // 获取 startDate 的值
+        const startDate = archiveInfo.value.startDate
+
+        // 如果 startDate 未选择或 endDate 未选择,直接通过验证(由 required 规则处理)
+        if (!startDate || !value) {
+          return callback()
+        }
+
+        // 确保 endDate 大于 startDate
+        if (new Date(value) <= new Date(startDate)) {
+          callback(new Error('结束日期必须大于起始日期'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ]
+}
+const archiveByDate = () => {
+  archiveDialogVisible.value = true
+}
+
+const elTableRef = ref(null)
+const handleSelectionChange = val => {
+  multipleSelection.value = val
+}
+// 定义一个函数来控制行是否可选中
+const handleSelectable = (row, index) => {
+  // 返回false禁止选中该行
+  return true
+}
+const audit = async(row) => {
+  ElMessageBox.confirm('确定要审计该日志吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(async() => {
+    const idens = [row.iden]
+    const res = await auditLogList({
+      idens: idens
+    })
+    if (res.code === 100000) {
+      const totalCount = res.data.totalCount
+      const validCount = res.data.validCount
+      const inValidCount = res.data.inValidCount
+      await ElMessageBox.alert(`审计 ${totalCount} 条日志记录完成,其中审计通过: ${validCount} 条,审计异常: ${inValidCount} 条`, '提示', {
+        confirmButtonText: '确定',
+        type: 'success'
+      })
+      getTableData()
+    }
+  }).catch(() => {
+    ElMessage({
+      type: 'info',
+      message: '取消审计',
+    })
+  })
+}
+const onArchiveSubmit = () => {
+  // 校验表单(起始/结束日期必选,且结束日期须大于起始日期)
+  elArchiveFormRef.value.validate(async valid => {
+    if (!valid) return
+    const res = await archiveLogList({
+      name: archiveInfo.value.name || `${userStore.userInfo.userName}_manual_archive`,
+      startDate: archiveInfo.value.startDate,
+      endDate: archiveInfo.value.endDate,
+      ids: []
+    })
+    if (res.code === 100000) {
+      ElMessage({
+        type: 'success',
+        message: `成功归档${res.data.count}条日志记录`
+      })
+      archiveDialogVisible.value = false
+      getTableData()
+    }
+  })
+}
+
+const activities = ref([])
+const activeStep = ref(1) // 默认第一个高亮
+const linkDialogVisible = ref(false)
+const viewLink = async(row) => {
+  linkDialogVisible.value = true
+  const res = await getTraceLog({
+    traceId: row.traceId
+  })
+  if (res.code === 100000) {
+    activities.value = res.data.logLinks.map(activity => ({
+      // 需要显示什么参数再改
+      type: activity.type,
+      color: getActivityColor(activity, row.iden),
+      timestamp: activity.createdAt,
+      method: activity.method,
+      serviceName: activity.serviceName,
+      uriPath: activity.uriPath,
+      responseCode: activity.responseCode,
+      durationMs: activity.durationMs,
+      errorMsg: activity.errorMsg,
+      iden: activity.iden,
+    }))
+    // 找到 traceId 对应的 index(注意 el-steps 索引从 1 开始)
+    const idx = activities.value.findIndex(a => a.iden === row.iden)
+    activeStep.value = idx >= 0 ? idx + 1 : 1
+  }
+}
+const getActivityColor = (activity, iden) => {
+  if (activity.iden === iden) return true
+  if (activity.errorMsg !== '') return '#FF0005'
+  return ''
+}
+</script>
+
+<style scoped lang="scss">
+.step-description {
+
+  width: 100%;
+  margin-left: auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center; /* 水平居中 */
+  justify-content: center; /* 垂直居中(如果有固定高度) */
+}
+.step-description-wrapper{
+  text-align: left;
+}
+:deep(.el-step__description) {
+  padding: 0 !important;
+}
+</style>

+ 180 - 0
src/view/ude/logManager/logRecoveryList/index.vue

@@ -0,0 +1,180 @@
+<script setup>
+import { ref } from 'vue'
+import { getRecoveryLogList } from '@/api/sys/sysLog'
+
+defineOptions({
+  name: 'MaskLogRecoveryList'
+})
+
+const searchInfo = ref({
+  restoreStatus: 0,
+  restoreMessage: ''
+})
+const tableData = ref([])
+const elTableRef = ref(null)
+const elSearchFormRef = ref(null)
+const page = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+
+const restoreStatusOptions = [
+  { id: 0,
+    label: '全部',
+    value: '0'
+  },
+  {
+    id: 1,
+    label: '恢复成功',
+    value: '1'
+  },
+  {
+    id: 2,
+    label: '恢复失败',
+    value: '2'
+  },
+]
+const handleCurrentChange = val => {
+  page.value = val
+  getTableData()
+}
+const handleSizeChange = val => {
+  pageSize.value = val
+  getTableData()
+}
+const onSubmit = () => {
+  getTableData()
+}
+const onReset = () => {
+  searchInfo.value = {
+    restoreStatus: 0,
+    restoreMessage: ''
+  }
+}
+const getTableData = async() => {
+  const res = await getRecoveryLogList({
+    page: page.value,
+    pageSize: pageSize.value,
+    ...searchInfo.value
+  })
+  if (res.code === 100000) {
+    tableData.value = res.data.logRestores
+    total.value = res.data.total
+  }
+}
+getTableData()
+</script>
+
+<template>
+  <div>
+    <div class="gva-search-box flex">
+      <el-form
+        ref="elSearchFormRef"
+        :inline="true"
+        :model="searchInfo"
+        class="demo-form-inline"
+        @keyup.enter="onSubmit"
+      >
+        <el-form-item label="恢复状态">
+          <el-select
+            v-model="searchInfo.restoreStatus"
+            value-key="id"
+            placeholder="请选择"
+            @change="getTableData"
+          >
+            <el-option
+              v-for="item in restoreStatusOptions"
+              :key="item.id"
+              :label="item.label"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="恢复说明/错误原因">
+          <el-input
+            v-model="searchInfo.restoreMessage"
+            placeholder="搜索条件"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            v-preReClick
+            icon="search"
+            type="primary"
+            @click="onSubmit"
+          >查询
+          </el-button>
+          <el-button
+            icon="refresh"
+            @click="onReset"
+          >重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="gva-table-box">
+      <!-- 表格 -->
+      <el-table
+        ref="elTableRef"
+        :data="tableData"
+        style="width: 100%"
+        :row-style="{ 'height': '120px' }"
+      >
+        <el-table-column
+          align="center"
+          fixed
+          header-align="center"
+          label="归档记录ID"
+          prop="archiveId"
+          width="120"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="恢复状态"
+          prop="restoreStatus"
+          width="120"
+        >
+          <template #default="scope">
+            {{ scope.row.restoreStatus === 1 ? '成功' : '失败' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="恢复说明/错误原因"
+          prop="restoreMessage"
+          width="500"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="恢复人"
+          prop="restoredBy"
+          width="160"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="恢复时间"
+          prop="restoredAt"
+        />
+      </el-table>
+    </div>
+    <!-- 分页栏 -->
+    <div class="gva-pagination">
+      <el-pagination
+        :current-page="page"
+        :page-size="pageSize"
+        :page-sizes="[10, 30, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+</style>