功能:完善通用云管工作台、审计与诊断能力

This commit is contained in:
songyiming committed 2026-11-11 18:46:16 +08:00
1 parent 5ac692724d
commit 9f05aa20ea
225 files changed
+14266 -141

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
<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>