fix(web): offset sticky table headers below layout header
This commit is contained in:
@@ -10,7 +10,7 @@ import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { remove, creatorUpdate, creatorGet, creatorAction } from '@/services/api';
|
||||
import { accountReadiness, conflictMessage, dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination } from '@/utils/table';
|
||||
import { fixedLeft, fixedRight, tablePagination, useTableSticky } from '@/utils/table';
|
||||
import AccountLoginModal from '@/components/accounts/AccountLoginModal';
|
||||
import AccountCreateModal from '@/components/accounts/AccountCreateModal';
|
||||
|
||||
@@ -70,6 +70,7 @@ export default function AccountManagementList() {
|
||||
const [actionBusy, setActionBusy] = useState('');
|
||||
const [loginTarget, setLoginTarget] = useState<Row | null>(null);
|
||||
const { message: messageApi } = App.useApp();
|
||||
const sticky = useTableSticky();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -333,7 +334,7 @@ export default function AccountManagementList() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={pending}
|
||||
sticky
|
||||
sticky={sticky}
|
||||
pagination={tablePagination()}
|
||||
scroll={{ x: 'max-content' }}
|
||||
locale={{ emptyText: '暂无账号或待登录环境。' }}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination } from '@/utils/table';
|
||||
import { fixedLeft, fixedRight, tablePagination, useTableSticky } from '@/utils/table';
|
||||
|
||||
interface CompetitorRow {
|
||||
id: string;
|
||||
@@ -46,6 +46,7 @@ export default function MonitoringAccountList() {
|
||||
const [actionBusy, setActionBusy] = useState('');
|
||||
const { message: messageApi } = App.useApp();
|
||||
const horizontal = !Grid.useBreakpoint().xl;
|
||||
const sticky = useTableSticky();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -227,7 +228,7 @@ export default function MonitoringAccountList() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={pending}
|
||||
sticky
|
||||
sticky={sticky}
|
||||
pagination={tablePagination()}
|
||||
locale={{ emptyText: '暂无监控账号。' }}
|
||||
scroll={{ x: horizontal ? 1100 : undefined }}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { create, getList, creatorAction } from '@/services/api';
|
||||
import { conflictMessage, dateTime, extractShareURL, platformForShareURL, platformLabel, shareJobStatus } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination } from '@/utils/table';
|
||||
import { fixedLeft, fixedRight, tablePagination, useTableSticky } from '@/utils/table';
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
@@ -35,6 +35,7 @@ export default function Page() {
|
||||
const [retryID, setRetryID] = useState<string | null>(null);
|
||||
const { message: messageApi } = App.useApp();
|
||||
const horizontal = !Grid.useBreakpoint().xl;
|
||||
const sticky = useTableSticky();
|
||||
|
||||
const loadShareJobs = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -188,7 +189,7 @@ export default function Page() {
|
||||
{error ? (
|
||||
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={loadShareJobs}>重试</Button>} />
|
||||
) : null}
|
||||
<Table rowKey="id" columns={columns} dataSource={jobs} loading={pending} sticky pagination={tablePagination()} scroll={{ x: horizontal ? 1000 : undefined }} locale={{ emptyText: '暂无分享链接解析任务。' }} />
|
||||
<Table rowKey="id" columns={columns} dataSource={jobs} loading={pending} sticky={sticky} pagination={tablePagination()} scroll={{ x: horizontal ? 1000 : undefined }} locale={{ emptyText: '暂无分享链接解析任务。' }} />
|
||||
<Modal open={createOpen} title="创建账号导入任务" onCancel={closeCreate} onOk={createJob} confirmLoading={busy} okText={busy ? '加入中…' : '加入解析队列'} width={560}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="shareText" label="分享内容或链接" required extra="可直接粘贴平台分享文案,系统会自动提取其中的链接并识别作者。">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { create, getList, remove, update } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination } from '@/utils/table';
|
||||
import { fixedLeft, fixedRight, tablePagination, useTableSticky } from '@/utils/table';
|
||||
|
||||
const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
||||
const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/;
|
||||
@@ -96,6 +96,7 @@ export default function Page() {
|
||||
const [created, setCreated] = useState<any>(null);
|
||||
const { message: messageApi } = App.useApp();
|
||||
const horizontal = !Grid.useBreakpoint().xl;
|
||||
const sticky = useTableSticky();
|
||||
const healthReloadTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -234,7 +235,7 @@ export default function Page() {
|
||||
{error ? (
|
||||
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
|
||||
) : null}
|
||||
<Table rowKey="name" columns={columns} dataSource={gateways} loading={pending} sticky pagination={tablePagination()} scroll={{ x: horizontal ? 960 : undefined }} locale={{ emptyText: '暂无网关;注册后环境即可调度到该网关。' }} />
|
||||
<Table rowKey="name" columns={columns} dataSource={gateways} loading={pending} sticky={sticky} pagination={tablePagination()} scroll={{ x: horizontal ? 960 : undefined }} locale={{ emptyText: '暂无网关;注册后环境即可调度到该网关。' }} />
|
||||
<GatewayFormModal
|
||||
open={createOpen}
|
||||
initial={null}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { create, getOne, getList, networkExitAction, remove, update } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination } from '@/utils/table';
|
||||
import { fixedLeft, fixedRight, tablePagination, useTableSticky } from '@/utils/table';
|
||||
|
||||
const healthText: Record<string, string> = {
|
||||
unchecked: '未检测',
|
||||
@@ -114,6 +114,7 @@ export default function Page() {
|
||||
const [busy, setBusy] = useState('');
|
||||
const { message: messageApi } = App.useApp();
|
||||
const horizontal = !Grid.useBreakpoint().xl;
|
||||
const sticky = useTableSticky();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -304,7 +305,7 @@ export default function Page() {
|
||||
columns={columns}
|
||||
dataSource={exits}
|
||||
loading={pending}
|
||||
sticky
|
||||
sticky={sticky}
|
||||
pagination={tablePagination()}
|
||||
scroll={{ x: horizontal ? 1100 : undefined }}
|
||||
locale={{ emptyText: '暂无网络出口。创建并检测健康后,才能绑定运行环境。' }}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
// 列表表格使用页面纵向滚动与 sticky 表头吸顶,不设置表内纵向滚动。
|
||||
// 保留横向滚动、固定首列/操作列与分页(每页 20 条,单页隐藏分页条)。
|
||||
import { ProProvider, RouteContext } from '@ant-design/pro-components';
|
||||
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const TABLE_PAGE_SIZE = 20;
|
||||
|
||||
export function useTableSticky() {
|
||||
const { hasHeader, fixedHeader, layout } = useContext(RouteContext);
|
||||
const { token } = useContext(ProProvider);
|
||||
// ProLayout 的 mix 布局默认固定顶部栏;高度与其原生 token/default 保持一致。
|
||||
const headerHeight = token.layout?.header?.heightLayoutHeader || 56;
|
||||
return { offsetHeader: hasHeader && (fixedHeader || layout === 'mix') ? headerHeight : 0 };
|
||||
}
|
||||
|
||||
export function tablePagination() {
|
||||
return { pageSize: TABLE_PAGE_SIZE, hideOnSinglePage: true };
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ function setup({ accounts = [{ id: 'account-1', name: '真实昵称', business_s
|
||||
async remove(resource, id) { calls.push({ resource, id }); if (options.actionError) throw options.actionError; },
|
||||
};
|
||||
if (id === '@/utils/helpers') return { accountReadiness: () => ({ ready: true, label: '可运行' }), dateTime: value => value, conflictMessage: (error, fallback) => error.message || fallback };
|
||||
if (id === '@/utils/table') return { fixedLeft: column => ({ ...column, fixed: 'left' }), fixedRight: column => ({ ...column, fixed: 'right' }), tablePagination: () => ({ pageSize: 20, hideOnSinglePage: true }) };
|
||||
if (id === '@/utils/table') return { fixedLeft: column => ({ ...column, fixed: 'left' }), fixedRight: column => ({ ...column, fixed: 'right' }), tablePagination: () => ({ pageSize: 20, hideOnSinglePage: true }), useTableSticky: () => ({ offsetHeader: 56 }) };
|
||||
if (id.startsWith('@/components/accounts/')) return { default: marker(id.split('/').at(-1)) };
|
||||
return requireWeb(id);
|
||||
};
|
||||
@@ -72,7 +72,7 @@ for (const rowCount of [1, 20, 100]) {
|
||||
const s = await ready({ accounts: Array.from({ length: rowCount }, (_, i) => ({ id: `account-${i}`, name: `账号 ${i}` })), environments: [] });
|
||||
const { columns, scroll, dataSource, sticky, pagination } = s.find('Table').props;
|
||||
assert.equal(dataSource.length, rowCount);
|
||||
assert.equal(sticky, true);
|
||||
assert.deepEqual(sticky, { offsetHeader: 56 });
|
||||
assert.deepEqual(pagination, { pageSize: 20, hideOnSinglePage: true });
|
||||
assert.equal(columns[0].title, '账号');
|
||||
assert.equal(columns[0].fixed, 'left');
|
||||
|
||||
@@ -32,7 +32,10 @@ for (const [file, width] of files) {
|
||||
test(`${file} 保留表头吸顶、分页和固定列`, () => {
|
||||
assert.equal(tables.length, 1);
|
||||
const props = tables[0].attributes.properties;
|
||||
assert.ok(props.some(prop => prop.name?.getText(source) === 'sticky'));
|
||||
const sticky = props.find(prop => prop.name?.getText(source) === 'sticky');
|
||||
assert.ok(sticky?.initializer && ts.isJsxExpression(sticky.initializer), '表头吸顶必须预留顶部栏高度');
|
||||
assert.equal(sticky.initializer.expression.getText(source), 'sticky');
|
||||
assert.match(code, /const sticky = useTableSticky\(\);/);
|
||||
assert.match(code, /pagination=\{tablePagination\(\)\}/);
|
||||
assert.doesNotMatch(code, /pagination=\{false\}/);
|
||||
assert.match(code, /\.\.\.fixedLeft</);
|
||||
@@ -67,9 +70,45 @@ const tableCode = ts.transpileModule(readFileSync(tableFile, 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const tableModule = { exports: {} };
|
||||
vm.runInNewContext(tableCode, { exports: tableModule.exports }, { filename: tableFile });
|
||||
const routeContext = {}, proProvider = {};
|
||||
let layout = {}, token = {};
|
||||
vm.runInNewContext(tableCode, { exports: tableModule.exports, require: id => {
|
||||
if (id === '@ant-design/pro-components') return { RouteContext: routeContext, ProProvider: proProvider };
|
||||
if (id === 'react') return { useContext: context => context === routeContext ? layout : { token } };
|
||||
throw new Error(`Unexpected import: ${id}`);
|
||||
} }, { filename: tableFile });
|
||||
const helpers = tableModule.exports;
|
||||
|
||||
test('mix 布局默认固定顶部栏,表头须避让;自定义高度随布局更新', () => {
|
||||
for (const height of [56, 72, 96]) {
|
||||
for (const isMobile of [false, true]) {
|
||||
layout = { hasHeader: true, layout: 'mix', fixedHeader: false, isMobile };
|
||||
token = { layout: { header: { heightLayoutHeader: height } } };
|
||||
assert.equal(helpers.useTableSticky().offsetHeader, height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('遵循 ProLayout 默认顶部栏高度', () => {
|
||||
layout = { hasHeader: true, layout: 'mix' };
|
||||
token = {};
|
||||
assert.equal(helpers.useTableSticky().offsetHeader, 56);
|
||||
});
|
||||
|
||||
test('其他布局只在存在固定顶部栏时预留高度', () => {
|
||||
token = { layout: { header: { heightLayoutHeader: 72 } } };
|
||||
for (const kind of ['top', 'side']) {
|
||||
for (const hasHeader of [false, true]) {
|
||||
for (const fixedHeader of [false, true]) {
|
||||
layout = { layout: kind, hasHeader, fixedHeader };
|
||||
assert.equal(helpers.useTableSticky().offsetHeader, hasHeader && fixedHeader ? 72 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
layout = { layout: 'mix', hasHeader: false, fixedHeader: true };
|
||||
assert.equal(helpers.useTableSticky().offsetHeader, 0);
|
||||
});
|
||||
|
||||
test('分页仍为每页 20 条,单页隐藏分页条', () => {
|
||||
assert.equal(helpers.TABLE_PAGE_SIZE, 20);
|
||||
const pagination = helpers.tablePagination();
|
||||
|
||||
Reference in New Issue
Block a user