68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
/**
|
||
* CSV 生成与浏览器下载。
|
||
*
|
||
* 抽成公共实现的直接原因:签署记录查询与报表分析都要「按当前条件导出全量」,
|
||
* 两边各写一份 CSV 转义迟早会分叉 —— 一边处理了换行一边没处理,
|
||
* 导出的文件里就会多出一行被 Excel 解析成错位的数据,而且很难一眼看出来。
|
||
*
|
||
* ⚠️ 前端导出是**临时手段**:导出行为、导出人、导出范围都该写入审计日志,
|
||
* 浏览器里做不到。后端提供导出任务接口后,这里应整体替换为服务端导出。
|
||
*/
|
||
|
||
export interface CsvColumn<T> {
|
||
label: string
|
||
value: (row: T) => string
|
||
}
|
||
|
||
/** 含逗号、引号或换行的字段必须整体加引号,内部引号翻倍 */
|
||
export function escapeCsvField(value: string): string {
|
||
return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value
|
||
}
|
||
|
||
/** 文件名时间戳:`YYYYMMDDHHmm` */
|
||
export function fileStamp(date = new Date()): string {
|
||
const pad = (value: number) => String(value).padStart(2, '0')
|
||
|
||
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}`
|
||
}
|
||
|
||
/**
|
||
* 触发浏览器下载。
|
||
*
|
||
* 用 `setTimeout(revoke, 0)` 而不是立刻 revoke:立刻释放会让部分浏览器
|
||
* 来不及读取 blob 就中断下载。
|
||
*/
|
||
export function saveBlob(blob: Blob, fileName: string): void {
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
|
||
link.href = url
|
||
link.download = fileName
|
||
link.click()
|
||
window.setTimeout(() => URL.revokeObjectURL(url), 0)
|
||
}
|
||
|
||
/** 按列定义把行数据拼成 CSV 文本(含表头,行分隔用 CRLF) */
|
||
export function toCsv<T>(columns: readonly CsvColumn<T>[], rows: readonly T[]): string {
|
||
return [
|
||
columns.map((column) => escapeCsvField(column.label)).join(','),
|
||
...rows.map((row) => columns.map((column) => escapeCsvField(column.value(row))).join(',')),
|
||
].join('\r\n')
|
||
}
|
||
|
||
/**
|
||
* 生成并下载 CSV。
|
||
*
|
||
* 带 BOM:Excel 直接打开 UTF-8 CSV 时,没有 BOM 会把中文显示成乱码。
|
||
*/
|
||
export function downloadCsv<T>(
|
||
columns: readonly CsvColumn<T>[],
|
||
rows: readonly T[],
|
||
fileName: string,
|
||
): void {
|
||
saveBlob(
|
||
new Blob([`\uFEFF${toCsv(columns, rows)}`], { type: 'text/csv;charset=utf-8' }),
|
||
fileName,
|
||
)
|
||
}
|