iter(g294): 数据表格支持列显隐面板
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user