feat(报表分析): 新增签署总览等四个报表菜单并拆分演示数据开关

This commit is contained in:
yelan
2026-09-24 21:03:39 +08:00
parent abe7e17582
commit 78402859fd
26 changed files with 3618 additions and 193 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* 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,
)
}