iter(g254): 派生后页码越界回写内核状态
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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>; // 向实例注入便捷方法
|
||||
}
|
||||
|
||||
|
||||
@@ -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 即插入管线"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user