feat: 增加报表 PDF 下载

This commit is contained in:
zxr
2026-08-03 21:13:07 +08:00
parent 7353c4eede
commit 195aa7ccff
8 changed files with 92 additions and 88 deletions

View File

@@ -64,6 +64,13 @@ export interface PageResult<T> {
data: T[]
}
export type ReportExportFormat = 'csv' | 'xlsx' | 'pdf'
export interface DownloadedReport {
blob: Blob
fileName: string
}
// ============ 报表生成参数接口 ============
export interface TrafficReportParams {
@@ -239,8 +246,8 @@ export const fetchReportMetricsRegistry = (params: { data_source: string }) =>
/** 查看报表内容 */
export const fetchReportContent = (id: number) => request.get<ApiResponse<Record<string, any>>>(`/DC-Control/v1/reports/${id}/content`)
/** 原始 ArrayBuffer → 下载用 Blobxlsx 校验 ZIP 魔数 PK */
function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType: string | null): Blob {
/** 原始 ArrayBuffer → 下载用 Blobxlsx 校验 ZIP 魔数 PKPDF 校验 %PDF- */
function exportBufferToBlob(ab: ArrayBuffer, format: ReportExportFormat, contentType: string | null): Blob {
const u8 = new Uint8Array(ab)
if (format === 'xlsx') {
const okZip = u8.length >= 4 && u8[0] === 0x50 && u8[1] === 0x4b
@@ -265,16 +272,42 @@ function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
return new Blob([ab], { type: mime })
}
if (format === 'pdf') {
const okPdf =
u8.length >= 5 &&
u8[0] === 0x25 &&
u8[1] === 0x50 &&
u8[2] === 0x44 &&
u8[3] === 0x46 &&
u8[4] === 0x2d
if (!okPdf) {
throw new Error('导出失败:服务器返回的不是有效的 PDF。')
}
const mime = contentType && /pdf/i.test(contentType) ? contentType : 'application/pdf'
return new Blob([ab], { type: mime })
}
const mime =
contentType && /csv|text|plain/i.test(contentType) ? contentType : 'text/csv;charset=utf-8'
return new Blob([ab], { type: mime })
}
function parseContentDispositionFileName(value: string | null): string | undefined {
if (!value) return undefined
const utf8FileName = value.match(/filename\*=UTF-8''([^;]+)/i)
if (utf8FileName) return decodeURIComponent(utf8FileName[1])
return value.match(/filename="([^"]+)"/i)?.[1]
}
/**
* 报表文件导出:使用 fetch + arrayBuffer绕过 axios 拦截器与 Blob 中间态,
* 避免网络面板里已是合法 xlsxPK…但落盘文件损坏的情况。
*/
export const exportReport = async (id: number, format: 'csv' | 'xlsx' = 'csv'): Promise<Blob> => {
export const exportReport = async (
id: number,
format: ReportExportFormat = 'csv',
): Promise<DownloadedReport> => {
const base = String(import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '')
const url = `${base}/DC-Control/v1/reports/${id}/export?format=${encodeURIComponent(format)}`
const token = SafeStorage.get(AppStorageKey.TOKEN)
@@ -296,7 +329,10 @@ export const exportReport = async (id: number, format: 'csv' | 'xlsx' = 'csv'):
}
throw new Error(msg)
}
return exportBufferToBlob(ab, format, r.headers.get('content-type'))
return {
blob: exportBufferToBlob(ab, format, r.headers.get('content-type')),
fileName: parseContentDispositionFileName(r.headers.get('content-disposition')) || `report_${id}.${format}`,
}
}
// ============ 监测指标类接口(旧版兼容) ============