yeg
2026-09-28 6a4ca47df73d99e952d926f6103bcccea1ecea89
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { listOperationLogs, type OperationLogRecord } from '@/api/operationLogs'
 
interface FilterState {
  id: string
  operator: string
  dateRange: [string, string] | null
}
 
const filters = reactive<FilterState>({ id: '', operator: '全部', dateRange: null })
const appliedFilters = ref<FilterState>({ id: '', operator: '全部', dateRange: null })
const currentPage = ref(1)
const pageSize = ref(10)
const operationLogRecords = ref<OperationLogRecord[]>([])
const loading = ref(false)
const loadError = ref('')
async function loadLogs() {
  loadError.value = ''
  loading.value = true
  try { operationLogRecords.value = await listOperationLogs() } catch (error) {
    operationLogRecords.value = []
    loadError.value = error instanceof Error ? error.message : '操作日志加载失败'
  } finally { loading.value = false }
}
onMounted(loadLogs)
const operatorOptions = computed(() => [...new Set(operationLogRecords.value.map(item => item.operator))].sort((a, b) => a.localeCompare(b, 'zh-CN')))
 
const filteredRecords = computed(() => {
  const query = appliedFilters.value
  return operationLogRecords.value
    .filter(item => !query.id.trim() || String(item.id).includes(query.id.trim()))
    .filter(item => query.operator === '全部' || item.operator === query.operator)
    .filter(item => !query.dateRange || (item.operatedAt.slice(0, 10) >= query.dateRange[0] && item.operatedAt.slice(0, 10) <= query.dateRange[1]))
    .sort((a, b) => b.operatedAt.localeCompare(a.operatedAt))
})
 
const pagedRecords = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value
  return filteredRecords.value.slice(start, start + pageSize.value)
})
 
function applySearch() {
  appliedFilters.value = {
    id: filters.id,
    operator: filters.operator,
    dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null,
  }
  currentPage.value = 1
  ElMessage.success(`已找到 ${filteredRecords.value.length} 条操作日志`)
}
 
function resetSearch() {
  Object.assign(filters, { id: '', operator: '全部', dateRange: null })
  appliedFilters.value = { id: '', operator: '全部', dateRange: null }
  currentPage.value = 1
}
</script>
 
<template>
  <div class="operation-log-page">
    <el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon /><el-button v-if="loadError" @click="loadLogs">重试</el-button>
    <section class="filter-panel" aria-label="操作日志查询条件">
      <header class="section-heading"><div><i></i><strong>查询条件</strong><span>OPERATION LOG QUERY</span></div><small>共 {{ operationLogRecords.length }} 条日志</small></header>
      <div class="filter-grid">
        <label><span>日志ID</span><el-input v-model="filters.id" clearable placeholder="输入日志ID" @keyup.enter="applySearch" /></label>
        <label><span>操作人</span><el-select v-model="filters.operator"><el-option label="全部" value="全部" /><el-option v-for="item in operatorOptions" :key="item" :label="item" :value="item" /></el-select></label>
        <label><span>操作时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" unlink-panels /></label>
        <div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div>
      </div>
    </section>
 
    <section class="result-panel">
      <div class="result-toolbar"><div><i></i><strong>操作日志</strong><span>{{ filteredRecords.length }} RESULTS · 按操作时间倒序</span></div><small>只读审计记录</small></div>
      <div class="table-wrap">
        <table>
          <thead><tr><th>日志ID</th><th>操作人</th><th>操作内容</th><th>操作时间</th></tr></thead>
          <tbody>
            <tr v-for="item in pagedRecords" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td :title="item.operator"><span class="operator-avatar">{{ item.operator.slice(0, 1) }}</span><strong>{{ item.operator }}</strong></td><td :title="item.content">{{ item.content }}</td><td class="time-cell">{{ item.operatedAt }}</td></tr>
            <tr v-if="!pagedRecords.length"><td colspan="4" class="empty-cell">{{ loading ? '正在加载操作日志' : loadError ? '操作日志加载失败' : '暂无符合条件的操作日志' }}</td></tr>
          </tbody>
        </table>
      </div>
      <footer class="pagination-bar"><span>显示 {{ pagedRecords.length }} 条,共 {{ filteredRecords.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="filteredRecords.length" layout="sizes, prev, pager, next" background /></footer>
    </section>
  </div>
</template>
 
<style scoped>
.operation-log-page{width:100%}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.primary-button:hover{background:#176fcb;border-color:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:hover{color:var(--blue);border-color:#9dc8f5}.filter-panel,.result-panel{background:#fff;border:1px solid #d8e1e9}.filter-panel{margin-bottom:16px}.section-heading,.result-toolbar{display:flex;align-items:center;justify-content:space-between;background:var(--surface-soft);border-bottom:1px solid var(--line)}.section-heading{height:44px;padding:0 16px}.section-heading>div,.result-toolbar>div{display:flex;align-items:center;gap:8px}.section-heading>div>i,.result-toolbar>div>i{width:3px;height:16px;background:#2b78a3}.section-heading strong,.result-toolbar strong{color:var(--ink);font-size:15px;font-weight:600}.section-heading span,.result-toolbar span{color:#81909d;font-size:10px}.section-heading small,.result-toolbar>small{color:#647380;font-size:11px}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px;padding:16px}.filter-grid label{width:100%;min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px;font-weight:500}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor){width:100%}.filter-grid :deep(.el-input__wrapper),.filter-grid :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.filter-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.result-toolbar{height:50px;padding:0 16px}.table-wrap{overflow-x:auto}table{width:100%;min-width:900px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}th,td{height:50px;padding:0 14px;border-right:1px solid #dde7f0;border-bottom:1px solid #dde7f0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th{height:40px;color:#52606d;background:#eaf4ff;border-color:#c7e0ff;font-size:12px;font-weight:600}th:nth-child(1){width:130px}th:nth-child(2){width:160px}th:nth-child(3){width:auto}th:nth-child(4){width:190px}th:last-child,td:last-child{border-right:0}tbody tr:hover{background:#f7fbff}tbody tr:last-child td{border-bottom:0}.id-cell,.time-cell{color:#647380;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.operator-avatar{width:24px;height:24px;display:inline-grid;place-items:center;margin-right:8px;color:#2b78a3;background:#e6f2fd;font-size:11px;font-weight:700;vertical-align:middle}td strong{color:#31404d;font-weight:600}.empty-cell{height:180px;color:#8795a3;text-align:center}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px 8px 16px;border-top:1px solid var(--line)}.pagination-bar>span{color:#7b8794;font-size:11px}.pagination-bar :deep(.el-pagination){--el-pagination-button-bg-color:#edf4fa;--el-pagination-hover-color:var(--blue)}@media(max-width:1320px){.filter-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.filter-actions{grid-column:-2/-1}}@media(max-width:980px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.filter-grid{grid-template-columns:1fr;gap:12px;padding:14px 12px}.filter-actions{grid-column:auto;justify-content:stretch}.filter-actions button{flex:1}.section-heading,.result-toolbar{padding-left:12px}.section-heading span,.result-toolbar span{display:none}.pagination-bar{align-items:flex-start;flex-direction:column;overflow-x:auto}}
</style>