fix 报表数据无法查看与下载
This commit is contained in:
47
README.md
47
README.md
@@ -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`:全工作区模块边界与文档入口。
|
||||
|
||||
@@ -45,7 +45,7 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
type: 1,
|
||||
sort_key: 3,
|
||||
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',
|
||||
},
|
||||
{
|
||||
@@ -60,7 +60,7 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
type: 1,
|
||||
sort_key: 4,
|
||||
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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ export const localMenuItems: MenuItem[] = [
|
||||
type: 1,
|
||||
sort_key: 2,
|
||||
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',
|
||||
children: [],
|
||||
},
|
||||
@@ -94,7 +94,7 @@ export const localMenuItems: MenuItem[] = [
|
||||
type: 1,
|
||||
sort_key: 2,
|
||||
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',
|
||||
children: [],
|
||||
},
|
||||
|
||||
54
src/views/ops/pages/report/statistics/content.ts
Normal file
54
src/views/ops/pages/report/statistics/content.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -215,22 +215,23 @@
|
||||
<div v-if="contentLoading" class="loading-container">
|
||||
<a-spin />
|
||||
</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-descriptions :column="3" bordered>
|
||||
<a-descriptions-item v-for="(value, key) in reportContent.scalars" :key="key" :label="formatLabel(String(key))">
|
||||
{{ formatValue(String(key), value) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-table
|
||||
:data="normalizedReportContent.scalarRows"
|
||||
:columns="scalarTableColumns"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
stripe
|
||||
/>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<!-- 时间序列结果 -->
|
||||
<template v-else-if="reportContent.output_mode === 'timeseries'">
|
||||
<template v-else-if="normalizedReportContent.outputMode === 'timeseries'">
|
||||
<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>
|
||||
</div>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
@@ -256,6 +257,7 @@ import * as echarts from 'echarts'
|
||||
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
||||
import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions'
|
||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||
import { normalizeStatisticsReportContent } from './content'
|
||||
|
||||
const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } =
|
||||
useReportTargetIdentityOptions()
|
||||
@@ -399,19 +401,17 @@ const reportContent = ref<Record<string, any> | null>(null)
|
||||
const chartRef = ref<HTMLElement | 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(() => [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
width: 150,
|
||||
},
|
||||
])
|
||||
const seriesTableColumns = computed(() =>
|
||||
buildContentColumns(normalizedReportContent.value.seriesRows, ['target_identity', 'time', 'timestamp', 'value', 'count']),
|
||||
)
|
||||
|
||||
// 获取报表列表
|
||||
const fetchList = async () => {
|
||||
@@ -604,17 +604,18 @@ const handleViewContent = async (record?: ReportRecord) => {
|
||||
contentModalVisible.value = true
|
||||
contentModalTitle.value = targetRecord.title
|
||||
reportContent.value = null
|
||||
let seriesToRender: Record<string, any>[] = []
|
||||
|
||||
try {
|
||||
const res = await fetchReportContent(targetRecord.id)
|
||||
|
||||
if (res.code === 0 && res.details) {
|
||||
reportContent.value = res.details
|
||||
const normalized = normalizeStatisticsReportContent(res.details)
|
||||
|
||||
// 如果是时间序列模式,渲染图表
|
||||
if (res.details.output_mode === 'timeseries' && res.details.series) {
|
||||
await nextTick()
|
||||
renderChart(res.details.series)
|
||||
if (normalized.outputMode === 'timeseries' && normalized.seriesRows.length > 0) {
|
||||
seriesToRender = normalized.seriesRows
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message || '获取报表内容失败')
|
||||
@@ -625,6 +626,11 @@ const handleViewContent = async (record?: ReportRecord) => {
|
||||
} finally {
|
||||
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> = {
|
||||
avg: '平均值',
|
||||
max: '最大值',
|
||||
min: '最小值',
|
||||
sum: '求和',
|
||||
count: '计数',
|
||||
target_identity: '目标标识',
|
||||
time: '时间',
|
||||
timestamp: '时间',
|
||||
value: '值',
|
||||
}
|
||||
@@ -680,16 +688,18 @@ const formatLabel = (key: string) => {
|
||||
return labelMap[key] || key
|
||||
}
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (key: string, value: any) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
function buildContentColumns(rows: Record<string, any>[], preferredKeys: string[]) {
|
||||
const firstRow = rows[0]
|
||||
if (!firstRow) return []
|
||||
|
||||
// 数字类型保留两位小数
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
const keys = Object.keys(firstRow)
|
||||
const orderedKeys = [...preferredKeys.filter((key) => keys.includes(key)), ...keys.filter((key) => !preferredKeys.includes(key))]
|
||||
|
||||
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: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: series.map((item: any) => item.timestamp),
|
||||
data: series.map((item: any) => item.time || item.timestamp),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
|
||||
Reference in New Issue
Block a user