feat(报表分析): 新增签署总览等四个报表菜单并拆分演示数据开关
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -43,6 +43,17 @@ export const mockFlags = {
|
||||
documentPermissions: resolveMockFlag(import.meta.env.VITE_MOCK_DOCUMENT_PERMISSIONS, false),
|
||||
/** 报表分析 */
|
||||
reports: resolveMockFlag(import.meta.env.VITE_MOCK_REPORTS, false),
|
||||
/**
|
||||
* 报表分析(签署总览 / 签署量分析 / 签署时效分析 / 患者结构分析)。
|
||||
*
|
||||
* 与上面的 `reports` 分开:`reports` 管的是「系统管理 → 报表分析」那个签署报表
|
||||
* 与导出任务页(走 reports/signing/* 接口),这里是「报表分析」目录下另外四个
|
||||
* 独立菜单。两者菜单不同、后端接口也不同,共用一个开关会让其中一个先落地时
|
||||
* 被另一个的缺省值带走。
|
||||
*
|
||||
* 缺省开着:后端目前没有任何分析聚合接口,真实模式只会得到空态。
|
||||
*/
|
||||
reportAnalytics: resolveMockFlag(import.meta.env.VITE_MOCK_REPORT_ANALYTICS, true),
|
||||
/** 用户与权限:用户、角色、接口权限目录 */
|
||||
users: resolveMockFlag(import.meta.env.VITE_MOCK_USERS, false),
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user