From 42ea31d5b54adedee1e2e178aafa17702cb408dd Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 22:03:37 +0800 Subject: [PATCH] =?UTF-8?q?iter(g247):=20=E8=A1=A8=E6=A0=BC=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=95=B4=E4=BD=93=E6=8D=A2=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=EF=BC=8CdataTable=20=E5=8F=AF=E6=8E=A5=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 7 +++--- AGENTS.md | 2 +- apps/demo/main.ts | 18 +++++++++++++++ llms-full.txt | 27 +++++++++++----------- packages/core/table-core/scripts/smoke.mjs | 21 +++++++++++++++++ packages/core/table-core/src/core.ts | 18 ++++++++++++++- packages/core/table-core/src/types.ts | 1 + packages/skin/ui/src/dataTable.ts | 12 +++++----- packages/skin/ui/src/tableDataSource.ts | 17 ++++++++++++++ packages/skin/ui/src/types.ts | 2 +- packages/skin/ui/ua.manifest.json | 5 ++-- 11 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 packages/skin/ui/src/tableDataSource.ts diff --git a/.ua/index.json b/.ua/index.json index ef2dc02..94c4522 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T13:59:39.013Z", + "generatedAt": "2026-10-26T14:02:07.910Z", "repoRoot": ".", "packages": [ { @@ -2016,9 +2016,9 @@ "props": [ { "n": "data", - "t": "T[]", + "t": "T[] | Signal", "r": 1, - "d": "全量行数据,每行须含 id 字段作 rowKey" + "d": "全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意" }, { "n": "columns", @@ -2080,6 +2080,7 @@ "准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)", "定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕", "列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)", + "数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断", "列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应", "selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数", "选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失", diff --git a/AGENTS.md b/AGENTS.md index 490b5f4..b65a2a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html - `apps/docs`:文档站,纯产物(ua docs 生成) -**门禁一条龙**:`npm run check` = build + demo + style + typecheck + smoke + index + verify + lint-iterations + audit:visual + audit:contrast,提交前必过。其中 `smoke` 是两段内核断言:`@ui-arch/frame` 14 条(信号读写与同值短路、effect 注销与条件分支重追踪、computed 链式派生、多订阅者扇出)+ `@ui-arch/table-core` 21 条(排序 / 筛选 / 分页 / 选择的派生管线);改内核语义前先跑它,别靠组件 demo 肉眼验。`ua audit:visual` 统计皮肤包里十四类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环 / 内距 / 控件高度 / 字重 / 行高 / 层级 / 边框色 / 混色浓度 / transform 位移 / 焦点描边)并与 `.ua/visual-baseline.json` 做棘轮:任一类超过基线即退出码 1 并指出疑似引入的提交;迁移脚本 `npm run visual:migrate` 可把字面量幂等改写成令牌。`ua audit:contrast` 走另一条线:把令牌里 16 组「文字色 × 底色」配对(正文/次级/辅助文字 × 页面底/表面/凹陷底、强调色与其反白、语义三色、禁用态豁免档)按 WCAG 公式算成对比度(支持 `color-mix` 与 `var()` 求值,半透明底先压到页面底色上),亮 / 暗 × 常规 / 高对比四套主题各算一遍,低于阈值(正文 7 / 常规 4.5 / 禁用 2.6)即退出码 1;`--json` 出逐对明细,`--list` 只列配对表。改任一颜色令牌后它会立刻告诉你「改完谁读不清了」,不必等人工目测暗色。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、每个文档字段名必须在组件实现源码里真实出现(改名后忘改文档会直接报错)、反向也校验 `src/types.ts` 里 `<组件>Props`(可用清单的 `propsType` 覆盖接口名,`false` 表示无入参)每个字段都在文档 props 表登记(新加字段忘了写文档会报错)、demo 锚点必须在 `apps/demo/main.ts` 中存在,且该组件必须被 demo 真实调用 `id(` / `id.`),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌)与令牌引用有效(`var(--ua-*)` 必须是 tokens.css 已定义或组件自设的变量)、核对每个 `--ua-color-*` 都在 `[data-theme='dark']` 里有覆盖值且暗色块没有凭空多出的变量名(新增颜色令牌漏写暗色值直接报错)、核对 `tokens.json` 的 `color` 键与 tokens.css 的 `--ua-color-*` 同名同值(名字按 camelCase → kebab 折算,亮色值与暗色值都比,双向查漏——只改一处时文档色板会凭空多/少一个令牌,过去只能靠人眼发现)、核对 `@keyframes` 没有「帧体完全相同但名字不同」的复制粘贴(同一段动效只该有一份声明,改名复制会让方向 / 时长各写各的,翻转态就出现过「朝反方向入场」),核对 README 的 `` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 backlog 的 done 条目与 `.ua/iterations.log` 的 done 留痕一一对应(另外查 backlog id 不重复、g 编号严格递增、同一 id 只有一条 done 留痕,且留痕 ts 带 +08:00、时间线不回退、不落在未来——都是过去手工插条目/手写时间戳踩过的坑);配方还要求编号连续、每条都有代码块与要点,且代码块里至少认出一个已注册组件 id(拼错组件名时 `ua recipes` 会把它列成空组件表)。 +**门禁一条龙**:`npm run check` = build + demo + style + typecheck + smoke + index + verify + lint-iterations + audit:visual + audit:contrast,提交前必过。其中 `smoke` 是两段内核断言:`@ui-arch/frame` 14 条(信号读写与同值短路、effect 注销与条件分支重追踪、computed 链式派生、多订阅者扇出)+ `@ui-arch/table-core` 26 条(排序 / 筛选 / 分页 / 选择的派生管线 + setRows 换源与失效行剔除);改内核语义前先跑它,别靠组件 demo 肉眼验。`ua audit:visual` 统计皮肤包里十四类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环 / 内距 / 控件高度 / 字重 / 行高 / 层级 / 边框色 / 混色浓度 / transform 位移 / 焦点描边)并与 `.ua/visual-baseline.json` 做棘轮:任一类超过基线即退出码 1 并指出疑似引入的提交;迁移脚本 `npm run visual:migrate` 可把字面量幂等改写成令牌。`ua audit:contrast` 走另一条线:把令牌里 16 组「文字色 × 底色」配对(正文/次级/辅助文字 × 页面底/表面/凹陷底、强调色与其反白、语义三色、禁用态豁免档)按 WCAG 公式算成对比度(支持 `color-mix` 与 `var()` 求值,半透明底先压到页面底色上),亮 / 暗 × 常规 / 高对比四套主题各算一遍,低于阈值(正文 7 / 常规 4.5 / 禁用 2.6)即退出码 1;`--json` 出逐对明细,`--list` 只列配对表。改任一颜色令牌后它会立刻告诉你「改完谁读不清了」,不必等人工目测暗色。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、每个文档字段名必须在组件实现源码里真实出现(改名后忘改文档会直接报错)、反向也校验 `src/types.ts` 里 `<组件>Props`(可用清单的 `propsType` 覆盖接口名,`false` 表示无入参)每个字段都在文档 props 表登记(新加字段忘了写文档会报错)、demo 锚点必须在 `apps/demo/main.ts` 中存在,且该组件必须被 demo 真实调用 `id(` / `id.`),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌)与令牌引用有效(`var(--ua-*)` 必须是 tokens.css 已定义或组件自设的变量)、核对每个 `--ua-color-*` 都在 `[data-theme='dark']` 里有覆盖值且暗色块没有凭空多出的变量名(新增颜色令牌漏写暗色值直接报错)、核对 `tokens.json` 的 `color` 键与 tokens.css 的 `--ua-color-*` 同名同值(名字按 camelCase → kebab 折算,亮色值与暗色值都比,双向查漏——只改一处时文档色板会凭空多/少一个令牌,过去只能靠人眼发现)、核对 `@keyframes` 没有「帧体完全相同但名字不同」的复制粘贴(同一段动效只该有一份声明,改名复制会让方向 / 时长各写各的,翻转态就出现过「朝反方向入场」),核对 README 的 `` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 backlog 的 done 条目与 `.ua/iterations.log` 的 done 留痕一一对应(另外查 backlog id 不重复、g 编号严格递增、同一 id 只有一条 done 留痕,且留痕 ts 带 +08:00、时间线不回退、不落在未来——都是过去手工插条目/手写时间戳踩过的坑);配方还要求编号连续、每条都有代码块与要点,且代码块里至少认出一个已注册组件 id(拼错组件名时 `ua recipes` 会把它列成空组件表)。 `ua verify` 另外校验 `ITERATION.md` 的《视觉回归抽查清单》:button / textField / select / modal / dataTable / toast 六段各 ≥3 条可勾选要点、每条维度标签必须落在边框 / 阴影 / 过渡 / 焦点环 / 暗色五类内、合计 ≥18 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。 diff --git a/apps/demo/main.ts b/apps/demo/main.ts index e7370e9..6e8ee23 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -260,6 +260,12 @@ const resendCd = countdown({ seconds: 90, running: false, format: (s) => `重发 const highlightText = signal('数据不出端、预测低于 50ms、习惯本地学习是输入法增强系统的三条底线'); // 高亮演示原文 const highlightKw = signal('本地'); // 高亮演示关键词(与搜索框信号联动) const demoTags = signal(['本地推理', '隐私优先']); // 标签输入演示集合(成标签 / 删 chip 双向写回) +const tagRows = [ + { id: 'r1', name: '本地 ASR 推理', tags: ['本地推理', '隐私优先'] }, + { id: 'r2', name: '云端补全兜底', tags: ['云端'] }, + { id: 'r3', name: '习惯冷启动加速', tags: ['本地推理'] }, +]; // 标签筛选演示数据:每行带 tags 供「且」关系匹配 +const tagMatched = computed(() => tagRows.filter((row) => demoTags().every((tag) => row.tags.includes(tag)))); // 派生行集合:标签变化即重算,喂给 dataTable 的信号数据源 const treeChecked = signal(['core']); // 多选树勾选集合(外部受控演示) const treePick = signal('core'); // 树形选中节点 key const transferKeys = signal(['a11y']); // 穿梭框右侧集合(外部受控演示) @@ -1168,6 +1174,18 @@ const content = h( inputTag({ value: demoTags, max: 5, onChange: (list) => toast.info(`当前 ${list.length} 个标签`) }), h('span', { class: 'out' }, () => `已选:${demoTags().join(' / ') || '(空)'}`), ), + h( + 'div', + { class: 'row' }, + dataTable({ + data: tagMatched, // 信号数据源:删标签后表格自动换源,排序 / 每页行数不被重置 + columns: [ + { key: 'name', header: '能力' }, + { key: 'tags', header: '标签', format: (value) => (value as string[]).join(' / ') }, + ], + pageSize: 3, + }), + ), ), section( 'sec-affix', diff --git a/llms-full.txt b/llms-full.txt index 432d889..6e21fcd 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T13:59:39.013Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T14:02:07.910Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -1005,7 +1005,7 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: 参数: -- data(必填):T[] —— 全量行数据,每行须含 id 字段作 rowKey +- data(必填):T[] | Signal —— 全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意 - columns(必填):ColumnDef[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应 - filterKey:string —— 工具栏筛选的列;缺省取第一列 - pageSize(默认 5):number —— 每页行数;缺省 5 @@ -1028,17 +1028,18 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓 1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它) 2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕ 3. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义) -4. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应 -5. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数 -6. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失 -7. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行 -8. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供 -9. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复 -10. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用 -11. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存) -12. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充 -13. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出 -14. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮 +4. 数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断 +5. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应 +6. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数 +7. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失 +8. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行 +9. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供 +10. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复 +11. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用 +12. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存) +13. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充 +14. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出 +15. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮 现场演示:apps/docs/demo.html#sec-table diff --git a/packages/core/table-core/scripts/smoke.mjs b/packages/core/table-core/scripts/smoke.mjs index 7e36fe0..e691e71 100644 --- a/packages/core/table-core/scripts/smoke.mjs +++ b/packages/core/table-core/scripts/smoke.mjs @@ -172,4 +172,25 @@ assert( ); assert(all.getPageInfo().totalPages === 1, 'getAllRows 不影响分页快照(2 行 / 每页 2 = 1 页)'); +// setRows:整体换数据源,选择状态按 rowKey 剔除失效行,页码越界由插件收敛 +const swapped = createTable({ + data: users, + columns: [{ key: 'name', header: 'Name' }], + rowKey: 'id', + plugins: [selectionPlugin(), paginationPlugin()], + initialState: { pagination: { page: 1, pageSize: 2 } }, +}); +swapped.dispatch({ type: 'toggleSelect', key: 1 }); // 选中两行,后面验证失效行被剔除 +swapped.dispatch({ type: 'toggleSelect', key: 4 }); +assert(swapped.isRowSelected(1) && swapped.isRowSelected(4), 'setRows 前:两行已选'); +swapped.setRows(users.filter((u) => u.id !== 4)); +assert(swapped.getVisibleRows().total === 4, 'setRows 后:total 跟随新数据(5 行 → 4 行)'); +assert(swapped.isRowSelected(1) && !swapped.isRowSelected(4), 'setRows 后:失效行键从选择里剔除,有效行保留'); +assert(swapped.getState().pagination.page === 1, 'setRows 不动页码(用户翻页意图不被打断)'); +const sameRows = users.filter((u) => u.id !== 4); +let notified = 0; +swapped.subscribe(() => (notified += 1)); +swapped.setRows(sameRows); +assert(notified === 1 && swapped.getVisibleRows().total === 4, 'setRows 换引用:通知一次并重算 total'); + console.log('SMOKE OK'); diff --git a/packages/core/table-core/src/core.ts b/packages/core/table-core/src/core.ts index f8988f4..4e88624 100644 --- a/packages/core/table-core/src/core.ts +++ b/packages/core/table-core/src/core.ts @@ -13,7 +13,8 @@ import type { export function createTable( options: TableOptions, ): TableInstance & Record { - const { data, columns, plugins = [], onStateChange } = options; + let data = options.data; // 可变:setRows 整体换数据源(筛选结果、轮询刷新),派生管线照旧跑 + const { columns, plugins = [], onStateChange } = options; const rowKey = options.rowKey ?? ('id' as keyof T & string); // 默认行标识 const getKey: (row: T) => RowKey = @@ -91,10 +92,25 @@ export function createTable( return model; } + /*** 整体替换数据源:引用未变或内容长度与行键全同则不动(避免无意义重算),变了就通知订阅方 + * 选择状态按 rowKey 存活:新数据里不再存在的行键会从 selection 里剔除,避免批量操作命中幽灵行。 ***/ + function setRows(next: T[]): void { + if (next === data) return; + data = next; + const keys = new Set(next.map(getKey)); + const kept = state.selection.filter((key) => keys.has(key)); + if (kept.length !== state.selection.length) { + state = { ...state, selection: kept }; // 只动选择:排序 / 筛选 / 页码保持用户原意 + } + onStateChange?.(state); + for (const fn of listeners) fn(); + } + const instance: TableInstance = { getState: () => state, dispatch, subscribe, + setRows, getVisibleRows, getAllRows: () => getVisibleRows(false), // 筛选 + 排序后、未分页的全量行(导出等场景) getColumns: () => columns, diff --git a/packages/core/table-core/src/types.ts b/packages/core/table-core/src/types.ts index 078d7b5..bfc77da 100644 --- a/packages/core/table-core/src/types.ts +++ b/packages/core/table-core/src/types.ts @@ -89,6 +89,7 @@ export interface TableInstance { getState(): TableState; // 当前状态快照 dispatch(action: Action): void; // 派发动作,唯一变更入口 subscribe(fn: () => void): () => void; // 订阅变更,返回取消函数 + setRows(rows: T[]): void; // 整体换数据源(筛选结果 / 轮询刷新),选择状态按 rowKey 剔除失效行 getVisibleRows(): RowModel; // 可见行模型(过完整管线) getAllRows(): RowModel; // 筛选 + 排序后、未分页的全量行(导出用) getColumns(): ColumnDef[]; // 列定义 diff --git a/packages/skin/ui/src/dataTable.ts b/packages/skin/ui/src/dataTable.ts index d1793b4..f10a7d5 100644 --- a/packages/skin/ui/src/dataTable.ts +++ b/packages/skin/ui/src/dataTable.ts @@ -4,8 +4,8 @@ import { paginationPlugin, selectionPlugin, sortPlugin, + type RowKey, } from '@ui-arch/table-core'; -import type { RowKey } from '@ui-arch/table-core'; import { iconSearch } from './icons.js'; import { computed, effect, h, renderList, signal } from '@ui-arch/frame'; import { attachResize } from './columnResize.js'; import { exportButton } from './csv.js'; @@ -13,23 +13,23 @@ import { button } from './button.js'; import { skeleton } from './skeleton.js'; import { textField } from './textField.js'; import { emptyRow } from './tableEmpty.js'; +import { attachDataSource, readRows } from './tableDataSource.js'; import type { DataTableProps, TableApi } from './types.js'; /*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/ export function dataTable(props: DataTableProps): HTMLElement { - // —— 状态集中声明 —— - const selectable = props.selectable ?? false; // 行选择开关 + const selectable = props.selectable ?? false; // 状态集中声明:行选择开关 const keyword = signal(''); // 筛选框关键字:受控信号,空状态里的「清除筛选」靠它一键还原 const table = createTable({ - data: props.data, + data: readRows(props.data), // 信号数据源先取当前值,后续靠 attachDataSource 换 columns: props.columns, rowKey: 'id', plugins: [filterPlugin(), sortPlugin(), paginationPlugin(), selectionPlugin()], initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } }, }); const api = table as unknown as TableApi; // 用到的两个取数方法在类型别名里声明 - const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统 - table.subscribe(() => version(version() + 1)); + const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增 → 派生统一重算 + table.subscribe(() => version(version() + 1)); attachDataSource(table, props.data); // 信号数据源变化即换源(静态数组直接返回) // —— 派生:全部由 version 单一来源推导,语义与内核一致 —— const visible = computed(() => { diff --git a/packages/skin/ui/src/tableDataSource.ts b/packages/skin/ui/src/tableDataSource.ts new file mode 100644 index 0000000..354bee8 --- /dev/null +++ b/packages/skin/ui/src/tableDataSource.ts @@ -0,0 +1,17 @@ +import { effect, type Signal } from '@ui-arch/frame'; +import type { RowKey, TableInstance } from '@ui-arch/table-core'; + +/*** 作用:取数据源当前值——传数组直接返回,传信号 / computed 则调用取值(表格创建时用) ***/ +export function readRows(data: T[] | Signal): T[] { + return typeof data === 'function' ? data() : data; +} + +/*** 作用:给表格挂换源 effect——数据源是信号时信号变化即整体换源,静态数组直接返回(不多挂 effect、不多算一次派生) ***/ +export function attachDataSource( + table: TableInstance, + data: T[] | Signal, +): void { + if (typeof data !== 'function') return; // 卫语句:静态数组无需响应式换源 + const source = data; + effect(() => table.setRows(source())); // setRows 内部按 rowKey 剔除失效选择项,页码与排序保持原意 +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index c2c41fd..f8119bb 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -61,7 +61,7 @@ export interface TableColumn extends ColumnDef { /*** 数据表格入参 ***/ export interface DataTableProps { - data: T[]; // 全量行数据,行须含 id 字段作 rowKey + data: T[] | Signal; // 全量行数据(行须含 id 作 rowKey);传信号 / computed 则数据变化时自动换源 columns: TableColumn[]; // 列定义:内核字段 + align / format filterKey?: string; // 工具栏筛选的列;缺省取第一列 pageSize?: number; // 每页行数;缺省 5 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 495c953..454fb82 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1887,9 +1887,9 @@ "props": [ { "n": "data", - "t": "T[]", + "t": "T[] | Signal", "r": 1, - "d": "全量行数据,每行须含 id 字段作 rowKey" + "d": "全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意" }, { "n": "columns", @@ -1951,6 +1951,7 @@ "准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)", "定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕", "列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)", + "数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断", "列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应", "selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数", "选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",