iter(g294): 数据表格支持列显隐面板

This commit is contained in:
HIsheR
2026-10-26 23:32:35 +08:00
parent abba348c9a
commit 3914d5d47d
11 changed files with 183 additions and 50 deletions
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+5 -4
View File
@@ -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; // 与单元格显示同源,导出内容与看到的一致
}));
+23 -36
View File
@@ -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,
+42
View File
@@ -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]); // 内部勾选 → 写回外部(拷贝,免外部改到内核数组)
});
}
+35
View File
@@ -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);
}
+2
View File
@@ -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 / 空格的回调;传了行才可聚焦(未传时行不可交互)
}
+15 -2
View File
@@ -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 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效"
]
},
{
+26
View File
@@ -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; /* 窄屏(页码 + 四个控件)自动换行,宁可两行也不横向溢出 */