feat(web): 审计表体内滚动、时间范围筛选与分页档位
This commit is contained in:
@@ -21,7 +21,7 @@ export function adminAuditRoutes() {
|
|||||||
method: "get",
|
method: "get",
|
||||||
path: "/audit-logs",
|
path: "/audit-logs",
|
||||||
summary: "审计日志",
|
summary: "审计日志",
|
||||||
description: "分页、时间倒序;可按 domain 过滤(key/payment/member/account)",
|
description: "分页、时间倒序;可按 domain 与 [from,to) 过滤",
|
||||||
tags: ["审计"],
|
tags: ["审计"],
|
||||||
security: [{ adminSession: [] }],
|
security: [{ adminSession: [] }],
|
||||||
request: {
|
request: {
|
||||||
@@ -31,6 +31,8 @@ export function adminAuditRoutes() {
|
|||||||
domain: z
|
domain: z
|
||||||
.enum(["key", "payment", "member", "account"])
|
.enum(["key", "payment", "member", "account"])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
from: z.string().optional(),
|
||||||
|
to: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
@@ -49,8 +51,12 @@ export function adminAuditRoutes() {
|
|||||||
200,
|
200,
|
||||||
Math.max(1, Number(q.page_size ?? 50) || 50),
|
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({
|
const { rows, total } = await c.get("repos").audit.listPage({
|
||||||
domain: q.domain,
|
domain: q.domain,
|
||||||
|
from: from && !Number.isNaN(from.getTime()) ? from : undefined,
|
||||||
|
to: to && !Number.isNaN(to.getTime()) ? to : undefined,
|
||||||
page,
|
page,
|
||||||
page_size,
|
page_size,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export async function fetchAuditLogs(params: {
|
|||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
}): Promise<Paginated<AuditLogItem>> {
|
}): Promise<Paginated<AuditLogItem>> {
|
||||||
const { data } = await adminHttp.get<Paginated<AuditLogItem>>("/audit-logs", {
|
const { data } = await adminHttp.get<Paginated<AuditLogItem>>("/audit-logs", {
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export const qk = {
|
|||||||
payments: () => ["admin", "payments"] as const,
|
payments: () => ["admin", "payments"] as const,
|
||||||
qpuLatest: () => ["admin", "qpu-latest"] as const,
|
qpuLatest: () => ["admin", "qpu-latest"] as const,
|
||||||
members: () => ["admin", "members"] as const,
|
members: () => ["admin", "members"] as const,
|
||||||
auditLogs: (q: PageQuery & { domain?: string }) =>
|
auditLogs: (q: PageQuery & { domain?: string; from?: string; to?: string }) =>
|
||||||
["admin", "audit-logs", q.page, q.page_size, q.domain ?? ""] as const,
|
["admin", "audit-logs", q.page, q.page_size, q.domain ?? "", q.from ?? "", q.to ?? ""] as const,
|
||||||
},
|
},
|
||||||
member: {
|
member: {
|
||||||
me: () => ["member", "me"] as const,
|
me: () => ["member", "me"] as const,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export function useIssueMemberToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAuditLogs(
|
export function useAuditLogs(
|
||||||
params: () => PageQuery & { domain?: string },
|
params: () => PageQuery & { domain?: string; from?: string; to?: string },
|
||||||
) {
|
) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: () => qk.admin.auditLogs(params()),
|
queryKey: () => qk.admin.auditLogs(params()),
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<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 PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import { useAuditLogs } from "@/queries/admin.ts";
|
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 domain = ref<string | null>(null);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(50);
|
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(() => ({
|
const params = computed(() => ({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
page_size: pageSize.value,
|
page_size: pageSize.value,
|
||||||
domain: domain.value ?? undefined,
|
domain: domain.value ?? undefined,
|
||||||
|
from: range.value.from,
|
||||||
|
to: range.value.to,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const auditQuery = useAuditLogs(() => params.value);
|
const auditQuery = useAuditLogs(() => params.value);
|
||||||
@@ -30,22 +39,45 @@ const domainOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: "时间", key: "created_at", width: 170 },
|
{ title: "时间", key: "created_at", width: 160 },
|
||||||
{ title: "操作者", key: "operator", width: 120 },
|
{ title: "操作者", key: "operator", width: 100 },
|
||||||
{ title: "域", key: "domain", width: 90 },
|
{ title: "域", key: "domain", width: 80 },
|
||||||
{ title: "操作", key: "action", width: 160 },
|
{ title: "操作", key: "action", width: 140 },
|
||||||
{ title: "摘要", key: "summary" },
|
{ title: "摘要", key: "summary", ellipsis: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
watch(domain, () => {
|
watch([domain, range], () => {
|
||||||
page.value = 1;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section class="audit">
|
||||||
<PageHeader title="审计日志" subtitle="管理操作留痕,只读">
|
<PageHeader title="审计日志" subtitle="管理操作留痕,只读">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
<span class="audit-range-label">时间范围</span>
|
||||||
|
<TimeRangePicker v-model="range" />
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="domain"
|
v-model:value="domain"
|
||||||
:options="domainOptions"
|
:options="domainOptions"
|
||||||
@@ -56,6 +88,7 @@ watch(domain, () => {
|
|||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
<div ref="boxRef" class="audit__box">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="rows"
|
:data-source="rows"
|
||||||
@@ -66,7 +99,7 @@ watch(domain, () => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
total,
|
total,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSizeOptions: ['50', '100'],
|
pageSizeOptions: ['20', '50', '100', '200'],
|
||||||
showTotal: (t: number) => `共 ${t} 条`,
|
showTotal: (t: number) => `共 ${t} 条`,
|
||||||
onChange: (p: number, ps: number) => {
|
onChange: (p: number, ps: number) => {
|
||||||
page = p;
|
page = p;
|
||||||
@@ -74,7 +107,7 @@ watch(domain, () => {
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
size="middle"
|
size="middle"
|
||||||
:scroll="{ x: 900 }"
|
:scroll="{ y: bodyY }"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'created_at'">
|
<template v-if="column.key === 'created_at'">
|
||||||
@@ -94,14 +127,37 @@ watch(domain, () => {
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.audit-action {
|
||||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||||
monospace;
|
monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(0, 0, 0, 0.65);
|
color: rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audit-range-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -323,10 +323,14 @@ export function createRepos(db: Db) {
|
|||||||
},
|
},
|
||||||
async listPage(params: {
|
async listPage(params: {
|
||||||
domain?: string;
|
domain?: string;
|
||||||
|
from?: Date;
|
||||||
|
to?: Date;
|
||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
}): Promise<{ rows: AuditLogRow[]; total: number }> {
|
}): Promise<{ rows: AuditLogRow[]; total: number }> {
|
||||||
const conds = params.domain ? [eq(auditLogs.domain, params.domain)] : [];
|
const conds = params.domain ? [eq(auditLogs.domain, params.domain)] : [];
|
||||||
|
if (params.from) conds.push(gte(auditLogs.created_at, params.from));
|
||||||
|
if (params.to) conds.push(lt(auditLogs.created_at, params.to));
|
||||||
const totalRows = await db
|
const totalRows = await db
|
||||||
.select({ n: count() })
|
.select({ n: count() })
|
||||||
.from(auditLogs)
|
.from(auditLogs)
|
||||||
|
|||||||
Reference in New Issue
Block a user