iter(g247): 表格支持整体换数据源,dataTable 可接信号数据
This commit is contained in:
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T13:59:39.013Z",
|
"generatedAt": "2026-10-26T14:02:07.910Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -2016,9 +2016,9 @@
|
|||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"n": "data",
|
"n": "data",
|
||||||
"t": "T[]",
|
"t": "T[] | Signal<T[]>",
|
||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "全量行数据,每行须含 id 字段作 rowKey"
|
"d": "全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "columns",
|
"n": "columns",
|
||||||
@@ -2080,6 +2080,7 @@
|
|||||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||||
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
||||||
|
"数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断",
|
||||||
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
||||||
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
||||||
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
||||||
- `apps/docs`:文档站,纯产物(ua docs 生成)
|
- `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 的 `<!-- ua:components -->` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 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 的 `<!-- ua:components -->` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 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 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。
|
`ua verify` 另外校验 `ITERATION.md` 的《视觉回归抽查清单》:button / textField / select / modal / dataTable / toast 六段各 ≥3 条可勾选要点、每条维度标签必须落在边框 / 阴影 / 过渡 / 焦点环 / 暗色五类内、合计 ≥18 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,12 @@ const resendCd = countdown({ seconds: 90, running: false, format: (s) => `重发
|
|||||||
const highlightText = signal('数据不出端、预测低于 50ms、习惯本地学习是输入法增强系统的三条底线'); // 高亮演示原文
|
const highlightText = signal('数据不出端、预测低于 50ms、习惯本地学习是输入法增强系统的三条底线'); // 高亮演示原文
|
||||||
const highlightKw = signal('本地'); // 高亮演示关键词(与搜索框信号联动)
|
const highlightKw = signal('本地'); // 高亮演示关键词(与搜索框信号联动)
|
||||||
const demoTags = signal<string[]>(['本地推理', '隐私优先']); // 标签输入演示集合(成标签 / 删 chip 双向写回)
|
const demoTags = signal<string[]>(['本地推理', '隐私优先']); // 标签输入演示集合(成标签 / 删 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<string[]>(['core']); // 多选树勾选集合(外部受控演示)
|
const treeChecked = signal<string[]>(['core']); // 多选树勾选集合(外部受控演示)
|
||||||
const treePick = signal('core'); // 树形选中节点 key
|
const treePick = signal('core'); // 树形选中节点 key
|
||||||
const transferKeys = signal<string[]>(['a11y']); // 穿梭框右侧集合(外部受控演示)
|
const transferKeys = signal<string[]>(['a11y']); // 穿梭框右侧集合(外部受控演示)
|
||||||
@@ -1168,6 +1174,18 @@ const content = h(
|
|||||||
inputTag({ value: demoTags, max: 5, onChange: (list) => toast.info(`当前 ${list.length} 个标签`) }),
|
inputTag({ value: demoTags, max: 5, onChange: (list) => toast.info(`当前 ${list.length} 个标签`) }),
|
||||||
h('span', { class: 'out' }, () => `已选:${demoTags().join(' / ') || '(空)'}`),
|
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(
|
section(
|
||||||
'sec-affix',
|
'sec-affix',
|
||||||
|
|||||||
+14
-13
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @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)。
|
> 组件 = 普通函数,传 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<T[]> —— 全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意
|
||||||
- columns(必填):ColumnDef<T>[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应
|
- columns(必填):ColumnDef<T>[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应
|
||||||
- filterKey:string —— 工具栏筛选的列;缺省取第一列
|
- filterKey:string —— 工具栏筛选的列;缺省取第一列
|
||||||
- pageSize(默认 5):number —— 每页行数;缺省 5
|
- pageSize(默认 5):number —— 每页行数;缺省 5
|
||||||
@@ -1028,17 +1028,18 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓
|
|||||||
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
|
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
|
||||||
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
|
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
|
||||||
3. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)
|
3. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)
|
||||||
4. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
|
4. 数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断
|
||||||
5. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
5. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
|
||||||
6. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
6. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
||||||
7. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
7. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
||||||
8. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
8. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
||||||
9. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复
|
9. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
||||||
10. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用
|
10. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复
|
||||||
11. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)
|
11. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用
|
||||||
12. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充
|
12. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)
|
||||||
13. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出
|
13. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充
|
||||||
14. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮
|
14. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出
|
||||||
|
15. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-table
|
现场演示:apps/docs/demo.html#sec-table
|
||||||
|
|
||||||
|
|||||||
@@ -172,4 +172,25 @@ assert(
|
|||||||
);
|
);
|
||||||
assert(all.getPageInfo().totalPages === 1, 'getAllRows 不影响分页快照(2 行 / 每页 2 = 1 页)');
|
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');
|
console.log('SMOKE OK');
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import type {
|
|||||||
export function createTable<T>(
|
export function createTable<T>(
|
||||||
options: TableOptions<T>,
|
options: TableOptions<T>,
|
||||||
): TableInstance<T> & Record<string, unknown> {
|
): TableInstance<T> & Record<string, unknown> {
|
||||||
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 rowKey = options.rowKey ?? ('id' as keyof T & string); // 默认行标识
|
||||||
const getKey: (row: T) => RowKey =
|
const getKey: (row: T) => RowKey =
|
||||||
@@ -91,10 +92,25 @@ export function createTable<T>(
|
|||||||
return model;
|
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<T> = {
|
const instance: TableInstance<T> = {
|
||||||
getState: () => state,
|
getState: () => state,
|
||||||
dispatch,
|
dispatch,
|
||||||
subscribe,
|
subscribe,
|
||||||
|
setRows,
|
||||||
getVisibleRows,
|
getVisibleRows,
|
||||||
getAllRows: () => getVisibleRows(false), // 筛选 + 排序后、未分页的全量行(导出等场景)
|
getAllRows: () => getVisibleRows(false), // 筛选 + 排序后、未分页的全量行(导出等场景)
|
||||||
getColumns: () => columns,
|
getColumns: () => columns,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export interface TableInstance<T = unknown> {
|
|||||||
getState(): TableState; // 当前状态快照
|
getState(): TableState; // 当前状态快照
|
||||||
dispatch(action: Action): void; // 派发动作,唯一变更入口
|
dispatch(action: Action): void; // 派发动作,唯一变更入口
|
||||||
subscribe(fn: () => void): () => void; // 订阅变更,返回取消函数
|
subscribe(fn: () => void): () => void; // 订阅变更,返回取消函数
|
||||||
|
setRows(rows: T[]): void; // 整体换数据源(筛选结果 / 轮询刷新),选择状态按 rowKey 剔除失效行
|
||||||
getVisibleRows(): RowModel<T>; // 可见行模型(过完整管线)
|
getVisibleRows(): RowModel<T>; // 可见行模型(过完整管线)
|
||||||
getAllRows(): RowModel<T>; // 筛选 + 排序后、未分页的全量行(导出用)
|
getAllRows(): RowModel<T>; // 筛选 + 排序后、未分页的全量行(导出用)
|
||||||
getColumns(): ColumnDef<T>[]; // 列定义
|
getColumns(): ColumnDef<T>[]; // 列定义
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
paginationPlugin,
|
paginationPlugin,
|
||||||
selectionPlugin,
|
selectionPlugin,
|
||||||
sortPlugin,
|
sortPlugin,
|
||||||
|
type RowKey,
|
||||||
} from '@ui-arch/table-core';
|
} 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 { iconSearch } from './icons.js'; import { computed, effect, h, renderList, signal } from '@ui-arch/frame';
|
||||||
import { attachResize } from './columnResize.js';
|
import { attachResize } from './columnResize.js';
|
||||||
import { exportButton } from './csv.js';
|
import { exportButton } from './csv.js';
|
||||||
@@ -13,23 +13,23 @@ import { button } from './button.js';
|
|||||||
import { skeleton } from './skeleton.js';
|
import { skeleton } from './skeleton.js';
|
||||||
import { textField } from './textField.js';
|
import { textField } from './textField.js';
|
||||||
import { emptyRow } from './tableEmpty.js';
|
import { emptyRow } from './tableEmpty.js';
|
||||||
|
import { attachDataSource, readRows } from './tableDataSource.js';
|
||||||
import type { DataTableProps, TableApi } from './types.js';
|
import type { DataTableProps, TableApi } from './types.js';
|
||||||
|
|
||||||
/*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/
|
/*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/
|
||||||
export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): HTMLElement {
|
export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): HTMLElement {
|
||||||
// —— 状态集中声明 ——
|
const selectable = props.selectable ?? false; // 状态集中声明:行选择开关
|
||||||
const selectable = props.selectable ?? false; // 行选择开关
|
|
||||||
const keyword = signal(''); // 筛选框关键字:受控信号,空状态里的「清除筛选」靠它一键还原
|
const keyword = signal(''); // 筛选框关键字:受控信号,空状态里的「清除筛选」靠它一键还原
|
||||||
const table = createTable({
|
const table = createTable({
|
||||||
data: props.data,
|
data: readRows(props.data), // 信号数据源先取当前值,后续靠 attachDataSource 换
|
||||||
columns: props.columns,
|
columns: props.columns,
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
plugins: [filterPlugin<T>(), sortPlugin<T>(), paginationPlugin<T>(), selectionPlugin<T>()],
|
plugins: [filterPlugin<T>(), sortPlugin<T>(), paginationPlugin<T>(), selectionPlugin<T>()],
|
||||||
initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } },
|
initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } },
|
||||||
});
|
});
|
||||||
const api = table as unknown as TableApi<T>; // 用到的两个取数方法在类型别名里声明
|
const api = table as unknown as TableApi<T>; // 用到的两个取数方法在类型别名里声明
|
||||||
const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统
|
const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增 → 派生统一重算
|
||||||
table.subscribe(() => version(version() + 1));
|
table.subscribe(() => version(version() + 1)); attachDataSource(table, props.data); // 信号数据源变化即换源(静态数组直接返回)
|
||||||
|
|
||||||
// —— 派生:全部由 version 单一来源推导,语义与内核一致 ——
|
// —— 派生:全部由 version 单一来源推导,语义与内核一致 ——
|
||||||
const visible = computed(() => {
|
const visible = computed(() => {
|
||||||
|
|||||||
@@ -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<T>(data: T[] | Signal<T[]>): T[] {
|
||||||
|
return typeof data === 'function' ? data() : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 作用:给表格挂换源 effect——数据源是信号时信号变化即整体换源,静态数组直接返回(不多挂 effect、不多算一次派生) ***/
|
||||||
|
export function attachDataSource<T extends { id: RowKey }>(
|
||||||
|
table: TableInstance<T>,
|
||||||
|
data: T[] | Signal<T[]>,
|
||||||
|
): void {
|
||||||
|
if (typeof data !== 'function') return; // 卫语句:静态数组无需响应式换源
|
||||||
|
const source = data;
|
||||||
|
effect(() => table.setRows(source())); // setRows 内部按 rowKey 剔除失效选择项,页码与排序保持原意
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ export interface TableColumn<T = unknown> extends ColumnDef<T> {
|
|||||||
|
|
||||||
/*** 数据表格入参 ***/
|
/*** 数据表格入参 ***/
|
||||||
export interface DataTableProps<T> {
|
export interface DataTableProps<T> {
|
||||||
data: T[]; // 全量行数据,行须含 id 字段作 rowKey
|
data: T[] | Signal<T[]>; // 全量行数据(行须含 id 作 rowKey);传信号 / computed 则数据变化时自动换源
|
||||||
columns: TableColumn<T>[]; // 列定义:内核字段 + align / format
|
columns: TableColumn<T>[]; // 列定义:内核字段 + align / format
|
||||||
filterKey?: string; // 工具栏筛选的列;缺省取第一列
|
filterKey?: string; // 工具栏筛选的列;缺省取第一列
|
||||||
pageSize?: number; // 每页行数;缺省 5
|
pageSize?: number; // 每页行数;缺省 5
|
||||||
|
|||||||
@@ -1887,9 +1887,9 @@
|
|||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"n": "data",
|
"n": "data",
|
||||||
"t": "T[]",
|
"t": "T[] | Signal<T[]>",
|
||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "全量行数据,每行须含 id 字段作 rowKey"
|
"d": "全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "columns",
|
"n": "columns",
|
||||||
@@ -1951,6 +1951,7 @@
|
|||||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||||
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
||||||
|
"数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断",
|
||||||
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
||||||
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
||||||
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
||||||
|
|||||||
Reference in New Issue
Block a user