Merge branch 'feature/report-pdf'
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { request } from "@/api/request"
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage"
|
||||
import { request } from '@/api/request'
|
||||
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
||||
|
||||
// ============ 通用响应类型 ============
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface ReportRecord {
|
||||
file_path?: string
|
||||
file_size?: number
|
||||
file_mime?: string
|
||||
started_at?: string
|
||||
finished_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -64,6 +66,13 @@ export interface PageResult<T> {
|
||||
data: T[]
|
||||
}
|
||||
|
||||
export type ReportExportFormat = 'csv' | 'xlsx' | 'pdf'
|
||||
|
||||
export interface DownloadedReport {
|
||||
blob: Blob
|
||||
fileName: string
|
||||
}
|
||||
|
||||
// ============ 报表生成参数接口 ============
|
||||
|
||||
export interface TrafficReportParams {
|
||||
@@ -127,9 +136,7 @@ export interface NetworkDeviceReportParams {
|
||||
|
||||
export interface StatisticsReportParams {
|
||||
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware'
|
||||
/** 与 metric_name 二选一;网络等指标优先用 metric_id */
|
||||
metric_id?: string
|
||||
metric_name?: string
|
||||
metric_id: string
|
||||
target_identities: string[]
|
||||
start_time: string
|
||||
end_time: string
|
||||
@@ -152,16 +159,13 @@ export interface HistoryReportParams {
|
||||
|
||||
export interface TopNReportParams {
|
||||
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware' | 'alert'
|
||||
metric_id?: string
|
||||
metric_name?: string
|
||||
metric_id: string
|
||||
target_identities: string[]
|
||||
start_time: string
|
||||
end_time: string
|
||||
n?: number
|
||||
rank_aggregate?: 'avg' | 'max' | 'min' | 'last'
|
||||
order?: 'desc' | 'asc'
|
||||
metric_type?: 'cpu' | 'disk' | 'io' | 'memory' | 'network'
|
||||
collector_identity?: string
|
||||
}
|
||||
|
||||
/** 证据导出事件行 */
|
||||
@@ -230,7 +234,8 @@ export const fetchReportMetricsAvailable = (params: {
|
||||
identities?: string
|
||||
keyword?: string
|
||||
limit?: number
|
||||
}) => request.get<ApiResponse<{ data_source: string; items: any[]; registry: any[] }>>('/DC-Control/v1/reports/metrics/available', { params })
|
||||
}) =>
|
||||
request.get<ApiResponse<{ data_source: string; items: any[]; registry: any[] }>>('/DC-Control/v1/reports/metrics/available', { params })
|
||||
|
||||
/** 逻辑指标目录(Registry) */
|
||||
export const fetchReportMetricsRegistry = (params: { data_source: string }) =>
|
||||
@@ -239,8 +244,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 → 下载用 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 +270,62 @@ function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType
|
||||
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
return new Blob([ab], { type: mime })
|
||||
}
|
||||
const mime =
|
||||
contentType && /csv|text|plain/i.test(contentType) ? contentType : 'text/csv;charset=utf-8'
|
||||
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) {
|
||||
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-\u009F\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…)但落盘文件损坏的情况。
|
||||
*/
|
||||
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 +347,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: normalizeReportFileName(parseContentDispositionFileName(r.headers.get('content-disposition')), id, format),
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 监测指标类接口(旧版兼容) ============
|
||||
|
||||
Reference in New Issue
Block a user