huoyan-enterprise/admin-frontend/src/views/AuditLog.vue

174 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.

<template>
<div>
<h2 class="h4 mb-4">知识库操作日志</h2>
<!-- 筛选区 -->
<div class="card card-body mb-4 shadow-sm">
<div class="row g-2 align-items-end">
<div class="col-6 col-md-3">
<label class="form-label small mb-0">操作类型</label>
<select v-model="filter.action" class="form-select form-select-sm">
<option value="">全部</option>
<option value="upload">upload上传</option>
<option value="download">download下载</option>
<option value="delete">delete删除</option>
<option value="create_kb">create_kb创建知识库</option>
<option value="delete_kb">delete_kb删除知识库</option>
<option value="permission_change">permission_change权限变更</option>
</select>
</div>
<div class="col-6 col-md-2">
<label class="form-label small mb-0">操作者 ID</label>
<input v-model.number="filter.actor_id" type="number" class="form-control form-control-sm" placeholder="留空=全部" />
</div>
<div class="col-6 col-md-2">
<label class="form-label small mb-0">知识库 ID</label>
<input v-model.number="filter.kb_id" type="number" class="form-control form-control-sm" placeholder="留空=全部" />
</div>
<div class="col-auto d-flex gap-2">
<button class="btn btn-sm btn-primary" @click="applyFilter">搜索</button>
<button class="btn btn-sm btn-outline-secondary" @click="resetFilter">重置</button>
</div>
</div>
</div>
<div v-if="loading" class="text-muted">加载中…</div>
<div v-else>
<div class="table-responsive">
<table class="table table-sm table-hover bg-white shadow-sm align-middle small">
<thead class="table-light">
<tr>
<th>时间</th>
<th>操作者</th>
<th>操作</th>
<th>被操作用户</th>
<th>部门</th>
<th>知识库</th>
<th>文件</th>
<th>IP</th>
<th>详情</th>
</tr>
</thead>
<tbody>
<tr v-for="r in items" :key="r.id">
<td class="text-nowrap text-muted">{{ fmt(r.created_at) }}</td>
<td>
<span class="fw-semibold">{{ r.actor_name || r.actor_id }}</span>
</td>
<td>
<span class="badge" :class="actionBadge(r.action)">{{ r.action }}</span>
</td>
<td>{{ r.target_name || (r.target_user_id ? '#' + r.target_user_id : '—') }}</td>
<td>{{ r.department_name || (r.department_id ? '#' + r.department_id : '—') }}</td>
<td>{{ r.kb_name || (r.kb_id ? '#' + r.kb_id : '—') }}</td>
<td class="text-truncate" style="max-width:140px" :title="r.file_name">
{{ r.file_name || '—' }}
</td>
<td>{{ r.ip || '—' }}</td>
<td>
<span v-if="r.metadata" class="text-muted small">{{ metaStr(r.metadata) }}</span>
<span v-else>—</span>
</td>
</tr>
</tbody>
</table>
<p v-if="!items.length" class="text-muted small">暂无日志记录。</p>
</div>
<!-- 分页 -->
<nav v-if="total > pageSize" class="mt-2">
<ul class="pagination pagination-sm">
<li class="page-item" :class="{ disabled: page <= 1 }">
<a class="page-link" href="#" @click.prevent="page > 1 && page-- && load()">上一页</a>
</li>
<li class="page-item disabled">
<span class="page-link">{{ page }} / {{ totalPages }}(共 {{ total }} 条)</span>
</li>
<li class="page-item" :class="{ disabled: page >= totalPages }">
<a class="page-link" href="#" @click.prevent="page < totalPages && page++ && load()">下一页</a>
</li>
</ul>
</nav>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import http from "../api/http";
const items = ref([]);
const total = ref(0);
const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const filter = reactive({ action: "", actor_id: null, kb_id: null });
const applied = reactive({ action: "", actor_id: null, kb_id: null });
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)));
async function load() {
loading.value = true;
try {
const params = { page: page.value, page_size: pageSize };
if (applied.action) params.action = applied.action;
if (applied.actor_id) params.actor_id = applied.actor_id;
if (applied.kb_id) params.kb_id = applied.kb_id;
const { data } = await http.get("/admin/audit-logs", { params });
const raw = data?.data;
items.value = raw?.items || [];
total.value = raw?.total || 0;
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
}
function applyFilter() {
Object.assign(applied, filter);
page.value = 1;
load();
}
function resetFilter() {
Object.assign(filter, { action: "", actor_id: null, kb_id: null });
Object.assign(applied, { action: "", actor_id: null, kb_id: null });
page.value = 1;
load();
}
function fmt(t) {
if (!t) return "—";
try { return new Date(t).toLocaleString("zh-CN"); } catch { return t; }
}
function actionBadge(action) {
const m = {
upload: "bg-success",
download: "bg-info text-dark",
delete: "bg-danger",
archive: "bg-secondary",
create_kb: "bg-primary",
delete_kb: "bg-danger",
permission_change: "bg-warning text-dark",
};
return m[action] || "bg-secondary";
}
function metaStr(meta) {
if (!meta) return "";
try {
const obj = typeof meta === "string" ? JSON.parse(meta) : meta;
return Object.entries(obj)
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
.join(" ");
} catch {
return String(meta);
}
}
onMounted(load);
</script>