@@ -539,3 +539,29 @@
|
|||||||
- 风险与影响:
|
- 风险与影响:
|
||||||
- 影响范围限定在 `/admin/users` 与 `/admin/roles` 页面样式行为,不涉及接口、权限与数据写入链路。
|
- 影响范围限定在 `/admin/users` 与 `/admin/roles` 页面样式行为,不涉及接口、权限与数据写入链路。
|
||||||
- 样式作用域使用页面专用 class,避免影响其他 AntD 表格。
|
- 样式作用域使用页面专用 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`,保证少量数据时表格不坍塌。
|
||||||
|
|
||||||
|
- 风险与影响:
|
||||||
|
- 影响范围限定在上述三页的前端展示层,不涉及接口契约和后端逻辑。
|
||||||
|
- 样式均使用页面专用作用域,避免对其他页面表格产生副作用。
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import Link from "next/link";
|
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 { useAuth } from "@/components/auth-provider";
|
||||||
import { Button, Card } from "@/components/ui-antd";
|
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() {
|
export default function AdminFilesPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [messageApi, messageContextHolder] = antdMessage.useMessage();
|
const [messageApi, messageContextHolder] = antdMessage.useMessage();
|
||||||
@@ -109,6 +113,8 @@ export default function AdminFilesPage() {
|
|||||||
const [moveNewName, setMoveNewName] = useState("");
|
const [moveNewName, setMoveNewName] = useState("");
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploadFileName, setUploadFileName] = useState("");
|
const [uploadFileName, setUploadFileName] = useState("");
|
||||||
|
const [tableScrollY, setTableScrollY] = useState(FILES_TABLE_MIN_SCROLL_Y);
|
||||||
|
const tableScrollAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const canRead = hasPermission("file.read") || hasPermission("file.manage");
|
const canRead = hasPermission("file.read") || hasPermission("file.manage");
|
||||||
const canManage = hasPermission("file.manage");
|
const canManage = hasPermission("file.manage");
|
||||||
@@ -563,6 +569,74 @@ export default function AdminFilesPage() {
|
|||||||
[listData?.breadcrumbs, resetActionPanels],
|
[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<HTMLElement>(".ant-table-wrapper");
|
||||||
|
const tableBody = anchor.querySelector<HTMLElement>(".ant-table-body");
|
||||||
|
const tableContent = anchor.querySelector<HTMLElement>(".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<FileEntryItem>["columns"] = [
|
const columns: TableProps<FileEntryItem>["columns"] = [
|
||||||
{
|
{
|
||||||
title: "名称",
|
title: "名称",
|
||||||
@@ -833,7 +907,11 @@ export default function AdminFilesPage() {
|
|||||||
<Breadcrumb items={breadcrumbItems} />
|
<Breadcrumb items={breadcrumbItems} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div
|
||||||
|
ref={tableScrollAnchorRef}
|
||||||
|
className="admin-files-table-anchor mt-4"
|
||||||
|
style={{ "--admin-files-table-body-min-height": `${tableScrollY}px` } as CSSProperties}
|
||||||
|
>
|
||||||
<AntTable<FileEntryItem>
|
<AntTable<FileEntryItem>
|
||||||
rowKey={(item) => `${item.path}-${item.id}`}
|
rowKey={(item) => `${item.path}-${item.id}`}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -841,7 +919,7 @@ export default function AdminFilesPage() {
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
loading={filesQuery.isLoading || filesQuery.isFetching}
|
loading={filesQuery.isLoading || filesQuery.isFetching}
|
||||||
size="middle"
|
size="middle"
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1100, y: tableScrollY }}
|
||||||
locale={{
|
locale={{
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<Empty
|
<Empty
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Alert, Button, Card, Empty, Form, Input, Space, Spin, Table, Tag, Typography, type CardProps } from "antd";
|
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 { ColumnsType } from "antd/es/table";
|
||||||
import type { ComponentType } from "react";
|
import type { CSSProperties, ComponentType } from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useAuth } from "@/components/auth-provider";
|
import { useAuth } from "@/components/auth-provider";
|
||||||
@@ -292,7 +292,11 @@ export default function AdminSyslogPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div ref={tableScrollAnchorRef} className="mt-4">
|
<div
|
||||||
|
ref={tableScrollAnchorRef}
|
||||||
|
className="admin-syslog-table-anchor mt-4"
|
||||||
|
style={{ "--admin-syslog-table-body-min-height": `${tableScrollY}px` } as CSSProperties}
|
||||||
|
>
|
||||||
<Table<AuditLogItem>
|
<Table<AuditLogItem>
|
||||||
rowKey={(record) => String(record.id)}
|
rowKey={(record) => String(record.id)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
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 {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@@ -482,7 +482,11 @@ export default function AdminSystemParamsPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div ref={tableScrollAnchorRef} className="mt-4">
|
<div
|
||||||
|
ref={tableScrollAnchorRef}
|
||||||
|
className="admin-system-params-table-anchor mt-4"
|
||||||
|
style={{ "--admin-system-params-table-body-min-height": `${tableScrollY}px` } as CSSProperties}
|
||||||
|
>
|
||||||
<Table<SystemParamSummary>
|
<Table<SystemParamSummary>
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={listQuery.isFetching}
|
loading={listQuery.isFetching}
|
||||||
@@ -494,6 +498,7 @@ export default function AdminSystemParamsPage() {
|
|||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
showTotal: (total) => `共 ${total} 条`,
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
style: { marginBottom: 0 },
|
||||||
}}
|
}}
|
||||||
locale={{
|
locale={{
|
||||||
emptyText: <Empty description="未找到符合筛选条件的系统参数。" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
emptyText: <Empty description="未找到符合筛选条件的系统参数。" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||||
|
|||||||
@@ -220,6 +220,18 @@ body {
|
|||||||
min-height: var(--admin-roles-table-body-min-height, 180px);
|
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 {
|
.fquiz-row-selected > td {
|
||||||
background: var(--fquiz-theme-bg-active) !important;
|
background: var(--fquiz-theme-bg-active) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user