From 43af4cf29dcc25b5d39385511b4947051849a8c9 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 22:14:44 +0800 Subject: [PATCH] =?UTF-8?q?iter(g254):=20=E6=B4=BE=E7=94=9F=E5=90=8E?= =?UTF-8?q?=E9=A1=B5=E7=A0=81=E8=B6=8A=E7=95=8C=E5=9B=9E=E5=86=99=E5=86=85?= =?UTF-8?q?=E6=A0=B8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 4 ++-- llms-full.txt | 4 ++-- packages/core/table-core/scripts/smoke.mjs | 17 +++++++++++++++++ packages/core/table-core/src/core.ts | 11 ++++++++++- .../core/table-core/src/plugins/pagination.ts | 9 ++++++++- packages/core/table-core/src/types.ts | 3 ++- packages/core/table-core/ua.manifest.json | 2 +- 7 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.ua/index.json b/.ua/index.json index c0c2477..06d45d8 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T14:09:42.246Z", + "generatedAt": "2026-10-26T14:14:20.015Z", "repoRoot": ".", "packages": [ { @@ -87,7 +87,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 即插入管线" ] }, "version": "0.1.0", diff --git a/llms-full.txt b/llms-full.txt index ea2aefb..431efa3 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T14:09:42.246Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T14:14:20.015Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -21,7 +21,7 @@ 3. 变更状态:只能 dispatch action,如 table.dispatch({ type: 'toggleSort', key: 'name' });状态不可变 4. 订阅刷新:table.subscribe(fn),fn 里重新 getVisibleRows() 并交给渲染层 5. 分页信息:api.getPageInfo() 返回 { page, pageSize, totalPages };越界页码自动钳制 -6. 扩展能力:实现 TablePlugin(derive / patchState / api 三钩子)传入 plugins 即插入管线 +6. 扩展能力:实现 TablePlugin(derive / patchState / settle / api 四钩子)传入 plugins 即插入管线 ### @ui-arch/tokens diff --git a/packages/core/table-core/scripts/smoke.mjs b/packages/core/table-core/scripts/smoke.mjs index e691e71..a3a6bec 100644 --- a/packages/core/table-core/scripts/smoke.mjs +++ b/packages/core/table-core/scripts/smoke.mjs @@ -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'); diff --git a/packages/core/table-core/src/core.ts b/packages/core/table-core/src/core.ts index 4e88624..63cfddc 100644 --- a/packages/core/table-core/src/core.ts +++ b/packages/core/table-core/src/core.ts @@ -81,7 +81,9 @@ export function createTable( return cachedBase; } - /*** 可见行模型:依序过插件 derive(filter → sort → paginate);paged=false 时跳过分页阶段取全量派生行 ***/ + /*** 可见行模型:依序过插件 derive(filter → sort → paginate);paged=false 时跳过分页阶段取全量派生行 + * 派生末尾跑一轮 settle:插件可把派生期算出的生效值回写自己的状态切片(如页码越界钳制), + * 保证 state 与视图一致;回写只走 onStateChange、不广播订阅,避免在读取中重入。 ***/ function getVisibleRows(paged = true): RowModel { ctx.paged = paged; // 由插件自行判断是否裁剪,核心不认插件细节 let model = baseModel(); @@ -89,6 +91,13 @@ export function createTable( 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; } diff --git a/packages/core/table-core/src/plugins/pagination.ts b/packages/core/table-core/src/plugins/pagination.ts index 2a4bd0f..59faaa2 100644 --- a/packages/core/table-core/src/plugins/pagination.ts +++ b/packages/core/table-core/src/plugins/pagination.ts @@ -20,6 +20,7 @@ export function paginationPlugin(): TablePlugin { let lastPage: { page: number; pageSize: number } | null = null; let cached: RowModel | 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(): TablePlugin { } }, derive(model: RowModel, state: TableState, ctx: DeriveContext): RowModel { - 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(): TablePlugin { 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) { return { setPage: (page: number) => table.dispatch({ type: 'setPage', page }), diff --git a/packages/core/table-core/src/types.ts b/packages/core/table-core/src/types.ts index bfc77da..f969a48 100644 --- a/packages/core/table-core/src/types.ts +++ b/packages/core/table-core/src/types.ts @@ -66,11 +66,12 @@ export interface DeriveContext { getKey(row: T): RowKey; // 行标识取值 } -/*** 插件三钩子:patchState 改状态、derive 插入数据管线、api 向实例注入方法 ***/ +/*** 插件四钩子:patchState 改状态、derive 插入数据管线、settle 派生后收敛自身状态、api 向实例注入方法 ***/ export interface TablePlugin { name: string; // 插件名,诊断用 patchState?(state: TableState, action: Action): TableState; // 处理自己关心的动作,无关动作原样返回 derive?(model: RowModel, state: TableState, ctx: DeriveContext): RowModel; // 插入派生管线阶段 + settle?(state: TableState): TableState; // 派生后回写修正(如页码越界钳制);无需修正须原样返回同一引用 api?(table: TableInstance): Record; // 向实例注入便捷方法 } diff --git a/packages/core/table-core/ua.manifest.json b/packages/core/table-core/ua.manifest.json index b108a8f..2d09734 100644 --- a/packages/core/table-core/ua.manifest.json +++ b/packages/core/table-core/ua.manifest.json @@ -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 即插入管线" ] } }