From 24dd0c17daddbe660cbb212a2cda1bc92d8911f4 Mon Sep 17 00:00:00 2001 From: zxr <271055687@qq.com> Date: Tue, 4 Aug 2026 01:02:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=8A=A5=E8=A1=A8=E4=BA=A4=E4=BA=92=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ops/report.ts | 44 +++------- src/views/ops/pages/report/history/index.vue | 90 +++++++++++--------- src/views/ops/pages/report/topn/index.vue | 79 ++--------------- 3 files changed, 68 insertions(+), 145 deletions(-) diff --git a/src/api/ops/report.ts b/src/api/ops/report.ts index b1def2c..9d7a546 100644 --- a/src/api/ops/report.ts +++ b/src/api/ops/report.ts @@ -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' // ============ 通用响应类型 ============ @@ -136,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 @@ -161,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 } /** 证据导出事件行 */ @@ -239,7 +234,8 @@ export const fetchReportMetricsAvailable = (params: { identities?: string keyword?: string limit?: number -}) => request.get>('/DC-Control/v1/reports/metrics/available', { params }) +}) => + request.get>('/DC-Control/v1/reports/metrics/available', { params }) /** 逻辑指标目录(Registry) */ export const fetchReportMetricsRegistry = (params: { data_source: string }) => @@ -275,21 +271,14 @@ function exportBufferToBlob(ab: ArrayBuffer, format: ReportExportFormat, content 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 + 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' + const mime = contentType && /csv|text|plain/i.test(contentType) ? contentType : 'text/csv;charset=utf-8' return new Blob([ab], { type: mime }) } @@ -308,11 +297,7 @@ function parseContentDispositionFileName(value: string | null): string | undefin return value.match(/filename="([^"]+)"/i)?.[1] } -function normalizeReportFileName( - fileName: string | undefined, - id: number, - format: ReportExportFormat, -): string { +function normalizeReportFileName(fileName: string | undefined, id: number, format: ReportExportFormat): string { const defaultName = `report_${id}.${format}` if (!fileName) return defaultName @@ -340,10 +325,7 @@ function normalizeReportFileName( * 报表文件导出:使用 fetch + arrayBuffer,绕过 axios 拦截器与 Blob 中间态, * 避免网络面板里已是合法 xlsx(PK…)但落盘文件损坏的情况。 */ -export const exportReport = async ( - id: number, - format: ReportExportFormat = '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) @@ -367,11 +349,7 @@ export const exportReport = async ( } return { blob: exportBufferToBlob(ab, format, r.headers.get('content-type')), - fileName: normalizeReportFileName( - parseContentDispositionFileName(r.headers.get('content-disposition')), - id, - format, - ), + fileName: normalizeReportFileName(parseContentDispositionFileName(r.headers.get('content-disposition')), id, format), } } diff --git a/src/views/ops/pages/report/history/index.vue b/src/views/ops/pages/report/history/index.vue index e675425..cae1831 100644 --- a/src/views/ops/pages/report/history/index.vue +++ b/src/views/ops/pages/report/history/index.vue @@ -48,7 +48,7 @@ v-if="canDownloadPDF(record)" type="text" size="small" - :loading="downloadingId === record.id" + :loading="downloadingIds.has(record.id)" @click="handleDownloadPDF(record)" > 下载 PDF @@ -66,13 +66,7 @@ import { computed, onBeforeUnmount, reactive, ref } from 'vue' import { Message } from '@arco-design/web-vue' import SearchTable from '@/components/search-table/index.vue' import type { FormItem } from '@/components/search-form/types' -import { - fetchReportList, - ReportStatus, - ReportType, - type ReportListParams, - type ReportRecord, -} from '@/api/ops/report' +import { fetchReportList, ReportStatus, ReportType, type ReportListParams, type ReportRecord } from '@/api/ops/report' import { downloadReportFile } from '../useReportExport' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' @@ -150,7 +144,7 @@ const formItems = computed(() => [ ]) const loading = ref(false) -const downloadingId = ref(null) +const downloadingIds = reactive(new Set()) const tableData = ref([]) let latestRequestId = 0 @@ -218,7 +212,7 @@ const tableColumns = computed(() => [ const canDownloadPDF = (record: ReportRecord) => record.status === ReportStatus.SUCCESS && pdfReportTypes.has(record.report_type as ReportType) -const fetchList = async () => { +const fetchList = async (): Promise => { const requestId = ++latestRequestId loading.value = true @@ -228,44 +222,57 @@ const fetchList = async () => { const timeRange = formModel.value.timeRange const page = pagination.current const size = pagination.pageSize + const params: ReportListParams = { + page, + size, + } + + if (reportType) { + params.report_type = reportType + } + if (status) { + params.status = status + } + if (keyword) { + params.keyword = keyword + } + if (timeRange?.length === 2 && timeRange[0] && timeRange[1]) { + params.created_from = timeRange[0] + params.created_to = timeRange[1] + } + try { - const params: ReportListParams = { - page, - size, - } + while (true) { + const res = await fetchReportList(params) + if (requestId !== latestRequestId) return false - if (reportType) { - params.report_type = reportType - } - if (status) { - params.status = status - } - if (keyword) { - params.keyword = keyword - } - if (timeRange?.length === 2 && timeRange[0] && timeRange[1]) { - params.created_from = timeRange[0] - params.created_to = timeRange[1] - } + if (res.code !== 0 || !res.details) { + tableData.value = [] + pagination.total = 0 + Message.error(res.message || '获取报表列表失败') + return false + } - const res = await fetchReportList(params) - if (requestId !== latestRequestId) return + const total = res.details.total || 0 + const maximumPage = Math.max(1, Math.ceil(total / size)) + if ((params.page || 1) > maximumPage) { + pagination.current = maximumPage + params.page = maximumPage + continue + } - if (res.code === 0 && res.details) { tableData.value = normalizeReportRows(res.details.data || []) - pagination.total = res.details.total || 0 - } else { - tableData.value = [] - pagination.total = 0 - Message.error(res.message || '获取报表列表失败') + pagination.total = total + return true } } catch (error: any) { - if (requestId !== latestRequestId) return + if (requestId !== latestRequestId) return false tableData.value = [] pagination.total = 0 console.error('获取报表列表失败:', error) Message.error(error.message || '获取报表列表失败') + return false } finally { if (requestId === latestRequestId) { loading.value = false @@ -296,9 +303,10 @@ const handleReset = () => { fetchList() } -const handleRefresh = () => { - fetchList() - Message.success('数据已刷新') +const handleRefresh = async () => { + if (await fetchList()) { + Message.success('数据已刷新') + } } const handlePageChange = (current: number) => { @@ -313,7 +321,7 @@ const handlePageSizeChange = (pageSize: number) => { } const handleDownloadPDF = async (record: ReportRecord) => { - downloadingId.value = record.id + downloadingIds.add(record.id) try { await downloadReportFile(record, 'pdf') Message.success('PDF 下载成功') @@ -321,7 +329,7 @@ const handleDownloadPDF = async (record: ReportRecord) => { console.error('PDF 下载失败:', error) Message.error(error.message || 'PDF 下载失败') } finally { - downloadingId.value = null + downloadingIds.delete(record.id) } } diff --git a/src/views/ops/pages/report/topn/index.vue b/src/views/ops/pages/report/topn/index.vue index 4a32beb..12662e0 100644 --- a/src/views/ops/pages/report/topn/index.vue +++ b/src/views/ops/pages/report/topn/index.vue @@ -99,11 +99,7 @@ - + - + - - - @@ -219,12 +187,7 @@
- + @@ -236,14 +199,7 @@ import { ref, reactive, computed, nextTick, watch } from 'vue' import { Message } from '@arco-design/web-vue' import SearchTable from '@/components/search-table/index.vue' import type { FormItem } from '@/components/search-form/types' -import { - fetchReportList, - generateReport, - fetchReportContent, - ReportType, - type ReportRecord, - type TopNReportParams, -} from '@/api/ops/report' +import { fetchReportList, generateReport, fetchReportContent, 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' @@ -251,8 +207,7 @@ import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOption import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' -const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } = - useReportTargetIdentityOptions() +const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } = useReportTargetIdentityOptions() const { metricOptions, metricOptionsLoading, loadMetricRegistryOptions } = useReportMetricRegistryOptions() @@ -362,8 +317,6 @@ const generateForm = ref<{ n: number rank_aggregate: string order: string - metric_type: string - collector_identity: string title: string }>({ data_source: '', @@ -373,8 +326,6 @@ const generateForm = ref<{ n: 10, rank_aggregate: 'avg', order: 'desc', - metric_type: '', - collector_identity: '', title: '', }) @@ -389,7 +340,7 @@ watch( } loadTargetIdentityOptions(ds) loadMetricRegistryOptions(ds) - }, + } ) // 查看内容弹窗 @@ -512,8 +463,6 @@ const handleOpenGenerateModal = () => { n: 10, rank_aggregate: 'avg', order: 'desc', - metric_type: '', - collector_identity: '', title: '', } generateModalVisible.value = true @@ -570,16 +519,6 @@ const handleGenerate = async () => { params.order = generateForm.value.order as any } - // 主机数据源额外参数 - if (generateForm.value.data_source === 'dc-host') { - if (generateForm.value.metric_type) { - params.metric_type = generateForm.value.metric_type as any - } - if (generateForm.value.collector_identity) { - params.collector_identity = generateForm.value.collector_identity - } - } - const res = await generateReport({ report_type: ReportType.TOPN, title: generateForm.value.title || undefined, @@ -696,9 +635,7 @@ const renderChart = (ranking: any[]) => { }, yAxis: { type: 'category', - data: ranking - .map((item: any) => item.target ?? item.identity ?? item.target_identity ?? '') - .reverse(), + data: ranking.map((item: any) => item.target ?? item.identity ?? item.target_identity ?? '').reverse(), }, series: [ {