# 看板 KPI 卡片：当月→当季→当年 三层递进设计

## 背景

原看板 3 张 KPI 卡片存在冗余（"半年度进度"与"年度目标"环形图重复显示同一达成率），且缺少月度目标和季度进度的独立视角。
重构为三层递进：**当月进展** → **当季（Q3）冲刺** → **全年跟踪**。

## 数据源

- `数据/kpi_dashboard.json`：年度总目标 `total_target`、H1 累计 `total_h1`、**H1+H2 累计 `total_h1_h2`**、各部门 `depts[].h2_months`（7-12月目标）
- `数据/three_year_monthly.json`：2026年逐月实际 `"2026":{"7":..., "8":..., ...}`
- `数据/kpi_progress.json`（可选）：当月 MCP 实时销售 `total` / `by_salesperson`

## 关键计算（getKPI 函数）

```javascript
// 🔴 年度累计：必须用 total_h1_h2（含7+8月实际），不能只取 total_h1
var ytd = dash.total_h1_h2 || dash.total_h1 || 0;

// 提取 H2 月度部门目标（三部门加总）
var h2TargetsArr = [0, 0, 0, 0, 0, 0];
(dash.depts || []).forEach(function(dp) {
  (dp.h2_months || []).forEach(function(v, i) { h2TargetsArr[i] += v || 0; });
});

// H2 实际从 three_year_monthly 取
var h2Actuals = [
  t2026['7'] || 0, t2026['8'] || 0, t2026['9'] || 0,
  t2026['10'] || 0, t2026['11'] || 0, t2026['12'] || 0
];

// 当月索引
var curMon = new Date().getMonth() + 1;
var h2Idx = Math.max(0, curMon - 7); // Jul=0, Aug=1...

// 三层指标
var monthActual = h2Actuals[h2Idx] || current || 0;
var monthTarget = h2TargetsArr[h2Idx] || 0;
var q3Target = h2TargetsArr[0] + h2TargetsArr[1] + h2TargetsArr[2];
var q3Actual = h2Actuals[0] + h2Actuals[1] + h2Actuals[2];

// 返回时新增 monthTarget/monthActual/q3Target/q3Actual
```

## 卡片渲染（renderDashboard）

三张卡片顺序：当月 → 当季 → 年度

### 卡片1：📅 当月进展
- 当月实际金额（大号数字）
- 当月目标 + 达成率%
- 进度条（6px 高度，`colorByRate` 着色）
- 环比上月% 变化（绿色↑/红色↓）

### 卡片2：📊 Q3 当季进展 (7-9月)
- Q3 累计实际
- Q3 目标 + 达成率%
- 进度条
- Q3 剩余金额

### 卡片3：🎯 年度目标（保留环形图）
- 年度目标金额
- 累计达成
- 年化投影（`ytd / curMonth * 12`）
- SVG 环形图（同原版，但去掉了冗余文字）

## 进度条内联样式

因原 CSS 无通用 `.bar-wrap` 规则，使用内联样式：
```javascript
'<div style="height:6px;background:#e5e7eb;border-radius:3px;margin-top:6px;overflow:hidden">' +
  '<div style="height:100%;border-radius:3px;background:' + colorByRate(rate) +
  ';width:' + Math.min(rate, 100) + '%"></div>' +
'</div>'
```

## 🔴 致命陷阱

### 陷阱1：ytd 用了 dash.total_h1 而不是 dash.total_h1_h2（2026-08-09 实战）

**症状**：年度卡片"累计达成"显示 2164万（仅H1），而非正确的 2631万（H1+H2）。环形图达成率偏低（31.1% 而非 37.9%），年化投影也偏低（3246万 而非 3946万）。

**根因**：`getKPI()` 中 `var ytd = dash.total_h1 || 0;`（旧代码），忘记改为 `dash.total_h1_h2`。`total_h1_h2` 字段已在 `kpi_dashboard.json` 中存在（值=26309808.9），但代码只用 `total_h1`。

**修复**：`var ytd = dash.total_h1_h2 || dash.total_h1 || 0;`

**检测**：浏览器控制台 → `state.data.kpiDashboard.total_h1_h2` 应输出 26309808.9。

### 陷阱2：只改了 app.html 漏了 index.html

两个文件 `getKPI()` 结构相似但不同。修改时必须同时改两处 `ytd` 赋值行。本次 session `app.html` 被正确更新但 `index.html` 漏改，导致 CloudBase 线上仍显示错误数值。

### 陷阱3：threeYearMonthly 数据时效性

`three_year_monthly.json` 的 meta 标注生成日期（如 2026-07-21），8月数据可能为部分快照而非完整月。MCP `kpi_progress.json` 的 `total` 字段可能有更新的当月数据。优先用 `t2026['8'] || current` 降级链（但注意 `h2Actuals` 数组内不要直接用 `|| current`，仅在 `monthActual` 计算时兜底）。

### 陷阱4：h2Actuals 不要用 prog.current 覆盖历史月份

原代码 `t2026['8'] || current` 会导致 8 月 threeYearMonthly 数据被 MCP 当月值覆盖。改为 `t2026['8'] || 0`，兜底逻辑放在 `monthActual` 计算中。

### 陷阱5：CloudBase HTML 部署 ≠ JSON 同步（2026-08-09 实战）

**症状**：修改了 `工具/index.html`，运行 `cb_sync_data.sh`，但 CloudBase 页面仍然是旧版。

**根因**：`~/.hermes/scripts/cb_sync_data.sh` 只部署 `数据/` 目录下的 JSON 文件，不部署 HTML。

**修复**：HTML 文件需要单独部署：
```bash
tcb hosting deploy 工具/index.html /workbench/index.html -e bier-sales-d0gatbvlx288724e9
```

### 陷阱6：CDN 缓存导致修正不可见（2026-08-09 实战）

**症状**：`tcb hosting deploy` 成功，`curl` 验证新代码已上线，但用户仍看到旧版数值。

**根因**：CloudBase CDN 缓存旧版 HTML，用户浏览器/飞书 WebView 也有本地缓存。

**验证方法**：
```bash
curl -s -H "Cache-Control: no-cache" "https://{env}.tcloudbaseapp.com/workbench/index.html" | grep 'total_h1_h2'
```

**用户侧**：无痕窗口打开 URL，或飞书设置 → 清除缓存。

## 修改同步清单

每次改看板卡片时必须同时修改两个文件：

| 文件 | 路径 | 修改位置 | 部署方式 |
|------|------|---------|---------|
| CloudBase 版 | `工具/index.html` | `getKPI()` (line~990) + `renderDashboard()` (line~1220) | `tcb hosting deploy` |
| 飞书版 | `工具/app.html` | `getKPI()` (line~593) + `renderDashboard()` (line~617) | 本地 HTTP 服务器重启 |

### 两个文件的关键差异

| 特性 | index.html | app.html |
|------|-----------|---------|
| 筛选器（部门/销售员） | ✅ | ❌ |
| `filteredSalesSet` 覆盖逻辑 | ✅ | ❌ |
| `threeYearMonthly` 数据源 | ✅ | ❌（原先只有 `[current,0,0,0,0,0]`） |
| 统计卡片（客户数/联系人） | ❌ | ✅ |
| 部署目标 | CloudBase | 本地 8765 → 飞书 WebView |

## 相关文件

- `工具/index.html` — renderDashboard() + getKPI()（CloudBase 部署到 /workbench/index.html）
- `工具/app.html` — 同上（飞书工作台主力入口）
- `数据/kpi_dashboard.json` — depts[].h2_months, total_h1_h2
- `数据/three_year_monthly.json` — 2026 年逐月实际
- `~/.hermes/scripts/cb_sync_data.sh` — JSON 同步脚本（不含 HTML）
