diff --git a/memory/2026-05-03.md b/memory/2026-05-03.md index 6453e96..417f9c6 100644 --- a/memory/2026-05-03.md +++ b/memory/2026-05-03.md @@ -539,3 +539,29 @@ - 风险与影响: - 影响范围限定在 `/admin/users` 与 `/admin/roles` 页面样式行为,不涉及接口、权限与数据写入链路。 - 样式作用域使用页面专用 class,避免影响其他 AntD 表格。 + +## Work Log - 系统参数/系统日志/文件管理页表格防坍塌(2026-05-03) + +- 背景: + - 在菜单、用户、角色页完成“筛选数据较少时表格高度不坍塌”后,新增需求要求将该体验同步到系统参数、系统日志、文件管理页面。 + +- 本次改动(最小闭环): + - 文件:`web/src/app/admin/system-params/page.tsx` + - 表格外层新增 `admin-system-params-table-anchor`,注入动态 `min-height` 变量。 + - 分页补齐 `style: { marginBottom: 0 }`,收敛底部留白。 + - 文件:`web/src/app/admin/syslog/page.tsx` + - 表格外层新增 `admin-syslog-table-anchor`,注入动态 `min-height` 变量。 + - 文件:`web/src/app/admin/files/page.tsx` + - 新增与其他管理页一致的动态表格高度计算(`tableScrollY` + `ResizeObserver` + `resize` 监听)。 + - 表格滚动从 `scroll={{ x: 1100 }}` 调整为 `scroll={{ x: 1100, y: tableScrollY }}`。 + - 表格外层新增 `admin-files-table-anchor`,注入动态 `min-height` 变量。 + - 文件:`web/src/app/globals.css` + - 新增三个局部样式: + - `.admin-system-params-table-anchor .ant-table-body` + - `.admin-syslog-table-anchor .ant-table-body` + - `.admin-files-table-anchor .ant-table-body` + - 三者均使用页面注入变量作为 `min-height`,保证少量数据时表格不坍塌。 + +- 风险与影响: + - 影响范围限定在上述三页的前端展示层,不涉及接口契约和后端逻辑。 + - 样式均使用页面专用作用域,避免对其他页面表格产生副作用。 diff --git a/web/src/app/admin/files/page.tsx b/web/src/app/admin/files/page.tsx index ee3acf5..e8dfdf8 100644 --- a/web/src/app/admin/files/page.tsx +++ b/web/src/app/admin/files/page.tsx @@ -34,7 +34,7 @@ import { MoreOutlined, } from "@ant-design/icons"; import Link from "next/link"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"; import { useAuth } from "@/components/auth-provider"; import { Button, Card } from "@/components/ui-antd"; @@ -88,6 +88,10 @@ function readXhrError(xhr: XMLHttpRequest): string { } } +const FILES_TABLE_MIN_SCROLL_Y = 180; +const FILES_TABLE_VIEWPORT_GAP = 40; +const FILES_TABLE_FALLBACK_RESERVE = 220; + export default function AdminFilesPage() { const queryClient = useQueryClient(); const [messageApi, messageContextHolder] = antdMessage.useMessage(); @@ -109,6 +113,8 @@ export default function AdminFilesPage() { const [moveNewName, setMoveNewName] = useState(""); const [uploadProgress, setUploadProgress] = useState(0); const [uploadFileName, setUploadFileName] = useState(""); + const [tableScrollY, setTableScrollY] = useState(FILES_TABLE_MIN_SCROLL_Y); + const tableScrollAnchorRef = useRef(null); const canRead = hasPermission("file.read") || hasPermission("file.manage"); const canManage = hasPermission("file.manage"); @@ -563,6 +569,74 @@ export default function AdminFilesPage() { [listData?.breadcrumbs, resetActionPanels], ); + const updateTableScrollY = useCallback(() => { + if (typeof window === "undefined") { + return; + } + const anchor = tableScrollAnchorRef.current; + if (!anchor) { + return; + } + + const anchorTop = anchor.getBoundingClientRect().top; + const tableWrapper = anchor.querySelector(".ant-table-wrapper"); + const tableBody = anchor.querySelector(".ant-table-body"); + const tableContent = anchor.querySelector(".ant-table-content"); + + let nextHeight = Math.floor(window.innerHeight - anchorTop - FILES_TABLE_FALLBACK_RESERVE); + if (tableWrapper) { + const wrapperRect = tableWrapper.getBoundingClientRect(); + const bodyHeight = tableBody?.getBoundingClientRect().height + ?? tableContent?.getBoundingClientRect().height + ?? FILES_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 - FILES_TABLE_VIEWPORT_GAP); + } + + const clampedHeight = Math.max(FILES_TABLE_MIN_SCROLL_Y, nextHeight); + setTableScrollY((previous) => (Math.abs(previous - clampedHeight) <= 1 ? previous : clampedHeight)); + }, []); + + useEffect(() => { + updateTableScrollY(); + }, [errorMessage, filesQuery.isFetching, items.length, listError, 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]); + const columns: TableProps["columns"] = [ { title: "名称", @@ -833,7 +907,11 @@ export default function AdminFilesPage() { -
+
rowKey={(item) => `${item.path}-${item.id}`} columns={columns} @@ -841,7 +919,7 @@ export default function AdminFilesPage() { pagination={false} loading={filesQuery.isLoading || filesQuery.isFetching} size="middle" - scroll={{ x: 1100 }} + scroll={{ x: 1100, y: tableScrollY }} locale={{ emptyText: ( -
+
rowKey={(record) => String(record.id)} columns={columns} diff --git a/web/src/app/admin/system-params/page.tsx b/web/src/app/admin/system-params/page.tsx index c6c4db2..83aae9d 100644 --- a/web/src/app/admin/system-params/page.tsx +++ b/web/src/app/admin/system-params/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"; import { Alert, Button, @@ -482,7 +482,11 @@ export default function AdminSystemParamsPage() { -
+
rowKey="id" loading={listQuery.isFetching} @@ -494,6 +498,7 @@ export default function AdminSystemParamsPage() { showSizeChanger: true, pageSizeOptions: [10, 20, 50, 100], showTotal: (total) => `共 ${total} 条`, + style: { marginBottom: 0 }, }} locale={{ emptyText: , diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 7f8c419..0605c5d 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -220,6 +220,18 @@ body { min-height: var(--admin-roles-table-body-min-height, 180px); } +.admin-system-params-table-anchor .ant-table-body { + min-height: var(--admin-system-params-table-body-min-height, 180px); +} + +.admin-syslog-table-anchor .ant-table-body { + min-height: var(--admin-syslog-table-body-min-height, 180px); +} + +.admin-files-table-anchor .ant-table-body { + min-height: var(--admin-files-table-body-min-height, 180px); +} + .fquiz-row-selected > td { background: var(--fquiz-theme-bg-active) !important; }