iter(g254): 派生后页码越界回写内核状态

This commit is contained in:
HIsheR
2026-10-26 22:14:44 +08:00
parent dec3a9ff8a
commit 43af4cf29d
7 changed files with 42 additions and 8 deletions
@@ -193,4 +193,21 @@ swapped.subscribe(() => (notified += 1));
swapped.setRows(sameRows);
assert(notified === 1 && swapped.getVisibleRows().total === 4, 'setRows 换引用:通知一次并重算 total');
// 页码收敛:数据缩水后越界页码范围回生效页,state 与视图保持一致(不再滞留旧页码)
const clamp = createTable({
data: users,
columns: [{ key: 'name', header: 'Name' }],
rowKey: 'id',
plugins: [paginationPlugin()],
initialState: { pagination: { page: 3, pageSize: 2 } },
});
assert(clamp.getVisibleRows().rows.length === 1 && clamp.getPageInfo().page === 3, '收敛前:5 行 / 每页 2 = 3 页,停在第 3 页');
clamp.setRows(users.slice(0, 1)); // 只剩 1 行 → 总页数 1
assert(clamp.getPageInfo().page === 1 && clamp.getState().pagination.page === 1, '数据缩水后:越界页码回写为 1(state 与生效页一致)');
clamp.setRows(users); // 数据恢复 5 行
assert(clamp.getVisibleRows().rows.length === 2 && clamp.getState().pagination.page === 1, '数据恢复后:仍停第 1 页,不记忆已失效的旧页码');
clamp.dispatch({ type: 'setPage', page: 9 }); // 显式跳到越界页
assert(clamp.getVisibleRows().rows.length === 1 && clamp.getState().pagination.page === 3, '跳越界页:派生钳制后回写为末页 3');
assert(clamp.getPageInfo().totalPages === 3, '收敛不改分页快照语义(totalPages 仍按 total 推算)');
console.log('SMOKE OK');
+10 -1
View File
@@ -81,7 +81,9 @@ export function createTable<T>(
return cachedBase;
}
/*** 可见行模型:依序过插件 derive(filter → sort → paginate);paged=false 时跳过分页阶段取全量派生行 ***/
/*** 可见行模型:依序过插件 derive(filter → sort → paginate);paged=false 时跳过分页阶段取全量派生行
* 派生末尾跑一轮 settle:插件可把派生期算出的生效值回写自己的状态切片(如页码越界钳制),
* 保证 state 与视图一致;回写只走 onStateChange、不广播订阅,避免在读取中重入。 ***/
function getVisibleRows(paged = true): RowModel<T> {
ctx.paged = paged; // 由插件自行判断是否裁剪,核心不认插件细节
let model = baseModel();
@@ -89,6 +91,13 @@ export function createTable<T>(
if (p.derive) model = p.derive(model, state, ctx);
}
ctx.paged = true; // 还原缺省,避免影响后续调用
let settled = false;
for (const p of plugins) {
if (!p.settle) continue;
const next = p.settle(state);
if (next !== state) { state = next; settled = true; }
}
if (settled) onStateChange?.(state); // 状态被收敛:回调通知外部,订阅方靠本次读取结果自更新
return model;
}
@@ -20,6 +20,7 @@ export function paginationPlugin<T = unknown>(): TablePlugin<T> {
let lastPage: { page: number; pageSize: number } | null = null;
let cached: RowModel<T> | null = null;
let pageInfo: PageInfo = { page: 1, pageSize: 20, totalPages: 1 };
let fresh = false; // 快照是否来自「按当前 state 完整派生」,settle 只认新鲜快照
return {
name: 'pagination',
@@ -34,7 +35,7 @@ export function paginationPlugin<T = unknown>(): TablePlugin<T> {
}
},
derive(model: RowModel<T>, state: TableState, ctx: DeriveContext<T>): RowModel<T> {
if (ctx.paged === false) return model; // 不分页:原样放行上游模型(导出全量行)
if (ctx.paged === false) { fresh = false; return model; } // 不分页:原样放行上游模型(导出全量行)
if (model === lastInput && state.pagination === lastPage && cached) return cached;
const { page, pageSize } = state.pagination;
const totalPages = Math.max(1, Math.ceil(model.total / pageSize));
@@ -48,8 +49,14 @@ export function paginationPlugin<T = unknown>(): TablePlugin<T> {
pageInfo = { page: safePage, pageSize, totalPages };
lastInput = model;
lastPage = state.pagination;
fresh = true;
return cached;
},
settle(state: TableState): TableState {
// 只把「当前 state 派生出的生效页」回写;快照过期(未按当前 state 派生)时不动,避免写入错误页码
if (!fresh || state.pagination !== lastPage || pageInfo.page === state.pagination.page) return state;
return { ...state, pagination: { ...state.pagination, page: pageInfo.page } };
},
api(table: TableInstance<T>) {
return {
setPage: (page: number) => table.dispatch({ type: 'setPage', page }),
+2 -1
View File
@@ -66,11 +66,12 @@ export interface DeriveContext<T = unknown> {
getKey(row: T): RowKey; // 行标识取值
}
/*** 插件三钩子:patchState 改状态、derive 插入数据管线、api 向实例注入方法 ***/
/*** 插件四钩子:patchState 改状态、derive 插入数据管线、settle 派生后收敛自身状态、api 向实例注入方法 ***/
export interface TablePlugin<T = unknown> {
name: string; // 插件名,诊断用
patchState?(state: TableState, action: Action): TableState; // 处理自己关心的动作,无关动作原样返回
derive?(model: RowModel<T>, state: TableState, ctx: DeriveContext<T>): RowModel<T>; // 插入派生管线阶段
settle?(state: TableState): TableState; // 派生后回写修正(如页码越界钳制);无需修正须原样返回同一引用
api?(table: TableInstance<T>): Record<string, unknown>; // 向实例注入便捷方法
}
+1 -1
View File
@@ -39,7 +39,7 @@
"变更状态:只能 dispatch action,如 table.dispatch({ type: 'toggleSort', key: 'name' });状态不可变",
"订阅刷新:table.subscribe(fn),fn 里重新 getVisibleRows() 并交给渲染层",
"分页信息:api.getPageInfo() 返回 { page, pageSize, totalPages };越界页码自动钳制",
"扩展能力:实现 TablePlugin(derive / patchState / api 三钩子)传入 plugins 即插入管线"
"扩展能力:实现 TablePlugin(derive / patchState / settle / api 四钩子)传入 plugins 即插入管线"
]
}
}