iter(g294): 数据表格支持列显隐面板
This commit is contained in:
+10
-1
@@ -3032,6 +3032,15 @@
|
||||
"status": "done",
|
||||
"domain": "tooling",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "g294",
|
||||
"domain": "tune",
|
||||
"status": "open",
|
||||
"title": "dataTable 列显隐:工具栏「列」面板 + 受控 hiddenColumns",
|
||||
"motivation": "表格列多时用户只想看关心的几列,目前只能靠调用方自己改 columns 重渲染;且 dataTable.ts 已顶到 199 行上限,受控桥接与工具栏构件该分出去。",
|
||||
"acceptance": "① 开 columnToggle 后工具栏出现「列」按钮,弹层勾选即时增删列(表头/单元格/colspan/骨架行/空状态/导出全部跟随);② 至少保留一列可见,最后一列的勾选框置灰;③ 传 hiddenColumns 信号可受控并写回;④ 样式只消费 --ua-* 令牌,npm run audit:visual 计数不涨;⑤ 受控页码与选中桥接抽到 tableBridge.ts,dataTable.ts 仍 ≤200 行;⑥ npm run check 全绿。",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "ui-arch-registry-index/0.1",
|
||||
"generatedAt": "2026-10-26T15:21:15.323Z",
|
||||
"generatedAt": "2026-10-26T15:31:57.268Z",
|
||||
"repoRoot": ".",
|
||||
"packages": [
|
||||
{
|
||||
@@ -2102,7 +2102,7 @@
|
||||
{
|
||||
"n": "exportable",
|
||||
"t": "boolean",
|
||||
"d": "工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页);缺省 false"
|
||||
"d": "工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页),列跟随当前可见列;缺省 false"
|
||||
},
|
||||
{
|
||||
"n": "exportName",
|
||||
@@ -2114,6 +2114,17 @@
|
||||
"t": "(row: T) => void",
|
||||
"k": 1,
|
||||
"d": "整行点击 / Enter / 空格的回调;传了行才可聚焦可点(未传时行保持纯展示,不抢 Tab 焦点)"
|
||||
},
|
||||
{
|
||||
"n": "columnToggle",
|
||||
"t": "boolean",
|
||||
"d": "工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列(最后一列的勾选框锁死),缺省 false",
|
||||
"df": "false"
|
||||
},
|
||||
{
|
||||
"n": "hiddenColumns",
|
||||
"t": "Signal<string[]>",
|
||||
"d": "受控隐藏列键集:与「列」面板双向同步,外部可据此持久化用户偏好;隐藏列不渲染、不进导出"
|
||||
}
|
||||
],
|
||||
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
||||
@@ -2133,7 +2144,9 @@
|
||||
"需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)",
|
||||
"可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充",
|
||||
"需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出",
|
||||
"整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮"
|
||||
"整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮",
|
||||
"需要让用户自己调列时开 columnToggle:工具栏多一个「列」按钮,弹层勾选即刻重绘表头与单元格(隐藏列不占 colspan,空状态与骨架行同步收窄)",
|
||||
"想记住用户偏好就传 hiddenColumns 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+2
-1
@@ -1467,11 +1467,12 @@ const content = h(
|
||||
section(
|
||||
'sec-table',
|
||||
'数据表格',
|
||||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
|
||||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「列」按钮勾选即可隐藏/恢复列(至少留一列,隐藏列不占表头也不进导出)、「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
|
||||
card({ title: '用户表' }, dataTable({
|
||||
data: users,
|
||||
selectable: true,
|
||||
exportable: true,
|
||||
columnToggle: true,
|
||||
loading: tableLoading,
|
||||
columns: [
|
||||
{ key: 'name', header: '姓名', sortable: true, width: 160 },
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||
|
||||
> 自动生成于 2026-10-26T15:21:15.323Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 自动生成于 2026-10-26T15:31:57.268Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||
|
||||
## 快速上手
|
||||
@@ -1027,9 +1027,11 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
|
||||
- page:Signal<number> —— 受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动
|
||||
- loading:boolean | Signal<boolean> —— 加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy
|
||||
- selected:Signal<RowKey[]> —— 受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写
|
||||
- exportable:boolean —— 工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页);缺省 false
|
||||
- exportable:boolean —— 工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页),列跟随当前可见列;缺省 false
|
||||
- exportName:string —— 导出文件名;缺省 table.csv
|
||||
- onRowClick(回调):(row: T) => void —— 整行点击 / Enter / 空格的回调;传了行才可聚焦可点(未传时行保持纯展示,不抢 Tab 焦点)
|
||||
- columnToggle(默认 false):boolean —— 工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列(最后一列的勾选框锁死),缺省 false
|
||||
- hiddenColumns:Signal<string[]> —— 受控隐藏列键集:与「列」面板双向同步,外部可据此持久化用户偏好;隐藏列不渲染、不进导出
|
||||
|
||||
示例:
|
||||
|
||||
@@ -1055,6 +1057,8 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓
|
||||
14. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充
|
||||
15. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出
|
||||
16. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮
|
||||
17. 需要让用户自己调列时开 columnToggle:工具栏多一个「列」按钮,弹层勾选即刻重绘表头与单元格(隐藏列不占 colspan,空状态与骨架行同步收窄)
|
||||
18. 想记住用户偏好就传 hiddenColumns 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效
|
||||
|
||||
现场演示:apps/docs/demo.html#sec-table
|
||||
|
||||
|
||||
@@ -20,16 +20,17 @@ export function downloadCsv(name: string, header: string[], rows: unknown[][]):
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/*** 导出按钮:取筛选 + 排序后的全量行(不分页),列序与表头一致、单元格走 format ***/
|
||||
/*** 导出按钮:取筛选 + 排序后的全量行(不分页),列序与表头一致、单元格走 format;列可由读取器惰性给出,跟随列显隐 ***/
|
||||
export function exportButton<T>(
|
||||
table: { getAllRows(): RowModel<T>; getCellValue(row: T, key: string): unknown },
|
||||
columns: TableColumn<T>[],
|
||||
columns: TableColumn<T>[] | (() => TableColumn<T>[]),
|
||||
name: string,
|
||||
): HTMLElement {
|
||||
const run = (): void => {
|
||||
const cols = typeof columns === 'function' ? columns() : columns; // 点导出时才取列:隐藏列变化后导出跟着变
|
||||
const rows = table.getAllRows().rows;
|
||||
const header = columns.map((c) => String(c.header ?? c.key));
|
||||
const body = rows.map((row) => columns.map((c) => {
|
||||
const header = cols.map((c) => String(c.header ?? c.key));
|
||||
const body = rows.map((row) => cols.map((c) => {
|
||||
const raw = table.getCellValue(row, c.key);
|
||||
return c.format ? c.format(raw, row) : raw; // 与单元格显示同源,导出内容与看到的一致
|
||||
}));
|
||||
|
||||
@@ -14,6 +14,8 @@ import { textField } from './textField.js';
|
||||
import { emptyRow } from './tableEmpty.js';
|
||||
import { tablePager } from './tablePager.js';
|
||||
import { attachDataSource, readRows } from './tableDataSource.js';
|
||||
import { bridgePage, bridgeSelection } from './tableBridge.js';
|
||||
import { columnPicker } from './tableColumns.js';
|
||||
import type { DataTableProps, TableApi } from './types.js';
|
||||
|
||||
/*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/
|
||||
@@ -47,35 +49,14 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
const selectedCount = computed(() => state().selection.length);
|
||||
// 加载态读取器:布尔直接读,信号则保持响应性(供 renderList 依赖收集)
|
||||
const busy = (): boolean => (typeof props.loading === 'function' ? props.loading() : props.loading === true);
|
||||
// —— 受控页码桥接:外部信号 ↔ 内核分页;外部越界先钳制,signal 等值写回不触发,双向同步收敛安全 ——
|
||||
if (props.page) {
|
||||
const pageSig = props.page;
|
||||
effect(() => {
|
||||
const external = Math.min(api.getPageInfo().totalPages, Math.max(1, pageSig())); // 越界钳制,防无效派发
|
||||
if (external !== info().page) table.dispatch({ type: 'setPage', page: external }); // 外部翻页 → 内核
|
||||
});
|
||||
effect(() => {
|
||||
const internal = info().page;
|
||||
if (internal !== pageSig()) pageSig(internal); // 内部翻页(筛选/内置翻页器)→ 写回外部
|
||||
});
|
||||
}
|
||||
|
||||
// —— 受控选中桥接:外部信号 ↔ 内核选择集;比较内容而非引用,避免来回派发 ——
|
||||
if (props.selected) {
|
||||
const selSig = props.selected;
|
||||
const sameKeys = (a: RowKey[], b: RowKey[]): boolean => a.length === b.length && a.every((k, i) => k === b[i]);
|
||||
effect(() => {
|
||||
const external = selSig();
|
||||
if (!sameKeys(external, state().selection)) table.dispatch({ type: 'setSelection', keys: external }); // 外部改写 → 内核
|
||||
});
|
||||
effect(() => {
|
||||
const internal = state().selection;
|
||||
if (!sameKeys(internal, selSig())) selSig([...internal]); // 内部勾选 → 写回外部(拷贝避免外部改到内核数组)
|
||||
});
|
||||
}
|
||||
// —— 受控桥接:页码与选中都走 tableBridge,外部信号与内核状态双向收敛 ——
|
||||
if (props.page) bridgePage({ page: props.page, info, onPage: (page) => table.dispatch({ type: 'setPage', page }) });
|
||||
if (props.selected) bridgeSelection({ selected: props.selected, selection: () => state().selection, onSelection: (keys) => table.dispatch({ type: 'setSelection', keys }) });
|
||||
|
||||
const filterKey = props.filterKey ?? props.columns[0].key;
|
||||
const colspan = props.columns.length + (selectable ? 1 : 0);
|
||||
const hidden = props.hiddenColumns ?? signal<string[]>([]); // 隐藏列键集:外部没给就自己持有
|
||||
const shown = computed(() => props.columns.filter((c) => !hidden().includes(c.key))); // 可见列:表头 / 单元格 / 导出 / colspan 全从这一处派生
|
||||
const span = (): number => shown().length + (selectable ? 1 : 0); // 跨列数随可见列变化
|
||||
|
||||
// —— 表头全选框:checked / indeterminate 三态跟随当页选中情况 ——
|
||||
const headCheck = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
|
||||
@@ -118,7 +99,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
col.header ?? col.key,
|
||||
col.sortable ? sortMark(col.key) : null,
|
||||
);
|
||||
if (typeof col.width === 'number') attachResize(el, cols[props.columns.indexOf(col)], col.width, String(col.header ?? col.key)); // 定宽列可拖拽
|
||||
if (typeof col.width === 'number') attachResize(el, colEls.get(col.key)!, col.width, String(col.header ?? col.key)); // 定宽列可拖拽
|
||||
if (col.sortable) {
|
||||
effect(() => {
|
||||
const rule = state().sorting.find((r) => r.key === col.key); // 与视觉箭头同源
|
||||
@@ -132,7 +113,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
const skeletonRows = (): Node[] => {
|
||||
const count = Math.min(props.pageSize ?? 5, 6);
|
||||
const cell = () => h('td', null, skeleton({ variant: 'text', rows: 1 }));
|
||||
return Array.from({ length: count }, () => h('tr', { class: 'ua-loading-row' }, ...Array.from({ length: colspan }, cell)));
|
||||
return Array.from({ length: count }, () => h('tr', { class: 'ua-loading-row' }, ...Array.from({ length: span() }, cell)));
|
||||
};
|
||||
|
||||
const tbody = h('tbody');
|
||||
@@ -140,11 +121,11 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
if (busy()) return skeletonRows(); // 加载态优先:不看数据,避免闪出旧行
|
||||
const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控)
|
||||
const rows = visible().rows;
|
||||
if (!rows.length) return [emptyRow(colspan, keyword, () => table.dispatch({ type: 'setFilter', key: filterKey, value: '' }), props.emptyText)]; // 空状态:筛选词与内核筛选一起清
|
||||
if (!rows.length) return [emptyRow(span(), keyword, () => table.dispatch({ type: 'setFilter', key: filterKey, value: '' }), props.emptyText)]; // 空状态:筛选词与内核筛选一起清
|
||||
return rows.map((row, i) => {
|
||||
const key = visible().keys[i];
|
||||
const selected = sel.includes(key);
|
||||
const cells = props.columns.map((col) => {
|
||||
const cells = shown().map((col) => {
|
||||
const raw = table.getCellValue(row, col.key);
|
||||
const text = col.format ? col.format(raw, row) : String(raw); // format 只产出纯文本,天然免注入
|
||||
return h('td', col.align ? { class: `ua-align-${col.align}` } : null, text);
|
||||
@@ -161,9 +142,10 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
return h('tr', rowProps, ...cells);
|
||||
});
|
||||
});
|
||||
// —— 列宽:数字落成 <col> 固定宽度,'flex' 或未设置交给浏览器自适应 ——
|
||||
const cols = props.columns.map((col) => (typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col'))); // 每个数据列一个 <col>,拖拽直接改它
|
||||
const colgroup = h('colgroup', null, selectable ? h('col', { style: 'width:44px' }) : null, ...cols);
|
||||
// —— 列宽:数字落成 <col> 固定宽度,'flex' 或未设置交给浏览器自适应;按列键建一次,显隐只切换是否挂进 colgroup ——
|
||||
const colEls = new Map(props.columns.map((col) => [col.key, typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col')])); // 复用同一批 <col>,拖拽改的宽度不因显隐丢失
|
||||
const colgroup = h('colgroup');
|
||||
renderList(colgroup, () => [...(selectable ? [h('col', { style: 'width:44px' })] : []), ...shown().map((c) => colEls.get(c.key)!)]);
|
||||
|
||||
const filterInput = textField({
|
||||
placeholder: `筛选 ${filterKey}…`,
|
||||
@@ -176,6 +158,10 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
},
|
||||
});
|
||||
|
||||
// —— 表头:随可见列重绘(隐藏列既不占表头也不占单元格) ——
|
||||
const thead = h('thead');
|
||||
renderList(thead, () => [h('tr', null, ...(selectable ? [headCheck] : []), ...shown().map(th))]);
|
||||
|
||||
return h(
|
||||
'section',
|
||||
{ class: 'ua-table', 'aria-busy': () => String(busy()) },
|
||||
@@ -184,9 +170,10 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
{ class: 'ua-table-toolbar' },
|
||||
filterInput,
|
||||
selectable ? h('span', { class: 'ua-sel-count' }, () => (selectedCount() ? `已选 ${selectedCount()} 行` : '')) : null,
|
||||
props.exportable ? exportButton(table, props.columns, props.exportName ?? 'table.csv') : null, // 导出筛选+排序后的全量行
|
||||
props.columnToggle ? columnPicker({ columns: props.columns, hidden, disabled: busy }) : null, // 列显隐面板:勾选即重绘表头与单元格
|
||||
props.exportable ? exportButton(table, () => shown(), props.exportName ?? 'table.csv') : null, // 导出筛选+排序后的全量行,列跟随当前可见列
|
||||
),
|
||||
h('div', { class: 'ua-table-scroll' }, h('table', null, colgroup, h('thead', null, h('tr', null, selectable ? headCheck : null, ...props.columns.map(th))), tbody)),
|
||||
h('div', { class: 'ua-table-scroll' }, h('table', null, colgroup, thead, tbody)),
|
||||
tablePager({
|
||||
info,
|
||||
total: () => visible().total,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { effect } from '@ui-arch/frame';
|
||||
import type { Signal } from '@ui-arch/frame';
|
||||
import type { PageInfo, RowKey } from '@ui-arch/table-core';
|
||||
|
||||
/*** 受控页码桥接入参:只给读取器与派发口,桥接层不依赖内核实例 ***/
|
||||
export interface PageBridgeProps {
|
||||
page: Signal<number>; // 外部受控页码信号
|
||||
info: () => PageInfo; // 分页快照读取器(响应式,读它才收集依赖)
|
||||
onPage: (page: number) => void; // 写回内核的派发口
|
||||
}
|
||||
|
||||
/*** 受控页码双向同步:外部越界先按总页数钳制再派发,内核侧只在页码真变化时写回外部 ***/
|
||||
export function bridgePage(props: PageBridgeProps): void {
|
||||
effect(() => {
|
||||
const next = Math.min(props.info().totalPages, Math.max(1, props.page())); // 越界钳制,防无效派发
|
||||
if (next !== props.info().page) props.onPage(next); // 外部改写 → 内核
|
||||
});
|
||||
effect(() => {
|
||||
const inner = props.info().page;
|
||||
if (inner !== props.page()) props.page(inner); // 内部翻页(筛选 / 内置翻页器)→ 写回外部
|
||||
});
|
||||
}
|
||||
|
||||
/*** 受控选中桥接入参 ***/
|
||||
export interface SelectionBridgeProps {
|
||||
selected: Signal<RowKey[]>; // 外部受控选中行信号
|
||||
selection: () => RowKey[]; // 内核选中集读取器
|
||||
onSelection: (keys: RowKey[]) => void; // 写回内核的派发口
|
||||
}
|
||||
|
||||
/*** 受控选中双向同步:比较内容而非引用,避免同一份选中集来回派发 ***/
|
||||
export function bridgeSelection(props: SelectionBridgeProps): void {
|
||||
const same = (a: RowKey[], b: RowKey[]): boolean => a.length === b.length && a.every((k, i) => k === b[i]);
|
||||
effect(() => {
|
||||
const external = props.selected();
|
||||
if (!same(external, props.selection())) props.onSelection(external); // 外部改写 → 内核
|
||||
});
|
||||
effect(() => {
|
||||
const inner = props.selection();
|
||||
if (!same(inner, props.selected())) props.selected([...inner]); // 内部勾选 → 写回外部(拷贝,免外部改到内核数组)
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { computed, h, renderList } from '@ui-arch/frame';
|
||||
import type { Signal } from '@ui-arch/frame';
|
||||
import { button } from './button.js';
|
||||
import { popover } from './popover.js';
|
||||
import type { TableColumn } from './types.js';
|
||||
|
||||
/*** 列显隐面板入参 ***/
|
||||
export interface ColumnPickerProps<T> {
|
||||
columns: TableColumn<T>[]; // 全部列,顺序即表头顺序
|
||||
hidden: Signal<string[]>; // 隐藏列键集:受控信号,勾选即刻写回
|
||||
disabled?: () => boolean; // 禁用读取器(加载中禁止改列)
|
||||
}
|
||||
|
||||
/*** 列显隐面板:工具栏「列」按钮 + 勾选弹层;至少保留一列,避免表格被关成空壳 ***/
|
||||
export function columnPicker<T>(props: ColumnPickerProps<T>): HTMLElement {
|
||||
/*** 切换一列:已隐藏则恢复,可见则隐藏;隐藏到只剩一列时不再放行 ***/
|
||||
const toggle = (key: string): void => {
|
||||
const cur = props.hidden();
|
||||
if (cur.includes(key)) { props.hidden(cur.filter((k) => k !== key)); return; } // 恢复该列
|
||||
if (cur.length < props.columns.length - 1) props.hidden([...cur, key]); // 至多隐藏到只剩一列
|
||||
};
|
||||
|
||||
const list = h('div', { class: 'ua-col-list' });
|
||||
renderList(list, () => props.columns.map((col) => {
|
||||
const box = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
|
||||
const hiddenNow = props.hidden().includes(col.key);
|
||||
box.checked = !hiddenNow;
|
||||
box.disabled = hiddenNow && props.hidden().length >= props.columns.length - 1; // 最后一列锁死,勾不回去就别给假希望
|
||||
box.addEventListener('change', () => toggle(col.key));
|
||||
return h('label', { class: 'ua-col-item' }, box, h('span', null, String(col.header ?? col.key)));
|
||||
}));
|
||||
|
||||
const trigger = button({ label: '列', variant: 'ghost', disabled: computed(() => props.disabled?.() ?? false) }); // 触发器常驻,弹层由 popover 托管开合
|
||||
return popover({ content: list, placement: 'bottom', trigger: 'click' }, trigger);
|
||||
}
|
||||
@@ -73,6 +73,8 @@ export interface DataTableProps<T> {
|
||||
selected?: Signal<RowKey[]>; // 受控选中行信号:与内核选择双向同步,供批量操作读取
|
||||
exportable?: boolean; // 工具栏显示导出按钮:导出筛选 + 排序后的全量行(不分页);缺省 false
|
||||
exportName?: string; // 导出文件名;缺省 table.csv
|
||||
columnToggle?: boolean; // 工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列;缺省 false
|
||||
hiddenColumns?: Signal<string[]>; // 受控隐藏列键集:与「列」面板双向同步,供调用方记住用户偏好
|
||||
onRowClick?: (row: T) => void; // 整行点击 / Enter / 空格的回调;传了行才可聚焦(未传时行不可交互)
|
||||
}
|
||||
|
||||
|
||||
@@ -1973,7 +1973,7 @@
|
||||
{
|
||||
"n": "exportable",
|
||||
"t": "boolean",
|
||||
"d": "工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页);缺省 false"
|
||||
"d": "工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页),列跟随当前可见列;缺省 false"
|
||||
},
|
||||
{
|
||||
"n": "exportName",
|
||||
@@ -1985,6 +1985,17 @@
|
||||
"t": "(row: T) => void",
|
||||
"k": 1,
|
||||
"d": "整行点击 / Enter / 空格的回调;传了行才可聚焦可点(未传时行保持纯展示,不抢 Tab 焦点)"
|
||||
},
|
||||
{
|
||||
"n": "columnToggle",
|
||||
"t": "boolean",
|
||||
"d": "工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列(最后一列的勾选框锁死),缺省 false",
|
||||
"df": "false"
|
||||
},
|
||||
{
|
||||
"n": "hiddenColumns",
|
||||
"t": "Signal<string[]>",
|
||||
"d": "受控隐藏列键集:与「列」面板双向同步,外部可据此持久化用户偏好;隐藏列不渲染、不进导出"
|
||||
}
|
||||
],
|
||||
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
||||
@@ -2004,7 +2015,9 @@
|
||||
"需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)",
|
||||
"可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充",
|
||||
"需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出",
|
||||
"整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮"
|
||||
"整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮",
|
||||
"需要让用户自己调列时开 columnToggle:工具栏多一个「列」按钮,弹层勾选即刻重绘表头与单元格(隐藏列不占 colspan,空状态与骨架行同步收窄)",
|
||||
"想记住用户偏好就传 hiddenColumns 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4509,4 +4522,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,6 +255,32 @@ body.ua-col-resizing {
|
||||
font-size: var(--ua-font-size-sm);
|
||||
color: var(--ua-color-accent);
|
||||
}
|
||||
/* —— 列显隐面板:勾选项纵向排列,禁用项降透明度,视觉上与下拉菜单同族 —— */
|
||||
.ua-col-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ua-space-1);
|
||||
min-width: 132px;
|
||||
}
|
||||
.ua-col-item {
|
||||
display: flex;
|
||||
gap: var(--ua-space-1);
|
||||
align-items: center;
|
||||
padding: var(--ua-space-05) var(--ua-space-1);
|
||||
border-radius: var(--ua-radius-sm);
|
||||
font-size: var(--ua-font-size-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ua-col-item:hover {
|
||||
background: var(--ua-color-bg);
|
||||
}
|
||||
.ua-col-item .ua-checkbox:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ua-col-item:has(.ua-checkbox:disabled) {
|
||||
color: var(--ua-color-text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ua-pager {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 窄屏(页码 + 四个控件)自动换行,宁可两行也不横向溢出 */
|
||||
|
||||
Reference in New Issue
Block a user