iter(g315): 表格支持行展开详情

This commit is contained in:
HIsheR
2026-10-27 00:46:37 +08:00
parent 27500636c4
commit aed5c75825
9 changed files with 95 additions and 9 deletions
+14 -5
View File
@@ -12,6 +12,7 @@ import { exportButton } from './csv.js';
import { skeleton } from './skeleton.js';
import { textField } from './textField.js';
import { emptyRow } from './tableEmpty.js';
import { detailRow, expandCell } from './tableExpand.js';
import { tablePager } from './tablePager.js';
import { attachDataSource, readRows } from './tableDataSource.js';
import { bridgePage, bridgeSelection } from './tableBridge.js';
@@ -21,6 +22,7 @@ import type { DataTableProps, TableApi } from './types.js';
/*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/
export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): HTMLElement {
const selectable = props.selectable ?? false; // 状态集中声明:行选择开关
const expanded = signal<RowKey[]>([]); // 已展开行的 rowKey:纯视图状态,不参与内核派生
const keyword = signal(''); // 筛选框关键字:受控信号,空状态里的「清除筛选」靠它一键还原
const table = createTable({
data: readRows(props.data), // 信号数据源先取当前值,后续靠 attachDataSource 换
@@ -56,7 +58,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
const filterKey = props.filterKey ?? props.columns[0].key;
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); // 跨列数随可见列变化
const span = (): number => shown().length + (selectable ? 1 : 0) + (props.renderDetail ? 1 : 0); // 跨列数随可见列与展开列变化
// —— 表头全选框:checked / indeterminate 三态跟随当页选中情况 ——
const headCheck = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
@@ -125,9 +127,10 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控)
const rows = visible().rows;
if (!rows.length) return [emptyRow(span(), keyword, () => table.dispatch({ type: 'setFilter', key: filterKey, value: '' }), props.emptyText, props.emptyAction)]; // 空状态:筛选词与内核筛选一起清;真没数据时给调用方的操作插槽
return rows.map((row, i) => {
return rows.flatMap((row, i) => {
const key = visible().keys[i];
const selected = sel.includes(key);
const open = !!props.renderDetail && expanded().includes(key); // 展开态按 rowKey 记:翻页或筛选后同一行仍保持着
const cells = shown().map((col) => {
const raw = table.getCellValue(row, col.key);
const text = col.format ? col.format(raw, row) : String(raw); // format 只产出纯文本,天然免注入
@@ -139,10 +142,12 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
check.addEventListener('change', () => table.dispatch({ type: 'toggleSelect', key }));
cells.unshift(h('td', { class: 'ua-cell-check' }, check));
}
if (props.renderDetail) cells.unshift(expandCell(open, `第 ${i + 1} 行`, () => expanded(open ? expanded().filter((k) => k !== key) : [...expanded(), key]))); // 展开列在最前:与选择列并列但不互斥
const rowProps = props.onRowClick // 传了才把整行做成可聚焦按钮语义(Tab 进入、Enter / 空格触发)
? { tabindex: '0', class: `ua-row-clickable${selected ? ' ua-row-selected' : ''}`, onclick: () => props.onRowClick?.(row), onkeydown: (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onRowClick?.(row); } } }
: selected ? { class: 'ua-row-selected' } : null;
return h('tr', rowProps, ...cells);
const tr = h('tr', rowProps, ...cells);
return props.renderDetail && open ? [tr, detailRow(span(), props.renderDetail(row))] : [tr];
});
});
// —— 列宽:数字落成 <col> 固定宽度,'flex' 或未设置交给浏览器自适应;按列键建一次,显隐只切换是否挂进 colgroup ——
@@ -161,9 +166,13 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
},
});
// —— 表头:随可见列重绘(隐藏列既不占表头也不占单元格) ——
// —— 表头:随可见列重绘(隐藏列既不占表头也不占单元格);选择与展开各占一个 th,顺序与行内单元格一致 ——
const thead = h('thead');
renderList(thead, () => [h('tr', null, ...(selectable ? [headCheck] : []), ...shown().map(th))]);
const lead = (): unknown[] => [
props.renderDetail ? h('th', { class: 'ua-cell-expand', scope: 'col' }, h('span', { class: 'ua-visually-hidden' }, '展开详情')) : null, // 展开列在最前
selectable ? h('th', { class: 'ua-cell-check', scope: 'col' }, headCheck) : null, // 全选框借这次归位进 th(原先直接塞进 tr,属无效结构)
];
renderList(thead, () => [h('tr', null, ...lead(), ...shown().map(th))]);
return h(
'section',