diff --git a/src/api/ops/report.ts b/src/api/ops/report.ts index e82f748..d005eff 100644 --- a/src/api/ops/report.ts +++ b/src/api/ops/report.ts @@ -64,6 +64,13 @@ export interface PageResult { 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>>(`/DC-Control/v1/reports/${id}/content`) -/** 原始 ArrayBuffer → 下载用 Blob;xlsx 校验 ZIP 魔数 PK */ -function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType: string | null): Blob { +/** 原始 ArrayBuffer → 下载用 Blob;xlsx 校验 ZIP 魔数 PK,PDF 校验 %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 中间态, * 避免网络面板里已是合法 xlsx(PK…)但落盘文件损坏的情况。 */ -export const exportReport = async (id: number, format: 'csv' | 'xlsx' = 'csv'): Promise => { +export const exportReport = async ( + id: number, + format: ReportExportFormat = 'csv', +): Promise => { 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}`, + } } // ============ 监测指标类接口(旧版兼容) ============ diff --git a/src/views/ops/pages/report/device/index.vue b/src/views/ops/pages/report/device/index.vue index e7c1f6c..130c0e2 100644 --- a/src/views/ops/pages/report/device/index.vue +++ b/src/views/ops/pages/report/device/index.vue @@ -53,6 +53,7 @@ @@ -70,6 +71,7 @@ 查看 CSV Excel + PDF @@ -211,12 +213,13 @@ import { fetchReportList, generateReport, fetchReportContent, - exportReport, ReportType, type ReportRecord, type NetworkDeviceReportParams, } from '@/api/ops/report' import * as echarts from 'echarts' +import { downloadReportFile } from '../useReportExport' +import type { ReportExportFormat } from '@/api/ops/report' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' import { useReportNetworkDevicePickOptions } from '../useReportNetworkDevicePickOptions' @@ -560,7 +563,7 @@ const handleViewContent = async (record?: ReportRecord) => { } // 导出报表 -const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { +const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => { const targetRecord = record || selectedRecord.value if (!targetRecord) { Message.warning('请选择要导出的报表') @@ -575,18 +578,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { exporting.value = true try { - const blob = await exportReport(targetRecord.id, format) - - // 创建下载链接 - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `report_${targetRecord.id}.${format}` - - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(url) + await downloadReportFile(targetRecord, format) Message.success('导出成功') } catch (error: any) { diff --git a/src/views/ops/pages/report/fault/index.vue b/src/views/ops/pages/report/fault/index.vue index 25200fb..e1d2539 100644 --- a/src/views/ops/pages/report/fault/index.vue +++ b/src/views/ops/pages/report/fault/index.vue @@ -57,6 +57,7 @@ @@ -74,6 +75,7 @@ 查看 CSV Excel + PDF @@ -235,13 +237,14 @@ import { fetchReportList, generateReport, fetchReportContent, - exportReport, exportEvidenceReport, ReportType, type ReportRecord, type FaultReportParams, } from '@/api/ops/report' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' +import { downloadReportFile } from '../useReportExport' +import type { ReportExportFormat } from '@/api/ops/report' import { useFaultReportServiceIdentityOptions } from '../useReportTargetIdentityOptions' const { @@ -619,7 +622,7 @@ const handleViewContent = async (record?: ReportRecord) => { } // 导出报表 -const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { +const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => { const targetRecord = record || selectedRecord.value if (!targetRecord) { Message.warning('请选择要导出的报表') @@ -634,18 +637,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { exporting.value = true try { - const blob = await exportReport(targetRecord.id, format) - - // 创建下载链接 - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `report_${targetRecord.id}.${format}` - - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(url) + await downloadReportFile(targetRecord, format) Message.success('导出成功') } catch (error: any) { diff --git a/src/views/ops/pages/report/host/index.vue b/src/views/ops/pages/report/host/index.vue index 15bd6b1..99e25f6 100644 --- a/src/views/ops/pages/report/host/index.vue +++ b/src/views/ops/pages/report/host/index.vue @@ -53,6 +53,7 @@ @@ -70,6 +71,7 @@ 查看 CSV Excel + PDF @@ -211,12 +213,13 @@ import { fetchReportList, generateReport, fetchReportContent, - exportReport, ReportType, type ReportRecord, type ServerReportParams, } from '@/api/ops/report' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' +import { downloadReportFile } from '../useReportExport' +import type { ReportExportFormat } from '@/api/ops/report' import { useReportServerPickOptions } from '../useReportServerPickOptions' const { serverIdentityOptions, serverOptionsLoading, loadServerPickOptions } = useReportServerPickOptions() @@ -548,7 +551,7 @@ const handleViewContent = async (record?: ReportRecord) => { } // 导出报表 -const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { +const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => { const targetRecord = record || selectedRecord.value if (!targetRecord) { Message.warning('请选择要导出的报表') @@ -563,18 +566,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { exporting.value = true try { - const blob = await exportReport(targetRecord.id, format) - - // 创建下载链接 - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `report_${targetRecord.id}.${format}` - - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(url) + await downloadReportFile(targetRecord, format) Message.success('导出成功') } catch (error: any) { diff --git a/src/views/ops/pages/report/statistics/index.vue b/src/views/ops/pages/report/statistics/index.vue index 245ebd1..cae24da 100644 --- a/src/views/ops/pages/report/statistics/index.vue +++ b/src/views/ops/pages/report/statistics/index.vue @@ -53,6 +53,7 @@ @@ -70,6 +71,7 @@ 查看 CSV Excel + PDF @@ -248,12 +250,13 @@ import { fetchReportList, generateReport, fetchReportContent, - exportReport, ReportType, type ReportRecord, type StatisticsReportParams, } from '@/api/ops/report' import * as echarts from 'echarts' +import { downloadReportFile } from '../useReportExport' +import type { ReportExportFormat } from '@/api/ops/report' import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions' import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' @@ -634,7 +637,7 @@ const handleViewContent = async (record?: ReportRecord) => { } // 导出报表 -const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { +const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => { const targetRecord = record || selectedRecord.value if (!targetRecord) { Message.warning('请选择要导出的报表') @@ -649,18 +652,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { exporting.value = true try { - const blob = await exportReport(targetRecord.id, format) - - // 创建下载链接 - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `report_${targetRecord.id}.${format}` - - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(url) + await downloadReportFile(targetRecord, format) Message.success('导出成功') } catch (error: any) { diff --git a/src/views/ops/pages/report/topn/index.vue b/src/views/ops/pages/report/topn/index.vue index f9ecc53..4a32beb 100644 --- a/src/views/ops/pages/report/topn/index.vue +++ b/src/views/ops/pages/report/topn/index.vue @@ -53,6 +53,7 @@ @@ -70,6 +71,7 @@ 查看 CSV Excel + PDF @@ -238,12 +240,13 @@ import { fetchReportList, generateReport, fetchReportContent, - exportReport, ReportType, type ReportRecord, type TopNReportParams, } from '@/api/ops/report' import * as echarts from 'echarts' +import { downloadReportFile } from '../useReportExport' +import type { ReportExportFormat } from '@/api/ops/report' import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions' import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' @@ -639,7 +642,7 @@ const handleViewContent = async (record?: ReportRecord) => { } // 导出报表 -const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { +const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => { const targetRecord = record || selectedRecord.value if (!targetRecord) { Message.warning('请选择要导出的报表') @@ -654,18 +657,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { exporting.value = true try { - const blob = await exportReport(targetRecord.id, format) - - // 创建下载链接 - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `report_${targetRecord.id}.${format}` - - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(url) + await downloadReportFile(targetRecord, format) Message.success('导出成功') } catch (error: any) { diff --git a/src/views/ops/pages/report/traffic/index.vue b/src/views/ops/pages/report/traffic/index.vue index 5587e3e..6f9db49 100644 --- a/src/views/ops/pages/report/traffic/index.vue +++ b/src/views/ops/pages/report/traffic/index.vue @@ -53,6 +53,7 @@ @@ -70,6 +71,7 @@ 查看 CSV Excel + PDF @@ -323,12 +325,13 @@ import { fetchReportList, generateReport, fetchReportContent, - exportReport, ReportType, type ReportRecord, type TrafficReportParams, } from '@/api/ops/report' import * as echarts from 'echarts' +import { downloadReportFile } from '../useReportExport' +import type { ReportExportFormat } from '@/api/ops/report' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' import { useReportTopologyOptions } from '../useReportTopologyOptions' import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions' @@ -815,7 +818,7 @@ const handleViewContent = async (record?: ReportRecord) => { } // 导出报表 -const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { +const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => { const targetRecord = record || selectedRecord.value if (!targetRecord) { Message.warning('请选择要导出的报表') @@ -830,18 +833,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => { exporting.value = true try { - const blob = await exportReport(targetRecord.id, format) - - // 创建下载链接 - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `report_${targetRecord.id}.${format}` - - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(url) + await downloadReportFile(targetRecord, format) Message.success('导出成功') } catch (error: any) { diff --git a/src/views/ops/pages/report/useReportExport.ts b/src/views/ops/pages/report/useReportExport.ts new file mode 100644 index 0000000..bc6c934 --- /dev/null +++ b/src/views/ops/pages/report/useReportExport.ts @@ -0,0 +1,16 @@ +import { exportReport, type ReportExportFormat, type ReportRecord } from '@/api/ops/report' + +export async function downloadReportFile(record: ReportRecord, format: ReportExportFormat): Promise { + const { blob, fileName } = await exportReport(record.id, format) + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + try { + link.href = url + link.download = fileName + document.body.appendChild(link) + link.click() + } finally { + link.remove() + window.URL.revokeObjectURL(url) + } +}