优化系统日志页面展示与表格高度

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-03 20:35:17 +08:00
parent bce0cfece7
commit 424b7225bd
2 changed files with 118 additions and 31 deletions
+93 -31
View File
@@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Alert, Button, Card, Empty, Form, Input, Space, Spin, Table, Tag, Typography, type CardProps } from "antd";
import type { ColumnsType } from "antd/es/table";
import type { ComponentType } from "react";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { useTopicSubscription } from "@/hooks/use-topic-subscription";
@@ -13,6 +13,9 @@ import { readApiError } from "@/lib/api";
import type { AuditLogItem, AuditLogListResponse } from "@/types/auth";
const PAGE_SIZE = 50;
const SYSLOG_TABLE_MIN_SCROLL_Y = 180;
const SYSLOG_TABLE_VIEWPORT_GAP = 40;
const SYSLOG_TABLE_FALLBACK_RESERVE = 220;
const AntCard = Card as unknown as ComponentType<CardProps>;
type Filters = {
@@ -40,6 +43,8 @@ export default function AdminSyslogPage() {
const [offset, setOffset] = useState(0);
const [draftFilters, setDraftFilters] = useState<Filters>(EMPTY_FILTERS);
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
const [tableScrollY, setTableScrollY] = useState(SYSLOG_TABLE_MIN_SCROLL_Y);
const tableScrollAnchorRef = useRef<HTMLDivElement | null>(null);
const canRead = hasPermission("menu.read") || hasPermission("menu.manage");
@@ -127,6 +132,71 @@ export default function AdminSyslogPage() {
[],
);
const updateTableScrollY = useCallback(() => {
if (typeof window === "undefined") {
return;
}
const anchor = tableScrollAnchorRef.current;
if (!anchor) {
return;
}
const anchorTop = anchor.getBoundingClientRect().top;
const tableWrapper = anchor.querySelector<HTMLElement>(".ant-table-wrapper");
const tableBody = anchor.querySelector<HTMLElement>(".ant-table-body");
let nextHeight = Math.floor(window.innerHeight - anchorTop - SYSLOG_TABLE_FALLBACK_RESERVE);
if (tableWrapper) {
const wrapperRect = tableWrapper.getBoundingClientRect();
const bodyHeight = tableBody?.getBoundingClientRect().height ?? SYSLOG_TABLE_MIN_SCROLL_Y;
const nonBodyHeight = Math.max(0, wrapperRect.height - bodyHeight);
const topGap = Math.max(0, wrapperRect.top - anchorTop);
nextHeight = Math.floor(window.innerHeight - anchorTop - topGap - nonBodyHeight - SYSLOG_TABLE_VIEWPORT_GAP);
}
const clampedHeight = Math.max(SYSLOG_TABLE_MIN_SCROLL_Y, nextHeight);
setTableScrollY((previous) => (Math.abs(previous - clampedHeight) <= 1 ? previous : clampedHeight));
}, []);
useEffect(() => {
updateTableScrollY();
}, [error, logs.length, logsQuery.isFetching, total, updateTableScrollY]);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const onViewportChange = () => {
window.requestAnimationFrame(updateTableScrollY);
};
window.addEventListener("resize", onViewportChange);
return () => {
window.removeEventListener("resize", onViewportChange);
};
}, [updateTableScrollY]);
useEffect(() => {
if (typeof window === "undefined" || typeof ResizeObserver === "undefined") {
return;
}
const anchor = tableScrollAnchorRef.current;
if (!anchor) {
return;
}
const resizeObserver = new ResizeObserver(() => {
window.requestAnimationFrame(updateTableScrollY);
});
resizeObserver.observe(anchor);
return () => {
resizeObserver.disconnect();
};
}, [updateTableScrollY]);
if (initializing || logsQuery.isLoading) {
return (
<div className="flex min-h-[240px] items-center justify-center">
@@ -167,11 +237,7 @@ export default function AdminSyslogPage() {
<div className="space-y-6">
{error ? <Alert type="error" showIcon message="日志加载失败" description={error} /> : null}
<AntCard title="系统日志" extra={<Typography.Text type="secondary">auth.login / auth.logout / auth.refresh</Typography.Text>}>
<Typography.Paragraph type="secondary" className="!mb-4">
</Typography.Paragraph>
<AntCard title="系统日志">
<Form layout="inline" style={{ rowGap: 12 }}>
<Form.Item label="动作" className="min-w-[280px]">
<Input
@@ -226,32 +292,28 @@ export default function AdminSyslogPage() {
</Form.Item>
</Form>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
<Typography.Text type="secondary">
{total} {currentPage}
</Typography.Text>
{logsQuery.isFetching ? <Typography.Text type="secondary">...</Typography.Text> : null}
<div ref={tableScrollAnchorRef} className="mt-4">
<Table<AuditLogItem>
rowKey={(record) => String(record.id)}
columns={columns}
dataSource={logs}
loading={logsQuery.isFetching}
pagination={{
current: currentPage,
pageSize: PAGE_SIZE,
total,
onChange: (page) => setOffset((page - 1) * PAGE_SIZE),
showSizeChanger: false,
showQuickJumper: false,
showTotal: (value) => `${value}`,
style: { marginBottom: 0 },
}}
locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无日志数据" />,
}}
scroll={{ x: 980, y: tableScrollY }}
/>
</div>
<Table<AuditLogItem>
rowKey={(record) => String(record.id)}
columns={columns}
dataSource={logs}
loading={logsQuery.isFetching}
pagination={{
current: currentPage,
pageSize: PAGE_SIZE,
total,
onChange: (page) => setOffset((page - 1) * PAGE_SIZE),
showSizeChanger: false,
showQuickJumper: false,
showTotal: (value) => `${value}`,
}}
locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无日志数据" />,
}}
scroll={{ x: 980 }}
/>
</AntCard>
</div>
);