iter(g324): table-core 补 setColumns,换列后失效排序与筛选剔除

This commit is contained in:
HIsheR
2026-10-27 01:16:36 +08:00
parent 766fe76cc4
commit 8e358c4faf
5 changed files with 68 additions and 4 deletions
+9
View File
@@ -3302,6 +3302,15 @@
"motivation": "21 条配方此前只在 CLI / MCP / llms-full.txt 三条 Agent 通道可查,人类版文档站完全没有入口——浏览器里翻配方只能回 recipes.md 源文件;文档站已有组件/令牌/搜索/亮暗主题,配方是最后一块缺口。", "motivation": "21 条配方此前只在 CLI / MCP / llms-full.txt 三条 Agent 通道可查,人类版文档站完全没有入口——浏览器里翻配方只能回 recipes.md 源文件;文档站已有组件/令牌/搜索/亮暗主题,配方是最后一块缺口。",
"acceptance": "① apps/docs/index.html 生成「组合配方」卡:每条配方一个 details 折叠块(标题 + 组件 chips + 代码块 + 要点),导航栏补「配方」锚点;② 搜索态不渲染配方卡(不属于任何包,避免稀释过滤结果);③ 配方数据由 parseRecipes 生成而非手写,recipes.md 改动后 npm run docs 即同步;④ npm run check 通过。", "acceptance": "① apps/docs/index.html 生成「组合配方」卡:每条配方一个 details 折叠块(标题 + 组件 chips + 代码块 + 要点),导航栏补「配方」锚点;② 搜索态不渲染配方卡(不属于任何包,避免稀释过滤结果);③ 配方数据由 parseRecipes 生成而非手写,recipes.md 改动后 npm run docs 即同步;④ npm run check 通过。",
"notes": "" "notes": ""
},
{
"id": "g324",
"domain": "tune",
"status": "open",
"title": "table-core 补 setColumns:动态换列后失效排序 / 筛选剔除",
"motivation": "setRows 已支持整体换源并按 rowKey 剔除失效选择,但列定义仍是构造时写死:动态列表(按角色 / 视图切换列集)只能重建整个实例,排序与筛选状态全丢;setColumns 与 setRows 对称——换列后指向不存在列的 sorting / filters 失去语义,应剔除而不是静默残留。",
"acceptance": "① core 新增 setColumns(next):引用未变不动;换列后剔除 sorting / filters 里 key 不在新列集的条目,其余状态(分页 / 选择)保持;② getColumns 返回最新列集,getCellValue 按新列 accessor 取值;③ smoke 冒烟补断言(失效剔除 + 未变短路 + accessor 跟随);④ npm run check 通过。",
"notes": ""
} }
] ]
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -210,4 +210,42 @@ clamp.dispatch({ type: 'setPage', page: 9 }); // 显式跳到越界页
assert(clamp.getVisibleRows().rows.length === 1 && clamp.getState().pagination.page === 3, '跳越界页:派生钳制后回写为末页 3'); assert(clamp.getVisibleRows().rows.length === 1 && clamp.getState().pagination.page === 3, '跳越界页:派生钳制后回写为末页 3');
assert(clamp.getPageInfo().totalPages === 3, '收敛不改分页快照语义(totalPages 仍按 total 推算)'); assert(clamp.getPageInfo().totalPages === 3, '收敛不改分页快照语义(totalPages 仍按 total 推算)');
// setColumns:动态换列,失效的排序 / 筛选按 key 剔除,accessor 跟随新列集
const colsTable = createTable({
data: users,
columns: [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true, accessor: (row) => row.age },
{ key: 'city', header: 'City' },
],
rowKey: 'id',
plugins: [filterPlugin(), sortPlugin(), paginationPlugin(), selectionPlugin()],
initialState: {
sorting: [{ key: 'age', dir: 'desc' }],
filters: [{ key: 'city', value: 'Beijing' }],
},
});
assert(colsTable.getVisibleRows().total === 2, 'setColumns 前:city 筛选生效(Beijing 2 行)');
colsTable.setColumns([
{ key: 'name', header: 'Name', sortable: true },
{ key: 'score', header: 'Score', accessor: (row) => 100 - row.age }, // 换列集:city 列没了,age 列换成派生 score
]);
assert(
colsTable.getState().sorting.length === 0 && colsTable.getState().filters.length === 0,
'setColumns 后:指向不存在列的排序 / 筛选整体剔除(age 列已换成 score)',
);
assert(colsTable.getVisibleRows().total === 5, 'setColumns 后:失效筛选被剔除,total 回到全量 5 行');
assert(colsTable.getColumns().length === 2 && colsTable.getColumns()[1].key === 'score', 'setColumns 后:getColumns 返回新列集');
assert(
colsTable.getCellValue(users[0], 'score') === 65,
'setColumns 后:getCellValue 跟随新列 accessor(score = 100 - age)',
);
const sameCols = colsTable.getColumns();
let colNotified = 0;
colsTable.subscribe(() => {
colNotified += 1;
});
colsTable.setColumns(sameCols);
assert(colNotified === 0, 'setColumns 引用未变:短路不通知');
console.log('SMOKE OK'); console.log('SMOKE OK');
+19 -3
View File
@@ -14,7 +14,8 @@ export function createTable<T>(
options: TableOptions<T>, options: TableOptions<T>,
): TableInstance<T> & Record<string, unknown> { ): TableInstance<T> & Record<string, unknown> {
let data = options.data; // 可变:setRows 整体换数据源(筛选结果、轮询刷新),派生管线照旧跑 let data = options.data; // 可变:setRows 整体换数据源(筛选结果、轮询刷新),派生管线照旧跑
const { columns, plugins = [], onStateChange } = options; let cols = options.columns; // 可变:setColumns 动态换列(按角色 / 视图切换列集),失效的排序 / 筛选按 key 剔除
const { plugins = [], onStateChange } = options;
const rowKey = options.rowKey ?? ('id' as keyof T & string); // 默认行标识 const rowKey = options.rowKey ?? ('id' as keyof T & string); // 默认行标识
const getKey: (row: T) => RowKey = const getKey: (row: T) => RowKey =
@@ -64,7 +65,7 @@ export function createTable<T>(
/*** 取单元格值:优先列 accessor,缺省取 row[key] ***/ /*** 取单元格值:优先列 accessor,缺省取 row[key] ***/
function getCellValue(row: T, key: string): unknown { function getCellValue(row: T, key: string): unknown {
const col = columns.find((c) => c.key === key); const col = cols.find((c) => c.key === key);
if (col?.accessor) return col.accessor(row); if (col?.accessor) return col.accessor(row);
return (row as Record<string, unknown>)[key]; return (row as Record<string, unknown>)[key];
} }
@@ -115,14 +116,29 @@ export function createTable<T>(
for (const fn of listeners) fn(); for (const fn of listeners) fn();
} }
/*** 整体替换列定义:引用未变则不动;换列后指向不存在列的排序 / 筛选剔除(列都没了,规则失去语义),分页 / 选择与列集无关、保持不动。 ***/
function setColumns(next: typeof cols): void {
if (next === cols) return;
cols = next;
const keys = new Set(next.map((c) => c.key));
const sorting = state.sorting.filter((s) => keys.has(s.key));
const filters = state.filters.filter((f) => keys.has(f.key));
if (sorting.length !== state.sorting.length || filters.length !== state.filters.length) {
state = { ...state, sorting, filters }; // 只动失效规则:页码 / 选择保持
}
onStateChange?.(state);
for (const fn of listeners) fn();
}
const instance: TableInstance<T> = { const instance: TableInstance<T> = {
getState: () => state, getState: () => state,
dispatch, dispatch,
subscribe, subscribe,
setRows, setRows,
setColumns,
getVisibleRows, getVisibleRows,
getAllRows: () => getVisibleRows(false), // 筛选 + 排序后、未分页的全量行(导出等场景) getAllRows: () => getVisibleRows(false), // 筛选 + 排序后、未分页的全量行(导出等场景)
getColumns: () => columns, getColumns: () => cols,
getCellValue, getCellValue,
isRowSelected: (key) => state.selection.includes(key), isRowSelected: (key) => state.selection.includes(key),
}; };
+1
View File
@@ -91,6 +91,7 @@ export interface TableInstance<T = unknown> {
dispatch(action: Action): void; // 派发动作,唯一变更入口 dispatch(action: Action): void; // 派发动作,唯一变更入口
subscribe(fn: () => void): () => void; // 订阅变更,返回取消函数 subscribe(fn: () => void): () => void; // 订阅变更,返回取消函数
setRows(rows: T[]): void; // 整体换数据源(筛选结果 / 轮询刷新),选择状态按 rowKey 剔除失效行 setRows(rows: T[]): void; // 整体换数据源(筛选结果 / 轮询刷新),选择状态按 rowKey 剔除失效行
setColumns(columns: ColumnDef<T>[]): void; // 整体换列定义(按角色 / 视图切换列集),失效的排序 / 筛选按 key 剔除
getVisibleRows(): RowModel<T>; // 可见行模型(过完整管线) getVisibleRows(): RowModel<T>; // 可见行模型(过完整管线)
getAllRows(): RowModel<T>; // 筛选 + 排序后、未分页的全量行(导出用) getAllRows(): RowModel<T>; // 筛选 + 排序后、未分页的全量行(导出用)
getColumns(): ColumnDef<T>[]; // 列定义 getColumns(): ColumnDef<T>[]; // 列定义