# 问题处理文档 - 报告查看TPS趋势和响应时间趋势图表不显示

> **文档类型**: 问题处理文档
> **创建日期**: 2026-08-13
> **作者**: czj
> **优先级**: P0
> **状态**: 待修复

---

## 一、问题描述

### 1.1 现象

性能测试任务执行完成后，通过左侧菜单「性能测试 → 报告查看」进入报告面板，摘要信息、核心指标卡片、响应时间摘要、状态码分布均正常显示，但**TPS趋势图和响应时间趋势图两个ECharts图表区域为空白**，没有渲染出任何折线或面积图。

![图表空白](图片占位 - 实际表现为两个图表卡片内无内容)

### 1.2 复现步骤

1. 执行一个性能测试任务（任意模式），等待完成
2. 通过左侧菜单「性能测试 → 报告查看」进入
3. 在任务选择器中选择刚完成的任务 → 点击"查看报告"
4. 观察：摘要和指标正常，但两个趋势图空白
5. 点击"刷新报告"按钮 → 现象不变

### 1.3 影响范围

- 所有已完成/失败的性能测试任务均受影响
- 报告面板的趋势图功能完全不可用
- 用户无法通过趋势图直观观察压测过程中TPS和响应时间的变化趋势

---

## 二、根因分析

### 2.1 代码定位

**文件**：`frontend/src/views/performance/ReportPanel.vue`

**关键代码片段**——模板结构（第 41-163 行）：

```html
<template v-else>
  <!-- 刷新/导出按钮 -->
  <el-skeleton v-if="loading" :rows="8" animated />
  <template v-else-if="report">
    <!-- 摘要信息、指标卡片、状态码表格 -->
    <!-- TPS 趋势图 (ref="tpsChartRef") -->
    <!-- 响应时间趋势图 (ref="rtChartRef") -->
  </template>
  <el-empty v-else description="暂无报告数据" />
</template>
```

**关键代码片段**——`loadReport()` 函数（第 247-261 行）：

```typescript
async function loadReport() {
  if (!taskId.value) return
  loading.value = true
  try {
    const res = await getReport(taskId.value)
    report.value = res
    await nextTick()
    initCharts()    // ← 此时 loading 仍为 true，图表 DOM 不存在
    updateCharts()  // ← 操作未初始化的图表实例
  } catch (e: any) {
    ElMessage.error('加载报告失败: ' + ...)
  } finally {
    loading.value = false  // ← 图表初始化完成后才隐藏骨架
  }
}
```

**`initCharts()` 函数**（第 276-283 行）：

```typescript
function initCharts() {
  if (tpsChartRef.value && !tpsChart) {
    tpsChart = echarts.init(tpsChartRef.value)
  }
  if (rtChartRef.value && !rtChart) {
    rtChart = echarts.init(rtChartRef.value)
  }
}
```

### 2.2 根因详解

**Vue 条件渲染冲突**：

1. 模板使用 `v-if="loading"`（骨架屏）和 `v-else-if="report"`（图表内容）构成互斥条件分支
2. `loadReport()` 的调用时序：
   - ① `loading.value = true` → 骨架屏渲染
   - ② `report.value = res` → 数据赋值
   - ③ `await nextTick()` → 等待 Vue 完成一次 DOM 更新
   - ④ **`initCharts()`** → 此时 `loading` 仍为 `true`，所以 DOM 中渲染的是骨架屏，**不是图表区块**
   - ⑤ `updateCharts()` → 操作未初始化的 `tpsChart` 和 `rtChart`（均为 `null`）
   - ⑥ `finally { loading.value = false }` → 骨架屏隐藏，图表区块渲染，但此时 `initCharts()` 已过

3. 结果：`tpsChartRef.value` 和 `rtChartRef.value` 为 `undefined`，`echarts.init(undefined)` 静默失败，图表实例未创建，后续 `setOption()` 无效果

### 2.3 对比分析：MonitorPanel 为何正常

**文件**：`frontend/src/views/performance/MonitorPanel.vue`

MonitorPanel 的图表在模板中**不在条件渲染分支内**——只要 `taskId` 存在，图表 div 始终在 DOM 中：

```html
<template v-else>
  <!-- TPS 趋势图 -->
  <el-card><div ref="tpsChartRef" class="chart" /></el-card>
  <!-- 响应时间趋势图 -->
  <el-card><div ref="rtChartRef" class="chart" /></el-card>
  <!-- 状态码分布 -->
  <el-card><div ref="statusChartRef" class="chart" /></el-card>
</template>
```

`initCharts()` 在 `onMounted` 和 `watch` 中调用，此时图表 div 已在 DOM 中，`ref` 值有效，`echarts.init()` 成功。

### 2.4 数据流验证

已通过实际 API 请求验证后端数据正常：

| 验证项 | 结果 |
|--------|------|
| API 返回快照数量 | 241 条 |
| 数据字段格式 | camelCase（`p50ResponseTime`、`avgResponseTime`、`tps` 等） |
| 字段值 | 非零有效值（如 TPS 22~64，响应时间 201~818ms） |
| 前端类型定义 | `PerformanceSnapshot` 接口字段名与 API 返回一致 |
| Pydantic 序列化 | `PerformanceSnapshotResponse` 使用 `populate_by_name=True`+`alias_generator=to_camel`，正确输出 camelCase |

**结论：后端 API 数据完全正确，问题纯属前端渲染时序 Bug。**

---

## 三、修复方案

### 3.1 方案 A（推荐）：调整 loading 状态设置时机

**改动最小**：在 `loadReport()` 中，设置 `report.value = res` 后立即将 `loading` 设为 `false`，使图表 div 渲染到 DOM 中，再调用 `initCharts()`。

```typescript
async function loadReport() {
  if (!taskId.value) return
  loading.value = true
  try {
    const res = await getReport(taskId.value)
    report.value = res
    loading.value = false         // ← 先让图表 div 渲染到 DOM
    await nextTick()              // ← 等待 DOM 更新
    initCharts()                  // ← 此时 ref 有效
    updateCharts()                // ← 图表正确渲染
  } catch (e: any) {
    ElMessage.error('加载报告失败: ' + ...)
    loading.value = false
  }
  // 移除 finally 中的 loading=false
}
```

**优点**：改动仅 1 行，逻辑清晰，不影响骨架屏的加载体验。
**缺点**：无。

### 3.2 方案 B：使用 `watch` 监听 `report` 变化

利用 Vue 的 `watch` 在 `report` 值变化后自动初始化图表：

```typescript
watch(report, (newVal) => {
  if (newVal) {
    nextTick(() => {
      initCharts()
      updateCharts()
    })
  }
})
```

**优点**：关注点分离，`loadReport()` 只负责数据加载。
**缺点**：增加额外 watch，需要处理组件卸载时 dispose。

### 3.3 方案选择

**推荐方案 A**，原因：
- 改动最小（1 行代码）
- 修复逻辑直观
- 不引入新的 watch 或生命周期复杂度
- 不影响骨架屏的首次加载体验（API 调用期间骨架屏正常显示）

---

## 四、验收标准

| 测试项 | 预期结果 |
|--------|----------|
| 报告查看 - TPS 趋势图 | 显示折线面积图，x 轴为时间（秒），y 轴为 TPS 值 |
| 报告查看 - 响应时间趋势图 | 显示多条折线（平均/P50/P90/P99），x 轴为时间，y 轴为 ms |
| 报告查看 - 刷新报告 | 点击刷新后图表重新加载，保持显示 |
| 报告查看 - 首次加载 | 骨架屏在 API 调用期间正常显示，数据到达后平滑切换为图表 |
| 报告查看 - 无数据任务 | 显示"暂无报告数据"空状态，图表不渲染 |
| 报告查看 - 无任务 | 显示任务选择器（回归测试） |
| MonitorPanel 回归 | 实时监控面板图表不受影响 |

---

## 五、附录

### 5.1 涉及文件

| 文件 | 说明 |
|------|------|
| `frontend/src/views/performance/ReportPanel.vue` | 需要修改的主文件（第 252 行后增加 `loading.value = false`） |

### 5.2 相关技术栈

- Vue 3 Composition API（`v-if`/`v-else-if` 条件渲染）
- ECharts（`echarts.init()` 需要 DOM 元素已存在）
- Element Plus `el-skeleton`（骨架屏组件）

### 5.3 已知风险

- 无。修复逻辑简单，且已通过 MonitorPanel 的实现验证了正确模式。

---

*本文档由 Claude Code 生成，遵循项目问题处理文档规范。*