feat(web): 审计表体内滚动、时间范围筛选与分页档位

This commit is contained in:
2026-10-08 23:27:42 +08:00
parent 66a9a93914
commit 4d31c20d35
6 changed files with 118 additions and 50 deletions
+2
View File
@@ -5,6 +5,8 @@ export async function fetchAuditLogs(params: {
page: number;
page_size: number;
domain?: string;
from?: string;
to?: string;
}): Promise<Paginated<AuditLogItem>> {
const { data } = await adminHttp.get<Paginated<AuditLogItem>>("/audit-logs", {
params,
+2 -2
View File
@@ -31,8 +31,8 @@ export const qk = {
payments: () => ["admin", "payments"] as const,
qpuLatest: () => ["admin", "qpu-latest"] as const,
members: () => ["admin", "members"] as const,
auditLogs: (q: PageQuery & { domain?: string }) =>
["admin", "audit-logs", q.page, q.page_size, q.domain ?? ""] as const,
auditLogs: (q: PageQuery & { domain?: string; from?: string; to?: string }) =>
["admin", "audit-logs", q.page, q.page_size, q.domain ?? "", q.from ?? "", q.to ?? ""] as const,
},
member: {
me: () => ["member", "me"] as const,
+1 -1
View File
@@ -176,7 +176,7 @@ export function useIssueMemberToken() {
}
export function useAuditLogs(
params: () => PageQuery & { domain?: string },
params: () => PageQuery & { domain?: string; from?: string; to?: string },
) {
return useQuery({
queryKey: () => qk.admin.auditLogs(params()),
+102 -46
View File
@@ -1,20 +1,29 @@
<script setup lang="ts">
/**
* 审计日志(交互设计-审计日志):只读时间线 + 域筛选 + 分页
* 表体内部滚动:高度按容器实测,避免内容区被撑出视口
*/
import { computed, ref, watch } from "vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import PageHeader from "@/components/common/page-header.vue";
import TimeRangePicker from "@/components/common/time-range-picker.vue";
import { useAuditLogs } from "@/queries/admin.ts";
import { formatDateTime } from "@/utils/time.ts";
import { formatDateTime, recentDaysRange } from "@/utils/time.ts";
const domain = ref<string | null>(null);
const page = ref(1);
const pageSize = ref(50);
const range = ref(recentDaysRange(30));
const boxRef = ref<HTMLElement | null>(null);
/** 表头 + 分页大约占掉的高度 */
const CHROME = 96;
const bodyY = ref(480);
const params = computed(() => ({
page: page.value,
page_size: pageSize.value,
domain: domain.value ?? undefined,
from: range.value.from,
to: range.value.to,
}));
const auditQuery = useAuditLogs(() => params.value);
@@ -30,22 +39,45 @@ const domainOptions = [
];
const columns = [
{ title: "时间", key: "created_at", width: 170 },
{ title: "操作者", key: "operator", width: 120 },
{ title: "域", key: "domain", width: 90 },
{ title: "操作", key: "action", width: 160 },
{ title: "摘要", key: "summary" },
{ title: "时间", key: "created_at", width: 160 },
{ title: "操作者", key: "operator", width: 100 },
{ title: "域", key: "domain", width: 80 },
{ title: "操作", key: "action", width: 140 },
{ title: "摘要", key: "summary", ellipsis: true },
];
watch(domain, () => {
watch([domain, range], () => {
page.value = 1;
});
let ro: ResizeObserver | null = null;
function measure() {
const h = boxRef.value?.clientHeight ?? 0;
if (h > 0) bodyY.value = Math.max(200, h - CHROME);
}
onMounted(() => {
measure();
if (typeof ResizeObserver !== "undefined" && boxRef.value) {
ro = new ResizeObserver(() => measure());
ro.observe(boxRef.value);
}
window.addEventListener("resize", measure);
});
onUnmounted(() => {
ro?.disconnect();
window.removeEventListener("resize", measure);
});
</script>
<template>
<section>
<section class="audit">
<PageHeader title="审计日志" subtitle="管理操作留痕,只读">
<template #actions>
<span class="audit-range-label">时间范围</span>
<TimeRangePicker v-model="range" />
<a-select
v-model:value="domain"
:options="domainOptions"
@@ -56,52 +88,76 @@ watch(domain, () => {
</template>
</PageHeader>
<a-table
:columns="columns"
:data-source="rows"
:row-key="(r: { id: number }) => r.id"
:loading="auditQuery.isPending.value || auditQuery.isFetching.value"
:pagination="{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['50', '100'],
showTotal: (t: number) => `共 ${t} 条`,
onChange: (p: number, ps: number) => {
page = p;
pageSize = ps;
},
}"
size="middle"
:scroll="{ x: 900 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'created_at'">
{{ formatDateTime(record.created_at) }}
<div ref="boxRef" class="audit__box">
<a-table
:columns="columns"
:data-source="rows"
:row-key="(r: { id: number }) => r.id"
:loading="auditQuery.isPending.value || auditQuery.isFetching.value"
:pagination="{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['20', '50', '100', '200'],
showTotal: (t: number) => `共 ${t} 条`,
onChange: (p: number, ps: number) => {
page = p;
pageSize = ps;
},
}"
size="middle"
:scroll="{ y: bodyY }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'created_at'">
{{ formatDateTime(record.created_at) }}
</template>
<template v-else-if="column.key === 'operator'">
{{ record.operator || "系统" }}
</template>
<template v-else-if="column.key === 'domain'">
<a-tag>{{ record.domain }}</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<span class="audit-action">{{ record.action }}</span>
</template>
<template v-else-if="column.key === 'summary'">
{{ record.summary }}
</template>
</template>
<template v-else-if="column.key === 'operator'">
{{ record.operator || "系统" }}
</template>
<template v-else-if="column.key === 'domain'">
<a-tag>{{ record.domain }}</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<span class="audit-action">{{ record.action }}</span>
</template>
<template v-else-if="column.key === 'summary'">
{{ record.summary }}
</template>
</template>
</a-table>
</a-table>
</div>
</section>
</template>
<style scoped>
/*
* 固定壳:视口高 − 顶栏 64 − 内容区 padding 24×2 − 内容面 padding 24×2
* 表体高度由 ResizeObserver 量出,内容区不随表格增高
*/
.audit {
height: calc(100vh - 64px - 48px - 48px);
display: flex;
flex-direction: column;
min-height: 0;
}
.audit__box {
flex: 1;
min-height: 0;
}
.audit-action {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
.audit-range-label {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
line-height: 32px;
}
</style>