This commit is contained in:
zxr
2026-05-02 17:08:10 +08:00
parent ea3e60c17c
commit 27e1f335a6
20 changed files with 1401 additions and 1050 deletions

View File

@@ -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:拓扑/NetFlowsnmp_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 → 下载用 Blobxlsx 校验 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 中间态,
* 避免网络面板里已是合法 xlsxPK…但落盘文件损坏的情况。
*/
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'))
}
// ============ 监测指标类接口(旧版兼容) ============

View File

@@ -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 = {