71 lines
6.3 KiB
Vue
71 lines
6.3 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { api, errorMessage, type Audit, type AuditQuery } from '../api'
|
||
import { auditActionLabels } from '../api/audit-labels'
|
||
import StatusTag from './StatusTag.vue'
|
||
|
||
const rows = ref<Audit[]>([]), total = ref(0), page = ref(1), busy = ref(false), exporting = ref(false), error = ref(''), loaded = ref(false)
|
||
const keyword = ref(''), actor = ref(''), action = ref(''), result = ref(''), range = ref<[Date, Date] | null>(null)
|
||
const applied = ref<AuditQuery>({})
|
||
const shortcuts = [1, 7, 30, 90].map(days => ({ text: days === 1 ? '最近 24 小时' : `最近 ${days} 天`, value: () => { const end = new Date(); return [new Date(end.getTime() - days * 86400000), end] } }))
|
||
const results: Record<string, string> = { success: '成功', submitted: '已提交', failed: '失败', timeout: '结果未确认', denied: '无权限' }
|
||
const time = (value: string) => new Date(value).toLocaleString('zh-CN', { hour12: false })
|
||
let generation = 0
|
||
onBeforeUnmount(() => { generation++ })
|
||
async function load(reset = false) {
|
||
if (reset) {
|
||
if (range.value && range.value[1] <= range.value[0]) { ElMessage.warning('结束时间必须晚于开始时间'); return }
|
||
applied.value = { keyword: keyword.value, actor: actor.value, action: action.value, result: result.value, from: range.value?.[0].toISOString(), to: range.value?.[1].toISOString() || new Date().toISOString() }
|
||
page.value = 1
|
||
}
|
||
const request = ++generation
|
||
busy.value = true; error.value = ''; loaded.value = false; rows.value = []
|
||
try {
|
||
const response = await api.audits({ ...applied.value, page: page.value, pageSize: 20 })
|
||
if (request !== generation) return
|
||
rows.value = response.items; total.value = response.total; loaded.value = true
|
||
} catch (e) { if (request === generation) { error.value = errorMessage(e); total.value = 0 } }
|
||
finally { if (request === generation) busy.value = false }
|
||
}
|
||
function clear() { keyword.value = ''; actor.value = ''; action.value = ''; result.value = ''; range.value = null; void load(true) }
|
||
async function download() {
|
||
exporting.value = true
|
||
try {
|
||
const blob = await api.exportAudits(applied.value)
|
||
const url = URL.createObjectURL(blob), link = document.createElement('a')
|
||
link.href = url; link.download = `操作审计-${new Date().toISOString().slice(0, 10)}.csv`; link.click()
|
||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||
ElMessage.success('已生成筛选结果的 CSV 文件')
|
||
} catch (e) { ElMessage.error(errorMessage(e)) } finally { exporting.value = false }
|
||
}
|
||
onMounted(() => { void load(true) })
|
||
</script>
|
||
<template>
|
||
<div class="audit-filters">
|
||
<el-input v-model="keyword" aria-label="审计关键词" placeholder="对象 / 请求标识 / 动作代码" maxlength="200" clearable @keyup.enter="load(true)" />
|
||
<el-input v-model="actor" aria-label="审计操作者" placeholder="操作者包含" maxlength="200" clearable @keyup.enter="load(true)" />
|
||
<el-select v-model="action" aria-label="审计动作" placeholder="全部动作" filterable allow-create clearable><el-option v-for="(label, value) in auditActionLabels" :key="value" :value="value" :label="label" /></el-select>
|
||
<el-select v-model="result" aria-label="审计结果" placeholder="全部结果" filterable allow-create clearable><el-option v-for="(label, value) in results" :key="value" :value="value" :label="label" /></el-select>
|
||
<el-date-picker v-model="range" type="datetimerange" aria-label="审计时间范围" start-placeholder="开始时间" end-placeholder="结束时间" :shortcuts="shortcuts" format="YYYY-MM-DD HH:mm:ss" />
|
||
<div class="audit-actions"><el-button type="primary" :loading="busy" @click="load(true)">查询审计</el-button><el-button :disabled="busy" @click="clear">重置</el-button><el-button :loading="exporting" :disabled="busy || !loaded || !total || total > 10000" @click="download">导出查询结果 CSV</el-button></div>
|
||
</div>
|
||
<p class="subtext audit-hint">不选时间可查询全部历史。导出按已查询条件,包含全部匹配页,单次最多 10000 条。CSV 时间为 UTC,动作与结果保留原始代码。</p>
|
||
<p v-if="loaded && applied.to" class="subtext audit-hint">查询结果截至 {{ time(applied.to) }},更新条件后点击“查询审计”。</p>
|
||
<el-alert v-if="loaded && total > 10000" title="匹配记录超过 10000 条,请缩小时间范围或增加筛选条件后导出。" type="warning" :closable="false" show-icon />
|
||
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
|
||
<el-table v-loading="busy" :data="rows" empty-text="没有匹配的审计记录">
|
||
<el-table-column prop="actor" label="操作者" min-width="110" />
|
||
<el-table-column label="操作" min-width="150"><template #default="{ row }">{{ auditActionLabels[row.action] || row.action }}<div class="mono secondary">{{ row.action }}</div></template></el-table-column>
|
||
<el-table-column prop="target" label="对象" min-width="180" show-overflow-tooltip />
|
||
<el-table-column label="结果" width="125"><template #default="{ row }"><StatusTag :value="row.result" /></template></el-table-column>
|
||
<el-table-column label="时间" min-width="180"><template #default="{ row }">{{ time(row.createdAt) }}</template></el-table-column>
|
||
<el-table-column prop="requestId" label="请求标识" min-width="200" show-overflow-tooltip />
|
||
</el-table>
|
||
<el-pagination v-model:current-page="page" :disabled="busy" :total="total" :page-size="20" layout="total, prev, pager, next" @current-change="load()" />
|
||
</template>
|
||
<style scoped>
|
||
.audit-filters{display:flex;flex-wrap:wrap;gap:12px;margin:18px 0}.audit-filters>.el-input{width:220px}.audit-filters>.el-select{width:180px}.audit-filters :deep(.el-date-editor){width:390px;flex-grow:0;max-width:100%;box-sizing:border-box}.audit-actions{display:flex;gap:10px;flex-wrap:wrap}.audit-actions .el-button{margin:0}.audit-hint{line-height:1.7;margin:10px 0}.el-alert{margin:12px 0}.el-pagination{margin-top:20px;flex-wrap:wrap}.mono{font-size:11px;margin-top:4px;overflow-wrap:anywhere}
|
||
@media(max-width:600px){.audit-filters>.el-input,.audit-filters>.el-select{width:100%}.audit-filters :deep(.el-date-editor){width:100%;min-width:0}.audit-actions{width:100%}.audit-actions .el-button:last-child{width:100%}}
|
||
</style>
|