Files
vdi-cloud/console/src/components/AuditHistory.vue
T

71 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>