Files
order_site/apps/frontend/src/views/admin/AdminAuditLogsView.vue
T
2026-04-08 16:30:42 +08:00

233 lines
6.0 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 { computed, onMounted, ref } from 'vue'
import AdminPaginationBar from '@/components/admin/AdminPaginationBar.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminAuditLogs } from '@/services/admin'
import type { AdminAuditLogItem, AdminPagination } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAuditAction, formatAuditTargetType } from '@/utils/admin-display'
import { adminAuditActionOptions, adminAuditTargetTypeOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
import { stringifyDisplayJson } from '@/utils/date-time'
const loading = ref(true)
const errorMessage = ref('')
const actorUsername = ref('')
const action = ref('')
const targetType = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const items = ref<AdminAuditLogItem[]>([])
const pagination = ref<AdminPagination>({
page: 1,
pageSize: 20,
total: 0,
})
async function loadAuditLogs(page = pagination.value.page) {
if (!hasAdminRole('admin')) {
loading.value = false
return
}
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminAuditLogs({
page,
pageSize: pagination.value.pageSize,
actorUsername: actorUsername.value.trim(),
action: action.value.trim(),
targetType: targetType.value.trim(),
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
})
items.value = response.data.items
pagination.value = response.data.pagination
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取审计日志失败'
} finally {
loading.value = false
}
}
function formatPayload(payload: Record<string, unknown>) {
const text = stringifyDisplayJson(payload, 0)
return text.length > 120 ? `${text.slice(0, 120)}...` : text
}
function resolveTargetLabel(targetType: string, targetId: string) {
return `${formatAuditTargetType(targetType)} #${targetId || '-'}`
}
const totalLabel = computed(() => `共 ${pagination.value.total} 条审计记录`)
onMounted(loadAuditLogs)
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>操作审计</h1>
<p>集中查看高风险后台动作便于排查谁在什么时间改了什么</p>
</div>
<span class="meta-copy">{{ totalLabel }}</span>
</header>
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以访问操作审计</div>
<template v-else>
<section class="filter-bar">
<input v-model="actorUsername" class="text-input" placeholder="操作账号" />
<select v-model="action" class="text-input select-input">
<option v-for="option in adminAuditActionOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<select v-model="targetType" class="text-input select-input">
<option v-for="option in adminAuditTargetTypeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</section>
<section class="filter-bar">
<input v-model="dateFrom" class="text-input" type="date" placeholder="开始日期" />
<input v-model="dateTo" class="text-input" type="date" placeholder="结束日期" />
<el-button round type="primary" @click="() => loadAuditLogs()">查询</el-button>
</section>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">审计日志加载中</div>
<div v-else class="table-card">
<table class="data-table">
<thead>
<tr>
<th>时间</th>
<th>操作人</th>
<th>角色</th>
<th>动作</th>
<th>目标</th>
<th>摘要</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.logId">
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
<td>{{ item.actorUsername }}</td>
<td><AdminStatusTag :status="item.actorRole" /></td>
<td>
<strong>{{ formatAuditAction(item.action) }}</strong>
</td>
<td>
<strong>{{ resolveTargetLabel(item.targetType, item.targetId) }}</strong>
</td>
<td class="payload-cell">{{ formatPayload(item.payload) }}</td>
</tr>
</tbody>
</table>
<AdminPaginationBar
:page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:loading="loading"
@change="loadAuditLogs"
/>
</div>
</template>
</section>
</template>
<style scoped>
.admin-panel {
display: grid;
gap: 16px;
}
.panel-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.panel-header h1 {
margin: 0;
color: #1d3555;
}
.panel-header p {
margin: 8px 0 0;
color: #64748b;
}
.meta-copy {
color: #64748b;
font-size: 13px;
}
.filter-bar,
.table-card,
.empty-block,
.error-copy {
padding: 18px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(86, 108, 138, 0.1);
}
.filter-bar {
display: flex;
gap: 12px;
align-items: center;
}
.error-copy {
color: #b42318;
}
.text-input {
width: 100%;
min-height: 42px;
padding: 0 14px;
border-radius: 14px;
border: 1px solid rgba(86, 108, 138, 0.18);
background: rgba(255, 255, 255, 0.94);
}
.select-input {
appearance: none;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px 10px;
text-align: left;
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
color: #334155;
vertical-align: top;
}
.data-table th {
color: #64748b;
font-size: 13px;
}
.payload-cell {
max-width: 420px;
word-break: break-all;
font-family: var(--font-mono);
font-size: 12px;
}
</style>