fix
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { request } from "@/api/request"
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage"
|
||||
|
||||
// ============ 通用响应类型 ============
|
||||
|
||||
@@ -17,7 +18,6 @@ export enum ReportType {
|
||||
FAULT = 'fault',
|
||||
SERVER = 'server',
|
||||
NETWORK_DEVICE = 'network_device',
|
||||
HISTORY = 'history',
|
||||
}
|
||||
|
||||
export enum ReportStatus {
|
||||
@@ -31,7 +31,8 @@ export enum ReportStatus {
|
||||
|
||||
export interface ReportRecord {
|
||||
id: number
|
||||
report_type: ReportType
|
||||
/** 列表接口可能含已下线类型字符串,仅六类可再次生成 */
|
||||
report_type: ReportType | string
|
||||
title: string
|
||||
description?: string
|
||||
status: ReportStatus
|
||||
@@ -65,7 +66,11 @@ export interface PageResult<T> {
|
||||
// ============ 报表生成参数接口 ============
|
||||
|
||||
export interface TrafficReportParams {
|
||||
topology_id: number
|
||||
/** topology:拓扑/NetFlow;snmp_devices:多设备 SNMP 接口流量汇总 */
|
||||
traffic_mode?: 'topology' | 'snmp_devices'
|
||||
/** traffic_mode=snmp_devices 时必填,service_identity 列表 */
|
||||
service_identities?: string[]
|
||||
topology_id?: number
|
||||
link_id?: number
|
||||
node_id?: string
|
||||
granularity?: 'minute' | 'hour' | 'day' | 'month'
|
||||
@@ -102,9 +107,21 @@ export interface ServerReportParams {
|
||||
include_daily_alerts?: boolean
|
||||
}
|
||||
|
||||
/** POST /reports/generate report_type=network_device */
|
||||
export interface NetworkDeviceReportParams {
|
||||
network_device_service_ids?: number[]
|
||||
service_identities?: string[]
|
||||
start_time: string
|
||||
end_time: string
|
||||
columns?: string[]
|
||||
include_daily_alerts?: boolean
|
||||
}
|
||||
|
||||
export interface StatisticsReportParams {
|
||||
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware'
|
||||
metric_name: string
|
||||
/** 与 metric_name 二选一;网络等指标优先用 metric_id */
|
||||
metric_id?: string
|
||||
metric_name?: string
|
||||
target_identities: string[]
|
||||
start_time: string
|
||||
end_time: string
|
||||
@@ -127,7 +144,8 @@ export interface HistoryReportParams {
|
||||
|
||||
export interface TopNReportParams {
|
||||
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware'
|
||||
metric_name: string
|
||||
metric_id?: string
|
||||
metric_name?: string
|
||||
target_identities: string[]
|
||||
start_time: string
|
||||
end_time: string
|
||||
@@ -142,7 +160,15 @@ export interface GenerateReportParams {
|
||||
report_type: ReportType
|
||||
title?: string
|
||||
description?: string
|
||||
params: TrafficReportParams | FaultReportParams | ServerReportParams | StatisticsReportParams | HistoryReportParams | TopNReportParams | Record<string, any>
|
||||
params:
|
||||
| TrafficReportParams
|
||||
| FaultReportParams
|
||||
| ServerReportParams
|
||||
| NetworkDeviceReportParams
|
||||
| StatisticsReportParams
|
||||
| HistoryReportParams
|
||||
| TopNReportParams
|
||||
| Record<string, any>
|
||||
}
|
||||
|
||||
// ============ 报表生成接口(新版) ============
|
||||
@@ -155,20 +181,91 @@ export const fetchReportList = (params: ReportListParams) =>
|
||||
export const fetchReportDetail = (id: number) =>
|
||||
request.get<ApiResponse<ReportRecord>>(`/DC-Control/v1/reports/${id}`)
|
||||
|
||||
/** 生成报表 */
|
||||
/** 同步生成报表(topn / statistics / traffic / fault / server / network_device) */
|
||||
export const generateReport = (data: GenerateReportParams) =>
|
||||
request.post<ApiResponse<ReportRecord>>('/DC-Control/v1/reports/generate', data)
|
||||
|
||||
/** 异步生成报表任务 */
|
||||
export const createReportAsyncJob = (data: GenerateReportParams) =>
|
||||
request.post<ApiResponse<ReportRecord>>('/DC-Control/v1/reports/jobs', data)
|
||||
|
||||
/** 指标发现(时间窗内有样本的 metric_name) */
|
||||
export const fetchReportMetricsAvailable = (params: {
|
||||
data_source: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
identities?: string
|
||||
keyword?: string
|
||||
limit?: number
|
||||
}) => 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 }) =>
|
||||
request.get<ApiResponse<{ data_source: string; metrics: any[] }>>('/DC-Control/v1/reports/metrics/registry', { params })
|
||||
|
||||
/** 查看报表内容 */
|
||||
export const fetchReportContent = (id: number) =>
|
||||
request.get<ApiResponse<Record<string, any>>>(`/DC-Control/v1/reports/${id}/content`)
|
||||
|
||||
/** 导出报表 */
|
||||
export const exportReport = (id: number, format: 'csv' | 'xlsx' = 'csv') =>
|
||||
request.get<Blob>(`/DC-Control/v1/reports/${id}/export`, {
|
||||
params: { format },
|
||||
responseType: 'blob',
|
||||
})
|
||||
/** 原始 ArrayBuffer → 下载用 Blob;xlsx 校验 ZIP 魔数 PK */
|
||||
function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType: string | null): Blob {
|
||||
const u8 = new Uint8Array(ab)
|
||||
if (format === 'xlsx') {
|
||||
const okZip = u8.length >= 4 && u8[0] === 0x50 && u8[1] === 0x4b
|
||||
if (!okZip) {
|
||||
const head = new TextDecoder('utf-8', { fatal: false }).decode(u8.slice(0, 4096)).trim()
|
||||
let detail = '服务器返回的不是有效的 xlsx(应为 ZIP,文件头 PK)。'
|
||||
if (head.startsWith('{') || head.startsWith('[')) {
|
||||
try {
|
||||
const j = JSON.parse(head) as { message?: string }
|
||||
if (j.message) detail = j.message
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} else if (head.startsWith('<')) {
|
||||
detail = '服务器返回了 HTML 而非文件,请检查网关、反向代理或登录态。'
|
||||
}
|
||||
throw new Error(`导出失败:${detail}`)
|
||||
}
|
||||
const mime =
|
||||
contentType && /spreadsheet|zip|octet-stream/i.test(contentType)
|
||||
? 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'
|
||||
return new Blob([ab], { type: mime })
|
||||
}
|
||||
|
||||
/**
|
||||
* 报表文件导出:使用 fetch + arrayBuffer,绕过 axios 拦截器与 Blob 中间态,
|
||||
* 避免网络面板里已是合法 xlsx(PK…)但落盘文件损坏的情况。
|
||||
*/
|
||||
export const exportReport = async (id: number, format: 'csv' | 'xlsx' = 'csv'): Promise<Blob> => {
|
||||
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)
|
||||
const headers: Record<string, string> = {
|
||||
Workspace: String(import.meta.env.VITE_APP_WORKSPACE || ''),
|
||||
}
|
||||
if (token) headers.Authorization = String(token)
|
||||
|
||||
const r = await fetch(url, { method: 'GET', headers })
|
||||
const ab = await r.arrayBuffer()
|
||||
if (!r.ok) {
|
||||
const head = new TextDecoder('utf-8', { fatal: false }).decode(new Uint8Array(ab).slice(0, 4096))
|
||||
let msg = `导出失败 (HTTP ${r.status})`
|
||||
try {
|
||||
const j = JSON.parse(head.trim()) as { message?: string }
|
||||
if (j.message) msg = j.message
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
return exportBufferToBlob(ab, format, r.headers.get('content-type'))
|
||||
}
|
||||
|
||||
// ============ 监测指标类接口(旧版兼容) ============
|
||||
|
||||
|
||||
@@ -35,8 +35,28 @@ import axios, {
|
||||
// 3. 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const cfg = response.config as RequestConfig
|
||||
if (cfg.rawResponse) {
|
||||
return response
|
||||
}
|
||||
// 二进制流:只返回 data,且勿对 Blob 访问 .status
|
||||
if (cfg.responseType === 'blob' || cfg.responseType === 'arraybuffer') {
|
||||
const body = response.data as unknown
|
||||
if (body instanceof Blob) return body
|
||||
if (typeof body === 'string') {
|
||||
return new Blob([body], {
|
||||
type: (response.headers['content-type'] as string) || 'application/octet-stream',
|
||||
})
|
||||
}
|
||||
if (body instanceof ArrayBuffer) {
|
||||
return new Blob([body], {
|
||||
type: (response.headers['content-type'] as string) || 'application/octet-stream',
|
||||
})
|
||||
}
|
||||
return body
|
||||
}
|
||||
// 统一处理响应数据格式[2](@ref)
|
||||
if (response.data.status === 401) {
|
||||
if (response.data?.status === 401) {
|
||||
// token过期处理
|
||||
SafeStorage.clearAppStorage();
|
||||
window.location.href = "/auth/login";
|
||||
@@ -59,6 +79,8 @@ import axios, {
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
data?: unknown;
|
||||
needWorkspace?: boolean;
|
||||
/** 为 true 时响应拦截器返回完整 AxiosResponse(用于 blob 等需自行取 data 的场景) */
|
||||
rawResponse?: boolean;
|
||||
}
|
||||
|
||||
export const request = {
|
||||
|
||||
Reference in New Issue
Block a user