From 3914d5d47d6befa0db079db7a24eade380b79b16 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 23:32:35 +0800 Subject: [PATCH] =?UTF-8?q?iter(g294):=20=E6=95=B0=E6=8D=AE=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E6=94=AF=E6=8C=81=E5=88=97=E6=98=BE=E9=9A=90=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 11 +++++- .ua/index.json | 19 +++++++-- apps/demo/main.ts | 3 +- llms-full.txt | 8 +++- packages/skin/ui/src/csv.ts | 9 +++-- packages/skin/ui/src/dataTable.ts | 59 +++++++++++----------------- packages/skin/ui/src/tableBridge.ts | 42 ++++++++++++++++++++ packages/skin/ui/src/tableColumns.ts | 35 +++++++++++++++++ packages/skin/ui/src/types.ts | 2 + packages/skin/ui/ua.manifest.json | 19 +++++++-- packages/skin/ui/ui.css | 26 ++++++++++++ 11 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 packages/skin/ui/src/tableBridge.ts create mode 100644 packages/skin/ui/src/tableColumns.ts diff --git a/.ua/backlog.json b/.ua/backlog.json index 346dc13..eabf7bf 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -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": "" } ] -} +} \ No newline at end of file diff --git a/.ua/index.json b/.ua/index.json index b3575ef..32f0e3d 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -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", + "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 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 8f26afe..9b506e9 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -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 }, diff --git a/llms-full.txt b/llms-full.txt index d9d8bcc..1f5eff8 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -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 —— 受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动 - loading:boolean | Signal —— 加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy - selected:Signal —— 受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写 -- exportable:boolean —— 工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页);缺省 false +- exportable:boolean —— 工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页),列跟随当前可见列;缺省 false - exportName:string —— 导出文件名;缺省 table.csv - onRowClick(回调):(row: T) => void —— 整行点击 / Enter / 空格的回调;传了行才可聚焦可点(未传时行保持纯展示,不抢 Tab 焦点) +- columnToggle(默认 false):boolean —— 工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列(最后一列的勾选框锁死),缺省 false +- hiddenColumns:Signal —— 受控隐藏列键集:与「列」面板双向同步,外部可据此持久化用户偏好;隐藏列不渲染、不进导出 示例: @@ -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 diff --git a/packages/skin/ui/src/csv.ts b/packages/skin/ui/src/csv.ts index f525d7c..0591ecf 100644 --- a/packages/skin/ui/src/csv.ts +++ b/packages/skin/ui/src/csv.ts @@ -20,16 +20,17 @@ export function downloadCsv(name: string, header: string[], rows: unknown[][]): URL.revokeObjectURL(url); } -/*** 导出按钮:取筛选 + 排序后的全量行(不分页),列序与表头一致、单元格走 format ***/ +/*** 导出按钮:取筛选 + 排序后的全量行(不分页),列序与表头一致、单元格走 format;列可由读取器惰性给出,跟随列显隐 ***/ export function exportButton( table: { getAllRows(): RowModel; getCellValue(row: T, key: string): unknown }, - columns: TableColumn[], + columns: TableColumn[] | (() => TableColumn[]), 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; // 与单元格显示同源,导出内容与看到的一致 })); diff --git a/packages/skin/ui/src/dataTable.ts b/packages/skin/ui/src/dataTable.ts index b7d1aa7..a7dac85 100644 --- a/packages/skin/ui/src/dataTable.ts +++ b/packages/skin/ui/src/dataTable.ts @@ -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(props: DataTableProps): 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([]); // 隐藏列键集:外部没给就自己持有 + 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(props: DataTableProps): 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(props: DataTableProps): 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(props: DataTableProps): 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(props: DataTableProps): H return h('tr', rowProps, ...cells); }); }); - // —— 列宽:数字落成 固定宽度,'flex' 或未设置交给浏览器自适应 —— - const cols = props.columns.map((col) => (typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col'))); // 每个数据列一个 ,拖拽直接改它 - const colgroup = h('colgroup', null, selectable ? h('col', { style: 'width:44px' }) : null, ...cols); + // —— 列宽:数字落成 固定宽度,'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')])); // 复用同一批 ,拖拽改的宽度不因显隐丢失 + 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(props: DataTableProps): 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(props: DataTableProps): 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, diff --git a/packages/skin/ui/src/tableBridge.ts b/packages/skin/ui/src/tableBridge.ts new file mode 100644 index 0000000..8c6f29c --- /dev/null +++ b/packages/skin/ui/src/tableBridge.ts @@ -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; // 外部受控页码信号 + 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; // 外部受控选中行信号 + 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]); // 内部勾选 → 写回外部(拷贝,免外部改到内核数组) + }); +} diff --git a/packages/skin/ui/src/tableColumns.ts b/packages/skin/ui/src/tableColumns.ts new file mode 100644 index 0000000..cce773a --- /dev/null +++ b/packages/skin/ui/src/tableColumns.ts @@ -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 { + columns: TableColumn[]; // 全部列,顺序即表头顺序 + hidden: Signal; // 隐藏列键集:受控信号,勾选即刻写回 + disabled?: () => boolean; // 禁用读取器(加载中禁止改列) +} + +/*** 列显隐面板:工具栏「列」按钮 + 勾选弹层;至少保留一列,避免表格被关成空壳 ***/ +export function columnPicker(props: ColumnPickerProps): 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); +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index c64c837..4226ade 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -73,6 +73,8 @@ export interface DataTableProps { selected?: Signal; // 受控选中行信号:与内核选择双向同步,供批量操作读取 exportable?: boolean; // 工具栏显示导出按钮:导出筛选 + 排序后的全量行(不分页);缺省 false exportName?: string; // 导出文件名;缺省 table.csv + columnToggle?: boolean; // 工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列;缺省 false + hiddenColumns?: Signal; // 受控隐藏列键集:与「列」面板双向同步,供调用方记住用户偏好 onRowClick?: (row: T) => void; // 整行点击 / Enter / 空格的回调;传了行才可聚焦(未传时行不可交互) } diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index ce97319..cc03edb 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -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", + "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 @@ } ] } -} +} \ No newline at end of file diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 5546a8b..93ef058 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -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; /* 窄屏(页码 + 四个控件)自动换行,宁可两行也不横向溢出 */