mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-06 14:33:46 +08:00
⚡ perf(table-overview): 优化大量表搜索渲染性能
- 预计算表概览搜索索引与排序键 - 使用 deferred value 降低搜索输入阻塞 - 限制大结果集首批渲染数量并支持继续加载 - 增加表概览过滤与渲染上限回归测试
This commit is contained in:
48
frontend/src/utils/tableOverviewFilter.test.ts
Normal file
48
frontend/src/utils/tableOverviewFilter.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
TABLE_OVERVIEW_RENDER_BATCH_SIZE,
|
||||
buildTableOverviewSearchIndex,
|
||||
filterAndSortTableOverviewRows,
|
||||
resolveTableOverviewVisibleRows,
|
||||
} from './tableOverviewFilter';
|
||||
|
||||
const buildRows = (count: number) => Array.from({ length: count }, (_, index) => ({
|
||||
name: `table_${String(index).padStart(4, '0')}`,
|
||||
comment: index === count - 1 ? 'target table comment' : 'normal table',
|
||||
rows: index,
|
||||
dataSize: count - index,
|
||||
indexSize: 0,
|
||||
}));
|
||||
|
||||
describe('tableOverviewFilter', () => {
|
||||
it('filters against the full table set before applying the render limit', () => {
|
||||
const rows = buildRows(1200);
|
||||
const indexed = buildTableOverviewSearchIndex(rows);
|
||||
const filtered = filterAndSortTableOverviewRows(indexed, 'target', 'name', 'asc');
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].name).toBe('table_1199');
|
||||
});
|
||||
|
||||
it('caps initially rendered rows for large overview result sets', () => {
|
||||
const rows = buildRows(1200);
|
||||
const visible = resolveTableOverviewVisibleRows(rows, TABLE_OVERVIEW_RENDER_BATCH_SIZE);
|
||||
|
||||
expect(visible.visibleRows).toHaveLength(TABLE_OVERVIEW_RENDER_BATCH_SIZE);
|
||||
expect(visible.hiddenCount).toBe(1200 - TABLE_OVERVIEW_RENDER_BATCH_SIZE);
|
||||
expect(visible.totalCount).toBe(1200);
|
||||
});
|
||||
|
||||
it('sorts with precomputed normalized table names', () => {
|
||||
const indexed = buildTableOverviewSearchIndex([
|
||||
{ name: 'z_table', comment: '', rows: 1, dataSize: 10, indexSize: 0 },
|
||||
{ name: 'A_table', comment: '', rows: 2, dataSize: 5, indexSize: 0 },
|
||||
]);
|
||||
|
||||
expect(filterAndSortTableOverviewRows(indexed, '', 'name', 'asc').map((item) => item.name)).toEqual([
|
||||
'A_table',
|
||||
'z_table',
|
||||
]);
|
||||
});
|
||||
});
|
||||
66
frontend/src/utils/tableOverviewFilter.ts
Normal file
66
frontend/src/utils/tableOverviewFilter.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export const TABLE_OVERVIEW_RENDER_BATCH_SIZE = 300;
|
||||
|
||||
export type TableOverviewSortField = 'name' | 'rows' | 'dataSize';
|
||||
export type TableOverviewSortOrder = 'asc' | 'desc';
|
||||
|
||||
export interface TableOverviewFilterRow {
|
||||
name: string;
|
||||
comment?: string;
|
||||
rows: number;
|
||||
dataSize: number;
|
||||
indexSize: number;
|
||||
}
|
||||
|
||||
export interface TableOverviewSearchIndexItem<T extends TableOverviewFilterRow> {
|
||||
row: T;
|
||||
searchText: string;
|
||||
sortName: string;
|
||||
}
|
||||
|
||||
export const buildTableOverviewSearchIndex = <T extends TableOverviewFilterRow>(
|
||||
rows: T[],
|
||||
): TableOverviewSearchIndexItem<T>[] => rows.map((row) => ({
|
||||
row,
|
||||
searchText: `${row.name}\n${row.comment || ''}`.toLowerCase(),
|
||||
sortName: row.name.toLowerCase(),
|
||||
}));
|
||||
|
||||
export const filterAndSortTableOverviewRows = <T extends TableOverviewFilterRow>(
|
||||
indexedRows: TableOverviewSearchIndexItem<T>[],
|
||||
rawSearchText: string,
|
||||
sortField: TableOverviewSortField,
|
||||
sortOrder: TableOverviewSortOrder,
|
||||
): T[] => {
|
||||
const keyword = String(rawSearchText || '').trim().toLowerCase();
|
||||
const matched = keyword
|
||||
? indexedRows.filter((item) => item.searchText.includes(keyword))
|
||||
: [...indexedRows];
|
||||
|
||||
matched.sort((a, b) => {
|
||||
let cmp = 0;
|
||||
if (sortField === 'name') {
|
||||
cmp = a.sortName.localeCompare(b.sortName);
|
||||
} else if (sortField === 'rows') {
|
||||
cmp = a.row.rows - b.row.rows;
|
||||
} else if (sortField === 'dataSize') {
|
||||
cmp = a.row.dataSize - b.row.dataSize;
|
||||
}
|
||||
return sortOrder === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
|
||||
return matched.map((item) => item.row);
|
||||
};
|
||||
|
||||
export const resolveTableOverviewVisibleRows = <T>(
|
||||
rows: T[],
|
||||
rawLimit: number,
|
||||
): { visibleRows: T[]; hiddenCount: number; totalCount: number } => {
|
||||
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
||||
? Math.min(Math.floor(rawLimit), rows.length)
|
||||
: Math.min(TABLE_OVERVIEW_RENDER_BATCH_SIZE, rows.length);
|
||||
return {
|
||||
visibleRows: rows.slice(0, limit),
|
||||
hiddenCount: Math.max(0, rows.length - limit),
|
||||
totalCount: rows.length,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user