Files
lexgo/admin/src/views/AuditLogs.vue
T

65 lines
5.2 KiB
Vue

<template>
<basic-layout><template #wrapper><el-card>
<div class="toolbar"><h1>{{ kind === 'login' ? '登录日志' : '操作日志' }}</h1><span class="retention">保留最近 90 天记录</span></div>
<el-form :model="filters" class="filters" label-position="top" @submit.prevent="search">
<el-form-item label="账号"><el-input v-model="filters.username" :placeholder="kind === 'login' ? '精确匹配账号' : '操作人或目标账号'" clearable /></el-form-item>
<el-form-item label="结果"><el-select v-model="filters.result"><el-option label="全部" value="" /><el-option label="成功" value="success" /><el-option label="失败" value="failure" /></el-select></el-form-item>
<el-form-item v-if="kind === 'operation'" label="操作"><el-select v-model="filters.action"><el-option label="全部" value="" /><el-option v-for="(label, action) in actionLabels" :key="action" :label="label" :value="action" /></el-select></el-form-item>
<el-form-item label="开始时间"><el-date-picker v-model="filters.from" type="datetime" placeholder="选择开始时间" /></el-form-item>
<el-form-item label="结束时间"><el-date-picker v-model="filters.to" type="datetime" placeholder="选择结束时间" /></el-form-item>
<div class="filter-actions"><el-button type="primary" native-type="submit" :loading="logs.loading">查询</el-button><el-button @click="reset">重置</el-button></div>
</el-form>
<el-alert v-if="logs.error" :title="logs.error" type="error" :closable="false" show-icon />
<el-table v-loading="logs.loading" :data="logs.items" border :empty-text="logs.error ? '加载失败,请重试' : '暂无符合条件的记录'">
<el-table-column label="时间" min-width="180"><template #default="scope">{{ formatDate(scope.row.createdAt) }}</template></el-table-column>
<el-table-column v-if="kind === 'login'" label="账号" prop="username" min-width="150" show-overflow-tooltip />
<template v-else>
<el-table-column label="操作人" prop="actorUsername" min-width="150" show-overflow-tooltip />
<el-table-column label="目标账号" min-width="150" show-overflow-tooltip><template #default="scope">{{ scope.row.targetUsername || '—' }}</template></el-table-column>
<el-table-column label="操作" min-width="120"><template #default="scope">{{ actionLabels[scope.row.action] || '未知操作' }}</template></el-table-column>
</template>
<el-table-column label="结果" width="90"><template #default="scope"><el-tag :type="scope.row.result === 'success' ? 'success' : 'danger'">{{ scope.row.result === 'success' ? '成功' : '失败' }}</el-tag></template></el-table-column>
<el-table-column label="说明" min-width="170"><template #default="scope">{{ reasonLabels[scope.row.reason] || '未知原因' }}</template></el-table-column>
<el-table-column v-if="kind === 'login'" label="IP 地址" prop="ip" min-width="150" show-overflow-tooltip />
</el-table>
<pagination v-show="logs.total" v-model:page="page" v-model:limit="limit" :total="logs.total" :page-sizes="[20, 50, 100]" :auto-scroll="false" layout="total, sizes, prev, pager, next" @pagination="paginate" />
</el-card></template></basic-layout>
</template>
<script>
import BasicLayout from '../layout/BasicLayout.vue'
import Pagination from '../components/Pagination/index.vue'
import { session } from '../store'
import { actionLabels, reasonLabels, createAuditLogLoader } from '../audit-logs.mjs'
const emptyFilters = () => ({ username: '', result: '', action: '', from: null, to: null })
export default {
name: 'AuditLogs', components: { BasicLayout, Pagination },
props: { kind: { type: String, required: true } },
data: () => ({ filters: emptyFilters(), applied: emptyFilters(), page: 1, limit: 20, logs: { items: [], total: 0, loading: false, error: '' }, actionLabels, reasonLabels }),
watch: {
kind() { this.loader.invalidate(); this.reset() },
'$store.state.generation': { flush: 'sync', handler() { this.loader.invalidate(); this.filters = emptyFilters(); this.applied = emptyFilters() } }
},
created() { this.loader = createAuditLogLoader(session, this.logs) },
mounted() { this.load() },
beforeUnmount() { this.loader.invalidate() },
methods: {
load() { return this.loader.load(this.kind, { ...this.applied, page: this.page, limit: this.limit }) },
search() { this.page = 1; this.applied = { ...this.filters }; this.load() },
reset() { this.filters = emptyFilters(); this.search() },
paginate({ page, limit }) { this.page = page; this.limit = limit; this.load() },
formatDate(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString('zh-CN', { hour12: false }) }
}
}
</script>
<style scoped>
.toolbar { display: flex; align-items: baseline; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
h1 { font-size: 20px; margin: 0; }
.retention { color: #909399; font-size: 13px; }
.filters { display: flex; align-items: flex-end; gap: 0 16px; flex-wrap: wrap; }
.filters .el-form-item { width: 200px; }
.filters :deep(.el-date-editor) { width: 100%; }
.filter-actions { display: flex; margin-bottom: 18px; }
.el-alert { margin-bottom: 16px; }
@media (max-width: 600px) { .filters .el-form-item { width: 100%; } }
</style>