iter(g88): 数据表支持导出 CSV

This commit is contained in:
HIsheR
2026-10-26 13:08:25 +08:00
parent fa07101f76
commit 9a7c46a3c6
12 changed files with 110 additions and 8 deletions
+39
View File
@@ -0,0 +1,39 @@
import { h } from '@ui-arch/frame';
import type { RowModel } from '@ui-arch/table-core';
import { button } from './button.js';
import type { TableColumn } from './types.js';
/*** 单元格转义:含逗号 / 引号 / 换行时整体加引号,内部引号翻倍 ***/
function cell(v: unknown): string {
const s = v == null ? '' : String(v);
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
/*** 导出 CSV:前置 BOM + CRLF 换行,Excel 打开中文不乱码 ***/
export function downloadCsv(name: string, header: string[], rows: unknown[][]): void {
const text = '\ufeff' + [header, ...rows].map((r) => r.map(cell).join(',')).join('\r\n') + '\r\n';
const url = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' }));
const a = h('a', { href: url, download: name }) as HTMLAnchorElement;
document.body.appendChild(a); // 挂进文档再点击:Safari 对游离锚点不触发下载
a.click();
a.remove(); // 立即摘除,不留残留节点
URL.revokeObjectURL(url);
}
/*** 导出按钮:取筛选 + 排序后的全量行(不分页),列序与表头一致、单元格走 format ***/
export function exportButton<T>(
table: { getAllRows(): RowModel<T>; getCellValue(row: T, key: string): unknown },
columns: TableColumn<T>[],
name: string,
): HTMLElement {
const run = (): void => {
const rows = table.getAllRows().rows;
const header = columns.map((c) => String(c.header ?? c.key));
const body = rows.map((row) => columns.map((c) => {
const raw = table.getCellValue(row, c.key);
return c.format ? c.format(raw, row) : raw; // 与单元格显示同源,导出内容与看到的一致
}));
downloadCsv(name, header, body);
};
return button({ label: '导出 CSV', variant: 'ghost', onClick: run });
}
+2
View File
@@ -8,6 +8,7 @@ import {
import type { PageInfo, RowKey, TableInstance } from '@ui-arch/table-core';
import { computed, effect, h, renderList, signal } from '@ui-arch/frame';
import { attachResize } from './columnResize.js';
import { exportButton } from './csv.js';
import { button } from './button.js';
import { skeleton } from './skeleton.js';
import { textField } from './textField.js';
@@ -184,6 +185,7 @@ 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, // 导出筛选+排序后的全量行
),
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(
+2
View File
@@ -63,6 +63,8 @@ export interface DataTableProps<T> {
page?: Signal<number>; // 受控页码信号:与内核分页双向同步,可接独立 pagination 组件联动
loading?: boolean | Signal<boolean>; // 加载态:为真时数据行换成骨架行,筛选与翻页禁用
selected?: Signal<RowKey[]>; // 受控选中行信号:与内核选择双向同步,供批量操作读取
exportable?: boolean; // 工具栏显示导出按钮:导出筛选 + 排序后的全量行(不分页);缺省 false
exportName?: string; // 导出文件名;缺省 table.csv
}
/*** 选项条目:select / radioGroup / tabs / segmented 共用的选项形状 ***/