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
+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,