fix 报表数据无法查看与下载

This commit is contained in:
zxr
2026-07-03 11:09:16 +08:00
parent 8f3dd3e43e
commit b01a0f0979
5 changed files with 149 additions and 38 deletions

View File

@@ -0,0 +1,47 @@
# Front - OPS 前端
`front` 是 OPS 平台的统一前端入口,基于 Vue 3、Vite、Arco Design Vue、Pinia 和 Vue Router。页面以动态菜单为主RBAC 菜单接口不可用时使用本地菜单数据兜底。
## 当前代码入口
| 类型 | 路径 |
| --- | --- |
| 应用入口 | `front/src/main.ts` |
| 路由入口 | `front/src/router/index.ts` |
| 静态路由 | `front/src/router/routes/modules/` |
| 动态菜单转换 | `front/src/router/menu-data.ts` |
| 本地菜单兜底 | `front/src/router/local-menu-items.ts``front/src/router/local-menu-flat.ts` |
| 页面目录 | `front/src/views/` |
| API 模块 | `front/src/api/module/` |
| 构建配置 | `front/config/` |
## 主要页面域
| 页面域 | 目录 |
| --- | --- |
| 首页与概览 | `front/src/views/home``front/src/views/ops/pages/overview` |
| 数据中心与资源 | `front/src/views/ops/pages/dc``datacenter``resource-context` |
| 告警与治理 | `front/src/views/ops/pages/alert``governance` |
| 工单闭环 | `front/src/views/ops/pages/feedback` |
| 知识库 | `front/src/views/ops/pages/kb` |
| 报表与自动化 | `front/src/views/ops/pages/report``automation` |
| 业务系统与拓扑 | `front/src/views/ops/pages/business-system``business-topology` |
| 大屏 | `front/src/views/ops/pages/big-screen``front/src/views/visualization` |
| 日志管理 | `front/src/views/ops/pages/log-mgmt` |
| 系统设置 | `front/src/views/ops/pages/system-settings` |
## 常用命令
```powershell
cd D:\work\ops\front
pnpm.cmd install
pnpm.cmd dev
pnpm.cmd build
```
## 相关文档
- `front/PAGES_README.md`:页面清单和路由说明。
- `front/QUICK_START.md`:本地启动说明。
- `front/docs/`:页面设计、适配验收和部分接口对接文档。
- `docs/模块文档索引.md`:全工作区模块边界与文档入口。

View File

@@ -45,7 +45,7 @@ export const localMenuFlatItems: MenuItem[] = [
type: 1, type: 1,
sort_key: 3, sort_key: 3,
is_web_page: true, is_web_page: true,
web_url: 'https://ops.apinb.com/view/#/project/items', web_url: 'https://ops2.apinb.com/view/#/project/items',
created_at: '2025-12-26T13:23:51.644296+08:00', created_at: '2025-12-26T13:23:51.644296+08:00',
}, },
{ {
@@ -60,7 +60,7 @@ export const localMenuFlatItems: MenuItem[] = [
type: 1, type: 1,
sort_key: 4, sort_key: 4,
is_web_page: true, is_web_page: true,
web_url: 'https://ops.apinb.com/view/#/project/management', web_url: 'https://ops2.apinb.com/view/#/project/management',
created_at: '2026-01-25T10:44:15.33024+08:00', created_at: '2026-01-25T10:44:15.33024+08:00',
}, },
{ {

View File

@@ -78,7 +78,7 @@ export const localMenuItems: MenuItem[] = [
type: 1, type: 1,
sort_key: 2, sort_key: 2,
is_web_page: true, is_web_page: true,
web_url: 'https://ops.apinb.com/view/#/project/items', web_url: 'https://ops2.apinb.com/view/#/project/items',
created_at: '2025-12-26T13:23:51.644296+08:00', created_at: '2025-12-26T13:23:51.644296+08:00',
children: [], children: [],
}, },
@@ -94,7 +94,7 @@ export const localMenuItems: MenuItem[] = [
type: 1, type: 1,
sort_key: 2, sort_key: 2,
is_web_page: true, is_web_page: true,
web_url: 'https://ops.apinb.com/view/#/project/management', web_url: 'https://ops2.apinb.com/view/#/project/management',
created_at: '2026-01-25T10:44:15.33024+08:00', created_at: '2026-01-25T10:44:15.33024+08:00',
children: [], children: [],
}, },

View File

@@ -0,0 +1,54 @@
export type StatisticsReportOutputMode = 'scalar' | 'timeseries' | ''
export interface StatisticsReportViewContent {
outputMode: StatisticsReportOutputMode
scalarRows: Record<string, any>[]
seriesRows: Record<string, any>[]
hasContent: boolean
}
const toRows = (value: unknown): Record<string, any>[] => {
if (!Array.isArray(value)) return []
return value.filter((item): item is Record<string, any> => item !== null && typeof item === 'object' && !Array.isArray(item))
}
const normalizeOutputMode = (value: unknown): StatisticsReportOutputMode => {
if (value === 'scalar' || value === 'timeseries') return value
return ''
}
/** 将后端统计报表 payload 归一化为弹窗可渲染的数据结构。 */
export const normalizeStatisticsReportContent = (content: Record<string, any> | null | undefined): StatisticsReportViewContent => {
if (!content) {
return {
outputMode: '',
scalarRows: [],
seriesRows: [],
hasContent: false,
}
}
const scalarRows = toRows(content.rows)
const legacyScalarRows =
content.scalars && typeof content.scalars === 'object' && !Array.isArray(content.scalars) ? [content.scalars] : []
const seriesRows = toRows(content.series)
let outputMode = normalizeOutputMode(content.output_mode || content.params?.output_mode)
if (!outputMode) {
if (seriesRows.length > 0) {
outputMode = 'timeseries'
} else if (scalarRows.length > 0 || legacyScalarRows.length > 0) {
outputMode = 'scalar'
}
}
const normalizedScalarRows = scalarRows.length > 0 ? scalarRows : legacyScalarRows
const hasContent = outputMode === 'timeseries' ? seriesRows.length > 0 : normalizedScalarRows.length > 0
return {
outputMode,
scalarRows: normalizedScalarRows,
seriesRows,
hasContent,
}
}

View File

@@ -215,22 +215,23 @@
<div v-if="contentLoading" class="loading-container"> <div v-if="contentLoading" class="loading-container">
<a-spin /> <a-spin />
</div> </div>
<div v-else-if="reportContent"> <div v-else-if="normalizedReportContent.hasContent">
<!-- 标量结果 --> <!-- 标量结果 -->
<template v-if="reportContent.output_mode === 'scalar'"> <template v-if="normalizedReportContent.outputMode === 'scalar'">
<a-card title="统计结果" :bordered="false" class="summary-card"> <a-card title="统计结果" :bordered="false" class="summary-card">
<a-descriptions :column="3" bordered> <a-table
<a-descriptions-item v-for="(value, key) in reportContent.scalars" :key="key" :label="formatLabel(String(key))"> :data="normalizedReportContent.scalarRows"
{{ formatValue(String(key), value) }} :columns="scalarTableColumns"
</a-descriptions-item> :pagination="{ pageSize: 10 }"
</a-descriptions> stripe
/>
</a-card> </a-card>
</template> </template>
<!-- 时间序列结果 --> <!-- 时间序列结果 -->
<template v-else-if="reportContent.output_mode === 'timeseries'"> <template v-else-if="normalizedReportContent.outputMode === 'timeseries'">
<div ref="chartRef" class="chart-container"></div> <div ref="chartRef" class="chart-container"></div>
<a-table :data="reportContent.series || []" :columns="seriesTableColumns" :pagination="{ pageSize: 10 }" stripe /> <a-table :data="normalizedReportContent.seriesRows" :columns="seriesTableColumns" :pagination="{ pageSize: 10 }" stripe />
</template> </template>
</div> </div>
<a-empty v-else description="暂无数据" /> <a-empty v-else description="暂无数据" />
@@ -256,6 +257,7 @@ import * as echarts from 'echarts'
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions' import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions' import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions'
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow' import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
import { normalizeStatisticsReportContent } from './content'
const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } = const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } =
useReportTargetIdentityOptions() useReportTargetIdentityOptions()
@@ -399,19 +401,17 @@ const reportContent = ref<Record<string, any> | null>(null)
const chartRef = ref<HTMLElement | null>(null) const chartRef = ref<HTMLElement | null>(null)
let chartInstance: echarts.ECharts | null = null let chartInstance: echarts.ECharts | null = null
const normalizedReportContent = computed(() => normalizeStatisticsReportContent(reportContent.value))
// 标量结果表格列配置
const scalarTableColumns = computed(() =>
buildContentColumns(normalizedReportContent.value.scalarRows, ['target_identity', 'avg', 'max', 'min', 'sum', 'count']),
)
// 时间序列表格列配置 // 时间序列表格列配置
const seriesTableColumns = computed(() => [ const seriesTableColumns = computed(() =>
{ buildContentColumns(normalizedReportContent.value.seriesRows, ['target_identity', 'time', 'timestamp', 'value', 'count']),
title: '时间', )
dataIndex: 'timestamp',
width: 180,
},
{
title: '值',
dataIndex: 'value',
width: 150,
},
])
// 获取报表列表 // 获取报表列表
const fetchList = async () => { const fetchList = async () => {
@@ -604,17 +604,18 @@ const handleViewContent = async (record?: ReportRecord) => {
contentModalVisible.value = true contentModalVisible.value = true
contentModalTitle.value = targetRecord.title contentModalTitle.value = targetRecord.title
reportContent.value = null reportContent.value = null
let seriesToRender: Record<string, any>[] = []
try { try {
const res = await fetchReportContent(targetRecord.id) const res = await fetchReportContent(targetRecord.id)
if (res.code === 0 && res.details) { if (res.code === 0 && res.details) {
reportContent.value = res.details reportContent.value = res.details
const normalized = normalizeStatisticsReportContent(res.details)
// 如果是时间序列模式,渲染图表 // 如果是时间序列模式,渲染图表
if (res.details.output_mode === 'timeseries' && res.details.series) { if (normalized.outputMode === 'timeseries' && normalized.seriesRows.length > 0) {
await nextTick() seriesToRender = normalized.seriesRows
renderChart(res.details.series)
} }
} else { } else {
Message.error(res.message || '获取报表内容失败') Message.error(res.message || '获取报表内容失败')
@@ -625,6 +626,11 @@ const handleViewContent = async (record?: ReportRecord) => {
} finally { } finally {
contentLoading.value = false contentLoading.value = false
} }
if (seriesToRender.length > 0) {
await nextTick()
renderChart(seriesToRender)
}
} }
// 导出报表 // 导出报表
@@ -666,13 +672,15 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
} }
// 格式化标签 // 格式化标签
const formatLabel = (key: string) => { function formatLabel(key: string) {
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
avg: '平均值', avg: '平均值',
max: '最大值', max: '最大值',
min: '最小值', min: '最小值',
sum: '求和', sum: '求和',
count: '计数', count: '计数',
target_identity: '目标标识',
time: '时间',
timestamp: '时间', timestamp: '时间',
value: '值', value: '值',
} }
@@ -680,16 +688,18 @@ const formatLabel = (key: string) => {
return labelMap[key] || key return labelMap[key] || key
} }
// 格式化数值 function buildContentColumns(rows: Record<string, any>[], preferredKeys: string[]) {
const formatValue = (key: string, value: any) => { const firstRow = rows[0]
if (value === null || value === undefined) return '-' if (!firstRow) return []
// 数字类型保留两位小数 const keys = Object.keys(firstRow)
if (typeof value === 'number') { const orderedKeys = [...preferredKeys.filter((key) => keys.includes(key)), ...keys.filter((key) => !preferredKeys.includes(key))]
return value.toFixed(2)
}
return value return orderedKeys.map((key) => ({
title: formatLabel(key),
dataIndex: key,
width: key === 'time' || key === 'timestamp' ? 180 : 150,
}))
} }
// 渲染图表 // 渲染图表
@@ -715,7 +725,7 @@ const renderChart = (series: any[]) => {
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: series.map((item: any) => item.timestamp), data: series.map((item: any) => item.time || item.timestamp),
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',