fix(web): use page scrolling for all tables

This commit is contained in:
2026-10-06 14:15:34 +08:00
parent 881afb6203
commit 3d668769f6
8 changed files with 117 additions and 77 deletions
@@ -7,7 +7,7 @@ const ts = require('typescript');
const requireWeb = createRequire(resolve(__dirname, '../package.json'));
const file = resolve(__dirname, '../src/components/accounts/AccountManagementList.tsx');
function setup({ accounts = [{ id: 'account-1', name: '真实昵称', business_status: 'normal', tags: ['主账号'] }], environments = [{ alias: 'env-1', gateway: 'gw-a' }], loadError, actionError, grid = { vertical: true, horizontal: true } } = {}) {
function setup({ accounts = [{ id: 'account-1', name: '真实昵称', business_status: 'normal', tags: ['主账号'] }], environments = [{ alias: 'env-1', gateway: 'gw-a' }], loadError, actionError } = {}) {
const states = [], calls = [], messages = [], paths = [];
let cursor = 0, started = false;
const marker = displayName => Object.assign(() => null, { displayName });
@@ -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: () => ({}), tableScroll: (x, y) => ({ x, y }), useOverflowGrid: () => grid };
if (id === '@/utils/table') return { fixedLeft: column => ({ ...column, fixed: 'left' }), fixedRight: column => ({ ...column, fixed: 'right' }), tablePagination: () => ({ pageSize: 20, hideOnSinglePage: true }) };
if (id.startsWith('@/components/accounts/')) return { default: marker(id.split('/').at(-1)) };
return requireWeb(id);
};
@@ -67,24 +67,25 @@ test('待登录环境与已登录账号共用唯一列表、计数及分页', as
assert.notEqual(rows[0].id, rows[1].id);
});
for (const horizontal of [false, true]) {
for (const vertical of [false, true]) {
test(`账号与操作列不挤压,其他列横向滚动(横向溢出 ${horizontal},纵向溢出 ${vertical})`, async () => {
const s = await ready({ grid: { horizontal, vertical } });
const { columns, scroll, dataSource } = s.find('Table').props;
assert.equal(columns[0].title, '账号');
assert.equal(columns[0].fixed, 'left');
assert.equal(columns[0].width, 240);
assert.equal(columns.at(-1).title, '操作');
assert.equal(columns.at(-1).fixed, 'right');
assert.equal(columns.at(-1).width, 300);
assert.ok(columns.slice(1, -1).every(column => !column.fixed));
assert.equal(scroll.x, 'max-content', '宽屏也应按内容宽度滚动,而不是挤压两侧列');
assert.equal(scroll.y, vertical ? 'calc(100vh - 300px)' : undefined);
const row = dataSource.find(row => !row.pending_environment);
assert.notEqual(s.find('Space', s.cell('操作', row)).props.wrap, true, '操作按钮不得换行折叠');
});
}
for (const rowCount of [1, 20, 100]) {
test(`账号与操作列不挤压,${rowCount} 条数据使用页面纵向滚动`, async () => {
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(pagination, { pageSize: 20, hideOnSinglePage: true });
assert.equal(columns[0].title, '账号');
assert.equal(columns[0].fixed, 'left');
assert.equal(columns[0].width, 240);
assert.equal(columns.at(-1).title, '操作');
assert.equal(columns.at(-1).fixed, 'right');
assert.equal(columns.at(-1).width, 300);
assert.ok(columns.slice(1, -1).every(column => !column.fixed));
assert.equal(scroll.x, 'max-content', '宽屏也应按内容宽度滚动,而不是挤压两侧列');
assert.equal(Object.hasOwn(scroll, 'y'), false, '不应启用表格内部纵向滚动');
const row = dataSource.find(row => !row.pending_environment);
assert.notEqual(s.find('Space', s.cell('操作', row)).props.wrap, true, '操作按钮不得换行折叠');
});
}
test('按共用环境数据库ID倒序排列,新建环境与账号在前,登录绑定不改变位置', async () => {
+76 -14
View File
@@ -1,18 +1,19 @@
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { test } = require('node:test');
const vm = require('node:vm');
const ts = require('typescript');
// 表格统一形态:固定表头(scroll.y)+ sticky 兜底 + 横向滚动 + 操作列固定右侧 + 分页。
// 页面纵向滚动 + sticky 表头吸顶 + 横向滚动 + 固定列 + 分页。
const files = [
'src/components/accounts/MonitoringAccountList.tsx',
'src/components/accounts/AccountManagementList.tsx',
'src/pages/accounts/import.tsx',
'src/pages/network-exits/index.tsx',
'src/pages/gateways/index.tsx',
['src/components/accounts/MonitoringAccountList.tsx', 1100],
['src/components/accounts/AccountManagementList.tsx', 'max-content'],
['src/pages/accounts/import.tsx', 1000],
['src/pages/network-exits/index.tsx', 1100],
['src/pages/gateways/index.tsx', 960],
];
for (const file of files) {
for (const [file, width] of files) {
const source = ts.createSourceFile(
file,
readFileSync(`${__dirname}/../${file}`, 'utf8'),
@@ -21,13 +22,74 @@ for (const file of files) {
ts.ScriptKind.TSX,
);
const code = source.getFullText();
const tables = [];
function visit(node) {
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && node.tagName.getText(source) === 'Table') tables.push(node);
ts.forEachChild(node, visit);
}
visit(source);
test(`${file} 表格使用统一固定表头与分页形态`, () => {
assert.match(code, /sticky/, '应开启表头吸顶兜底');
assert.match(code, /pagination=\{tablePagination\(\)\}/, '应恢复分页(每页 20,单页隐藏)');
assert.doesNotMatch(code, /pagination=\{false\}/, '不应再保留 pagination={false}');
assert.match(code, /\.\.\.fixedLeft</, '首列应固定在左侧');
assert.match(code, /\.\.\.fixedRight</, '操作列应固定在右侧');
assert.match(code, /useOverflowGrid\(/, '应按数据量按需开启表内滚动/横向滚动');
test(`${file} 保留表头吸顶、分页和固定列`, () => {
assert.equal(tables.length, 1);
const props = tables[0].attributes.properties;
assert.ok(props.some(prop => prop.name?.getText(source) === 'sticky'));
assert.match(code, /pagination=\{tablePagination\(\)\}/);
assert.doesNotMatch(code, /pagination=\{false\}/);
assert.match(code, /\.\.\.fixedLeft</);
assert.match(code, /\.\.\.fixedRight</);
});
test(`${file} 数据多或小屏时仍使用页面纵向滚动,横向滚动不变`, () => {
const scroll = tables[0].attributes.properties.find(prop => prop.name?.getText(source) === 'scroll');
assert.ok(scroll?.initializer && ts.isJsxExpression(scroll.initializer));
const expression = scroll.initializer.expression.getText(source);
for (const horizontal of [false, true]) {
for (const height of [480, 1080]) {
for (const rowCount of [0, 1, 20, 100]) {
const result = vm.runInNewContext(`(${expression})`, {
horizontal,
vertical: rowCount * 48 + 120 >= height - 300 || horizontal,
tableScroll: (x, y) => ({ x, y }),
});
assert.equal(Object.hasOwn(result, 'y'), false, `不应配置 scroll.y(行数 ${rowCount},高度 ${height})`);
assert.equal(result.x, width === 'max-content' ? width : horizontal ? width : undefined);
}
}
}
assert.doesNotMatch(code, /window\.innerHeight|useOverflowGrid|tableScroll/, '不再保留按高度和行数启用表内滚动的旧路径');
if (width !== 'max-content') assert.match(code, /!Grid\.useBreakpoint\(\)\.xl/, '横向断点保持 xl');
});
}
const tableFile = `${__dirname}/../src/utils/table.ts`;
const tableCode = ts.transpileModule(readFileSync(tableFile, 'utf8'), {
fileName: tableFile,
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
}).outputText;
const tableModule = { exports: {} };
vm.runInNewContext(tableCode, { exports: tableModule.exports }, { filename: tableFile });
const helpers = tableModule.exports;
test('分页仍为每页 20 条,单页隐藏分页条', () => {
assert.equal(helpers.TABLE_PAGE_SIZE, 20);
const pagination = helpers.tablePagination();
assert.equal(pagination.pageSize, 20);
assert.equal(pagination.hideOnSinglePage, true);
});
test('固定列保留原配置且不修改输入', () => {
const column = { title: '账号', width: 240 };
for (const [helper, side] of [[helpers.fixedLeft, 'left'], [helpers.fixedRight, 'right']]) {
const fixed = helper(column);
assert.equal(fixed.fixed, side);
assert.equal(fixed.title, column.title);
assert.equal(fixed.width, column.width);
assert.notEqual(fixed, column);
assert.equal(Object.hasOwn(column, 'fixed'), false);
}
});
test('移除纵向滚动判断与无必要的滚动包装函数', () => {
assert.equal(helpers.useOverflowGrid, undefined);
assert.equal(helpers.tableScroll, undefined);
});