Files
hfb_sys/frontend/src/features/admin/views/AdminAuditLogsView.vue
T
yml2213 f51aed1f0f 优化审计日志中文展示与动作筛选
补全动作/业务类型中文映射,表格与下拉按分组展示,系统任务操作人可读。
2026-07-13 21:38:16 +08:00

335 lines
8.7 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 { Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminAuditLogs, type AdminAuditLog } from '@/features/admin/api/adminAudit'
import {
AUDIT_BIZ_TYPE_OPTIONS,
auditActionLabel,
auditActionTagType,
auditBizTypeLabel,
groupedAuditActionOptions,
isHighRiskAuditAction,
} from '@/shared/utils/auditLabels'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
const logs = ref<AdminAuditLog[]>([])
const activeLog = ref<AdminAuditLog | null>(null)
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const filters = reactive({
actor_id: '',
action: '',
biz_type: '',
})
const actionGroups = groupedAuditActionOptions()
/** 当前页高风险条数(筛选结果内的本页统计) */
const highRiskCount = computed(
() => logs.value.filter(item => isHighRiskAuditAction(item.action)).length
)
onMounted(loadLogs)
async function loadLogs() {
loading.value = true
try {
const query = {
...filters,
page: currentPage.value,
page_size: currentPageSize.value,
}
const result = await fetchAdminAuditLogs(query)
logs.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
function resetFilters() {
filters.actor_id = ''
filters.action = ''
filters.biz_type = ''
currentPage.value = 1
void loadLogs()
}
async function handlePageChange() {
await loadLogs()
}
function queryLogs() {
currentPage.value = 1
void loadLogs()
}
function actorName(row: AdminAuditLog) {
if (row.actor_type === 'system' || row.actor_id === 0) {
return '系统任务'
}
return row.actor_nickname || row.actor_username || `管理员 ${row.actor_id}`
}
function detailText(row: AdminAuditLog | null) {
if (!row?.detail) return '{}'
return JSON.stringify(row.detail, null, 2)
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Audit Logs</p>
<h1>审计日志</h1>
<p>查看后台管理员操作、业务对象和操作明细,用于追踪冻结、配置修改等高风险动作。</p>
</div>
<div class="toolbar-actions">
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" :icon="Search" :loading="loading" @click="queryLogs"
>查询</el-button
>
</div>
</div>
<div class="metric-grid">
<div class="metric-card">
<span>总条数</span>
<strong>{{ total }} 条</strong>
</div>
<div class="metric-card">
<span>本页高风险</span>
<strong>{{ highRiskCount }} 条</strong>
</div>
</div>
<el-form class="filter-panel" label-position="top" @submit.prevent="queryLogs">
<el-form-item label="管理员 ID">
<el-input
v-model="filters.actor_id"
clearable
placeholder="按管理员 ID 筛选"
@keyup.enter="queryLogs"
@clear="queryLogs"
/>
</el-form-item>
<el-form-item label="动作">
<el-select
v-model="filters.action"
clearable
filterable
placeholder="全部动作"
class="full-control"
@change="queryLogs"
>
<el-option-group v-for="group in actionGroups" :key="group.group" :label="group.group">
<el-option
v-for="item in group.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="业务类型">
<el-select
v-model="filters.biz_type"
clearable
filterable
placeholder="全部业务"
class="full-control"
@change="queryLogs"
>
<el-option
v-for="item in AUDIT_BIZ_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<el-table v-loading="loading" class="table-panel" :data="logs" empty-text="暂无审计记录">
<el-table-column label="操作人" min-width="140">
<template #default="{ row }">
<strong>{{ actorName(row) }}</strong>
</template>
</el-table-column>
<el-table-column label="动作" min-width="170">
<template #default="{ row }">
<el-tag :type="auditActionTagType(row.action)" effect="light">
{{ auditActionLabel(row.action) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="业务类型" width="110">
<template #default="{ row }">{{ auditBizTypeLabel(row.biz_type) }}</template>
</el-table-column>
<el-table-column prop="biz_id" label="业务 ID" width="100" />
<el-table-column label="时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="activeLog = row">明细</el-button>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:loading="loading"
@page-change="handlePageChange"
/>
<el-dialog
:model-value="!!activeLog"
title="审计明细"
width="720px"
@update:model-value="activeLog = null"
>
<div v-if="activeLog" class="dialog-body audit-detail">
<div class="detail-summary">
<div>
<span>动作</span>
<strong>
<el-tag :type="auditActionTagType(activeLog.action)" effect="light">
{{ auditActionLabel(activeLog.action) }}
</el-tag>
</strong>
<small v-if="auditActionLabel(activeLog.action) !== activeLog.action" class="code-hint">
{{ activeLog.action }}
</small>
</div>
<div>
<span>业务</span>
<strong
>{{ auditBizTypeLabel(activeLog.biz_type) }} #{{ activeLog.biz_id || '-' }}</strong
>
</div>
<div>
<span>操作人</span>
<strong>{{ actorName(activeLog) }}</strong>
</div>
<div>
<span>时间</span>
<strong>{{ formatDateTime(activeLog.created_at) }}</strong>
</div>
<div>
<span>IP</span>
<strong>{{ activeLog.ip || '-' }}</strong>
</div>
<div class="wide">
<span>User-Agent</span>
<strong class="ua">{{ activeLog.user_agent || '-' }}</strong>
</div>
</div>
<div class="code-panel">
<div class="code-panel-title">操作明细</div>
<pre>{{ detailText(activeLog) }}</pre>
</div>
</div>
<template #footer>
<el-button type="primary" @click="activeLog = null">关闭</el-button>
</template>
</el-dialog>
</section>
</template>
<style scoped>
.audit-detail {
display: grid;
gap: 16px;
}
.detail-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.detail-summary > div {
display: grid;
gap: 6px;
padding: 12px 14px;
border: 1px solid #eef2f7;
border-radius: 8px;
background: #f8fafc;
}
.detail-summary > div.wide {
grid-column: 1 / -1;
}
.detail-summary span {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.detail-summary strong {
color: #0f172a;
font-size: 14px;
font-weight: 700;
overflow-wrap: anywhere;
}
.detail-summary .ua {
font-size: 12px;
font-weight: 500;
color: #475569;
}
.code-hint {
display: block;
margin-top: 4px;
color: #94a3b8;
font-size: 12px;
font-weight: 500;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.code-panel {
overflow: hidden;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #0f172a;
}
.code-panel-title {
padding: 10px 14px;
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
color: #94a3b8;
font-size: 12px;
font-weight: 700;
}
.code-panel pre {
margin: 0;
padding: 14px;
overflow: auto;
max-height: 360px;
color: #e2e8f0;
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 720px) {
.detail-summary {
grid-template-columns: 1fr;
}
}
</style>