diff --git a/src/api/ops/report.ts b/src/api/ops/report.ts index d005eff..fb0874e 100644 --- a/src/api/ops/report.ts +++ b/src/api/ops/report.ts @@ -295,11 +295,45 @@ function parseContentDispositionFileName(value: string | null): string | undefin if (!value) return undefined const utf8FileName = value.match(/filename\*=UTF-8''([^;]+)/i) - if (utf8FileName) return decodeURIComponent(utf8FileName[1]) + if (utf8FileName) { + try { + return decodeURIComponent(utf8FileName[1]) + } catch { + // 编码异常时继续读取普通文件名 + } + } return value.match(/filename="([^"]+)"/i)?.[1] } +function normalizeReportFileName( + fileName: string | undefined, + id: number, + format: ReportExportFormat, +): string { + const defaultName = `report_${id}.${format}` + if (!fileName) return defaultName + + let name = fileName.split(/[\\/]/).pop() || '' + name = name + .replace(/[\u0000-\u001F\u007F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '') + .replace(/[<>:"/\\|?*]/g, '_') + .replace(/[ .]+$/, '') + + const extensionIndex = name.lastIndexOf('.') + if (extensionIndex > 0) { + name = name.slice(0, extensionIndex).replace(/[ .]+$/, '') + } + if (!name) return defaultName + + const reservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i + if (reservedName.test(name.split('.')[0])) { + name = `_${name}` + } + + return `${name}.${format}` +} + /** * 报表文件导出:使用 fetch + arrayBuffer,绕过 axios 拦截器与 Blob 中间态, * 避免网络面板里已是合法 xlsx(PK…)但落盘文件损坏的情况。 @@ -331,7 +365,11 @@ export const exportReport = async ( } return { blob: exportBufferToBlob(ab, format, r.headers.get('content-type')), - fileName: parseContentDispositionFileName(r.headers.get('content-disposition')) || `report_${id}.${format}`, + fileName: normalizeReportFileName( + parseContentDispositionFileName(r.headers.get('content-disposition')), + id, + format, + ), } }