feat(web): 审计表体内滚动、时间范围筛选与分页档位
This commit is contained in:
@@ -21,7 +21,7 @@ export function adminAuditRoutes() {
|
||||
method: "get",
|
||||
path: "/audit-logs",
|
||||
summary: "审计日志",
|
||||
description: "分页、时间倒序;可按 domain 过滤(key/payment/member/account)",
|
||||
description: "分页、时间倒序;可按 domain 与 [from,to) 过滤",
|
||||
tags: ["审计"],
|
||||
security: [{ adminSession: [] }],
|
||||
request: {
|
||||
@@ -31,6 +31,8 @@ export function adminAuditRoutes() {
|
||||
domain: z
|
||||
.enum(["key", "payment", "member", "account"])
|
||||
.optional(),
|
||||
from: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
@@ -49,8 +51,12 @@ export function adminAuditRoutes() {
|
||||
200,
|
||||
Math.max(1, Number(q.page_size ?? 50) || 50),
|
||||
);
|
||||
const from = q.from ? new Date(q.from) : undefined;
|
||||
const to = q.to ? new Date(q.to) : undefined;
|
||||
const { rows, total } = await c.get("repos").audit.listPage({
|
||||
domain: q.domain,
|
||||
from: from && !Number.isNaN(from.getTime()) ? from : undefined,
|
||||
to: to && !Number.isNaN(to.getTime()) ? to : undefined,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user