iter(g302): 表头接上多列排序(Shift 叠加)
This commit is contained in:
@@ -3104,6 +3104,15 @@
|
|||||||
"motivation": "库里有 list 展示列表、virtualList 长列表,但缺「可调整顺序」的交互件:任务排优先级、字段顺序、看板列这类场景每次都得自己写一遍拖放,dragstart/dragover 的边界(不拦 dragover 就没 drop、拖拽中重建节点会中断手势)反复踩。",
|
"motivation": "库里有 list 展示列表、virtualList 长列表,但缺「可调整顺序」的交互件:任务排优先级、字段顺序、看板列这类场景每次都得自己写一遍拖放,dragstart/dragover 的边界(不拦 dragover 就没 drop、拖拽中重建节点会中断手势)反复踩。",
|
||||||
"acceptance": "① 整行可拖,指针越过邻行中线即换位,dragend 落定;② 键盘等价 Alt+↑↓ 上下搬一格、Alt+Home/End 搬首尾,裸 ↑↓ 留给页面滚动;③ 顺序写回 value 信号并触发 onChange,拖拽期间跳过外部重排避免中断手势;④ 句柄 { el, move, order },下标越界与禁用时静默忽略;⑤ 样式只消费 --ua-* 令牌,禁止新增硬编码阴影/时长/字号;⑥ manifest/docs/README/demo/llms-full 同步,npm run check 全绿。",
|
"acceptance": "① 整行可拖,指针越过邻行中线即换位,dragend 落定;② 键盘等价 Alt+↑↓ 上下搬一格、Alt+Home/End 搬首尾,裸 ↑↓ 留给页面滚动;③ 顺序写回 value 信号并触发 onChange,拖拽期间跳过外部重排避免中断手势;④ 句柄 { el, move, order },下标越界与禁用时静默忽略;⑤ 样式只消费 --ua-* 令牌,禁止新增硬编码阴影/时长/字号;⑥ manifest/docs/README/demo/llms-full 同步,npm run check 全绿。",
|
||||||
"notes": "新增 src/sortableList.ts(118 行)+ types 两项 + index 导出 + ui.css 六条(只消费令牌);整行可拖、越过邻行中线换位、dragend 落定,Alt+↑↓/Home/End 键盘搬位;顺序写回 value 信号并触发 onChange,拖拽中跳过外部重排;句柄 { el, move, order } 越界静默忽略;组件 87→88,README/AGENTS/ARCHITECTURE 计数与 docs 同步"
|
"notes": "新增 src/sortableList.ts(118 行)+ types 两项 + index 导出 + ui.css 六条(只消费令牌);整行可拖、越过邻行中线换位、dragend 落定,Alt+↑↓/Home/End 键盘搬位;顺序写回 value 信号并触发 onChange,拖拽中跳过外部重排;句柄 { el, move, order } 越界静默忽略;组件 87→88,README/AGENTS/ARCHITECTURE 计数与 docs 同步"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g302",
|
||||||
|
"domain": "tune",
|
||||||
|
"status": "open",
|
||||||
|
"title": "dataTable 接上内核的多列排序:Shift+点击叠加第二列",
|
||||||
|
"motivation": "table-core 的 toggleSort 早就支持 action.multi(按 rules 顺序逐列比较),但皮肤层 dispatch 时从不传 multi,用户按住 Shift 点第二个表头毫无反应,多列排序只能靠手写 setSorting;表头标记也只反映单列方向,看不出比较优先级。",
|
||||||
|
"acceptance": "① 表头点击与键盘 Enter / 空格都读 shiftKey 并透传 multi;② 参与排序的列 ≥ 2 时标记追加优先级序号(↑1 / ↓2),单列时保持原箭头;③ manifest 手把手与 demo 描述同步;④ npm run check 全绿。",
|
||||||
|
"notes": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T16:28:37.124Z",
|
"generatedAt": "2026-10-26T16:30:17.353Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -2134,6 +2134,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||||
|
"多列排序:按住 Shift 点第二个可排序列即叠加——内核按 rules 顺序逐列比较,表头标记带优先级(↑1 / ↓2),不按 Shift 则回到单列语义",
|
||||||
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
||||||
"数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断",
|
"数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断",
|
||||||
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
||||||
|
|||||||
+1
-1
@@ -1472,7 +1472,7 @@ const content = h(
|
|||||||
section(
|
section(
|
||||||
'sec-table',
|
'sec-table',
|
||||||
'数据表格',
|
'数据表格',
|
||||||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「列」按钮勾选即可隐藏/恢复列(至少留一列,隐藏列不占表头也不进导出)、「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
|
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序(按住 Shift 点第二个表头即多列叠加,标记带优先级 ↑1 / ↓2)、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「列」按钮勾选即可隐藏/恢复列(至少留一列,隐藏列不占表头也不进导出)、「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
|
||||||
card({ title: '用户表' }, dataTable({
|
card({ title: '用户表' }, dataTable({
|
||||||
data: users,
|
data: users,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
|||||||
+18
-17
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T16:28:37.124Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T16:30:17.353Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -1045,22 +1045,23 @@ 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. 多列排序:按住 Shift 点第二个可排序列即叠加——内核按 rules 顺序逐列比较,表头标记带优先级(↑1 / ↓2),不按 Shift 则回到单列语义
|
||||||
4. 数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断
|
4. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)
|
||||||
5. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
|
5. 数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断
|
||||||
6. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
6. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
|
||||||
7. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
7. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
||||||
8. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
8. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
||||||
9. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
9. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
||||||
10. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复
|
10. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
||||||
11. 分页条自带每页条数下拉(5 / 10 / 20 条,调用方传的 pageSize 若不在候选里会自动补进去):切换即改页大小并回第 1 页,首末页时上/下一页置灰不再派发无效页码
|
11. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复
|
||||||
12. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用
|
12. 分页条自带每页条数下拉(5 / 10 / 20 条,调用方传的 pageSize 若不在候选里会自动补进去):切换即改页大小并回第 1 页,首末页时上/下一页置灰不再派发无效页码
|
||||||
13. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)
|
13. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用
|
||||||
14. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充
|
14. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)
|
||||||
15. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出
|
15. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充
|
||||||
16. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮
|
16. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出
|
||||||
17. 需要让用户自己调列时开 columnToggle:工具栏多一个「列」按钮,弹层勾选即刻重绘表头与单元格(隐藏列不占 colspan,空状态与骨架行同步收窄)
|
17. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮
|
||||||
18. 想记住用户偏好就传 hiddenColumns 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效
|
18. 需要让用户自己调列时开 columnToggle:工具栏多一个「列」按钮,弹层勾选即刻重绘表头与单元格(隐藏列不占 colspan,空状态与骨架行同步收窄)
|
||||||
|
19. 想记住用户偏好就传 hiddenColumns 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-table
|
现场演示:apps/docs/demo.html#sec-table
|
||||||
|
|
||||||
|
|||||||
@@ -72,28 +72,31 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
headCheck.indeterminate = onPage > 0 && onPage < keys.length;
|
headCheck.indeterminate = onPage > 0 && onPage < keys.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
/*** 排序标记:升 ↑ / 降 ↓ / 未排序 ↕(可点击提示) ***/
|
/*** 排序标记:升 ↑ / 降 ↓ / 未排序 ↕;多列排序时追加优先级序号(↑1 / ↓2) ***/
|
||||||
const sortMark = (key: string) =>
|
const sortMark = (key: string) =>
|
||||||
h(
|
h(
|
||||||
'span',
|
'span',
|
||||||
{ class: 'ua-sort-mark' },
|
{ class: 'ua-sort-mark' },
|
||||||
() => {
|
() => {
|
||||||
const rule = state().sorting.find((r) => r.key === key);
|
const rules = state().sorting;
|
||||||
return rule ? (rule.dir === 'asc' ? '↑' : '↓') : '↕';
|
const at = rules.findIndex((r) => r.key === key);
|
||||||
|
if (at === -1) return '↕';
|
||||||
|
const arrow = rules[at].dir === 'asc' ? '↑' : '↓';
|
||||||
|
return rules.length > 1 ? `${arrow}${at + 1}` : arrow; // 只有多列参与排序时才显示序号,单列保持原样
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const th = (col: (typeof props.columns)[number]) => {
|
const th = (col: (typeof props.columns)[number]) => {
|
||||||
const toggle = () => col.sortable && table.dispatch({ type: 'toggleSort', key: col.key }); // 鼠标与键盘共用的排序入口
|
const toggle = (multi: boolean) => col.sortable && table.dispatch({ type: 'toggleSort', key: col.key, multi }); // multi 由 Shift 决定:叠加 / 单列
|
||||||
const el = h(
|
const el = h(
|
||||||
'th',
|
'th',
|
||||||
{
|
{
|
||||||
class: `${col.sortable ? 'ua-sortable' : ''}${col.align ? ` ua-align-${col.align}` : ''}`.trim(),
|
class: `${col.sortable ? 'ua-sortable' : ''}${col.align ? ` ua-align-${col.align}` : ''}`.trim(),
|
||||||
scope: 'col', // 表头单元格语义:读屏据此把列标题与数据格关联
|
scope: 'col', // 表头单元格语义:读屏据此把列标题与数据格关联
|
||||||
tabindex: col.sortable ? '0' : null, // 可排序表头进 Tab 序,不可排序的不抢焦点
|
tabindex: col.sortable ? '0' : null, // 可排序表头进 Tab 序,不可排序的不抢焦点
|
||||||
onClick: toggle,
|
onClick: (e: MouseEvent) => toggle(e.shiftKey), // Shift+点击 = 多列叠加排序(内核按 rules 顺序逐列比较)
|
||||||
onKeydown: (e: KeyboardEvent) => {
|
onKeydown: (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } // Enter / 空格 等价鼠标点击
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e.shiftKey); } // Enter / 空格 等价鼠标点击,Shift 同样生效
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
col.header ?? col.key,
|
col.header ?? col.key,
|
||||||
|
|||||||
@@ -2005,6 +2005,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||||
|
"多列排序:按住 Shift 点第二个可排序列即叠加——内核按 rules 顺序逐列比较,表头标记带优先级(↑1 / ↓2),不按 Shift 则回到单列语义",
|
||||||
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
||||||
"数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断",
|
"数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断",
|
||||||
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
||||||
|
|||||||
Reference in New Issue
Block a user