From 27e1f335a60c28e7de68f61d7e287e2de8016e44 Mon Sep 17 00:00:00 2001 From: zxr <271055687@qq.com> Date: Sat, 2 May 2026 17:08:10 +0800 Subject: [PATCH] fix --- src/api/ops/report.ts | 123 ++- src/api/request.ts | 24 +- src/router/local-menu-flat.ts | 15 - src/router/local-menu-items.ts | 16 - src/views/help/index.vue | 9 +- .../ops/pages/dc/device-collect/index.vue | 20 +- .../ops/pages/netarch/auto-topology/index.vue | 2 +- src/views/ops/pages/report/device/index.vue | 293 +++----- src/views/ops/pages/report/fault/index.vue | 52 +- src/views/ops/pages/report/history/index.vue | 701 +----------------- src/views/ops/pages/report/host/index.vue | 125 ++-- .../ops/pages/report/statistics/index.vue | 74 +- src/views/ops/pages/report/topn/index.vue | 106 ++- src/views/ops/pages/report/traffic/index.vue | 290 +++++++- .../ops/pages/report/useReportListRow.ts | 38 + .../report/useReportMetricRegistryOptions.ts | 47 ++ .../useReportNetworkDevicePickOptions.ts | 75 ++ .../report/useReportServerPickOptions.ts | 75 ++ .../report/useReportTargetIdentityOptions.ts | 245 ++++++ .../pages/report/useReportTopologyOptions.ts | 121 +++ 20 files changed, 1401 insertions(+), 1050 deletions(-) create mode 100644 src/views/ops/pages/report/useReportListRow.ts create mode 100644 src/views/ops/pages/report/useReportMetricRegistryOptions.ts create mode 100644 src/views/ops/pages/report/useReportNetworkDevicePickOptions.ts create mode 100644 src/views/ops/pages/report/useReportServerPickOptions.ts create mode 100644 src/views/ops/pages/report/useReportTargetIdentityOptions.ts create mode 100644 src/views/ops/pages/report/useReportTopologyOptions.ts diff --git a/src/api/ops/report.ts b/src/api/ops/report.ts index 704a72d..c5c7f52 100644 --- a/src/api/ops/report.ts +++ b/src/api/ops/report.ts @@ -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 { // ============ 报表生成参数接口 ============ 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 + params: + | TrafficReportParams + | FaultReportParams + | ServerReportParams + | NetworkDeviceReportParams + | StatisticsReportParams + | HistoryReportParams + | TopNReportParams + | Record } // ============ 报表生成接口(新版) ============ @@ -155,20 +181,91 @@ export const fetchReportList = (params: ReportListParams) => export const fetchReportDetail = (id: number) => request.get>(`/DC-Control/v1/reports/${id}`) -/** 生成报表 */ +/** 同步生成报表(topn / statistics / traffic / fault / server / network_device) */ export const generateReport = (data: GenerateReportParams) => request.post>('/DC-Control/v1/reports/generate', data) +/** 异步生成报表任务 */ +export const createReportAsyncJob = (data: GenerateReportParams) => + request.post>('/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>('/DC-Control/v1/reports/metrics/available', { params }) + +/** 逻辑指标目录(Registry) */ +export const fetchReportMetricsRegistry = (params: { data_source: string }) => + request.get>('/DC-Control/v1/reports/metrics/registry', { params }) + /** 查看报表内容 */ export const fetchReportContent = (id: number) => request.get>>(`/DC-Control/v1/reports/${id}/content`) -/** 导出报表 */ -export const exportReport = (id: number, format: 'csv' | 'xlsx' = 'csv') => - request.get(`/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 => { + 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 = { + 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')) +} // ============ 监测指标类接口(旧版兼容) ============ diff --git a/src/api/request.ts b/src/api/request.ts index 9acefc5..76d322d 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -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 = { diff --git a/src/router/local-menu-flat.ts b/src/router/local-menu-flat.ts index a42f2a9..73cd8d6 100644 --- a/src/router/local-menu-flat.ts +++ b/src/router/local-menu-flat.ts @@ -1007,21 +1007,6 @@ export const localMenuFlatItems: MenuItem[] = [ sort_key: 54, created_at: '2025-12-26T13:23:52.533693+08:00', }, - { - id: 75, - identity: '019b591d-0495-7891-8893-5f93b073c4ba', - title: '历史报告', - title_en: 'History Report', - code: 'ops:报表管理:历史报表', - description: '报表管理 - 历史报表', - app_id: 2, - parent_id: 69, - menu_path: '/report/history', - menu_icon: 'appstore', - type: 1, - sort_key: 55, - created_at: '2025-12-26T13:23:52.597561+08:00', - }, { id: 70, identity: '019b591d-0446-7270-b3a6-b0a0e4623962', diff --git a/src/router/local-menu-items.ts b/src/router/local-menu-items.ts index dd7453e..b3b78a8 100644 --- a/src/router/local-menu-items.ts +++ b/src/router/local-menu-items.ts @@ -1084,22 +1084,6 @@ export const localMenuItems: MenuItem[] = [ created_at: '2025-12-26T13:23:52.533693+08:00', children: [], }, - { - id: 75, - identity: '019b591d-0495-7891-8893-5f93b073c4ba', - title: '历史报告', - title_en: 'History Report', - code: 'ops:报表管理:历史报表', - description: '报表管理 - 历史报表', - app_id: 2, - parent_id: 69, - menu_path: '/report/history', - menu_icon: 'appstore', - type: 1, - sort_key: 12, - created_at: '2025-12-26T13:23:52.597561+08:00', - children: [], - }, { id: 70, identity: '019b591d-0446-7270-b3a6-b0a0e4623962', diff --git a/src/views/help/index.vue b/src/views/help/index.vue index b639579..e0325ab 100644 --- a/src/views/help/index.vue +++ b/src/views/help/index.vue @@ -494,13 +494,18 @@ const guides = ref([ items: ['手动创建拓扑图', '自动发现网络拓扑', '编辑设备连接关系', '设置拓扑图展示样式'] }, { - title: '自动感知拓扑图', + title: '自动感知', text: '系统可自动发现网络设备及其连接关系,生成动态拓扑图,实时反映网络架构变化。' }, { title: '流量分析管理', text: '在"流量分析管理"中查看网络流量数据。', - items: ['查看端口流量统计', '分析流量趋势', '识别流量异常', '生成流量报表'] + items: [ + '查看端口流量统计', + '分析流量趋势', + '识别流量异常', + '在「报表管理 → 流量统计」生成报表:拓扑模式走 NetFlow/拓扑汇总;多设备 SNMP 选 traffic_mode=snmp_devices,详见 dc-control 文档《报表管理接口文档》', + ], }, { title: 'IP地址管理', diff --git a/src/views/ops/pages/dc/device-collect/index.vue b/src/views/ops/pages/dc/device-collect/index.vue index 056e5f3..0bbaf56 100644 --- a/src/views/ops/pages/dc/device-collect/index.vue +++ b/src/views/ops/pages/dc/device-collect/index.vue @@ -30,19 +30,22 @@ @@ -128,6 +131,11 @@ const pagination = reactive({ const formItems = computed(() => searchFormConfig) const columns = computed(() => columnsConfig) +/** 避免在模板 :color 中写 ? : 三元,部分 Vue/Vite 版本会误解析冒号 */ +const tagColorOnOff = (on: boolean | undefined) => (on ? 'green' : 'gray') +const tagColorCollectMethod = (m: string | undefined) => + m === 'snmp' ? 'purple' : 'arcoblue' + const fetchRoomDeviceData = async () => { loading.value = true diff --git a/src/views/ops/pages/netarch/auto-topology/index.vue b/src/views/ops/pages/netarch/auto-topology/index.vue index b9146a3..32f0892 100644 --- a/src/views/ops/pages/netarch/auto-topology/index.vue +++ b/src/views/ops/pages/netarch/auto-topology/index.vue @@ -2,7 +2,7 @@