# 趋势大改 + 健康日历移至主页 — 设计文档

> 日期:2026-06-07 · 状态:已定方案(用户授权直接实现,免确认)

## 1. 背景与目标

当前「趋势」Tab(`TrendsView.swift`)把两件事混在一起:

1. **健康日历**(月/年视图 + 当日详情)—— 占据页面上半部分。
2. **长期监测折线图**(`seriesSection`)—— 页面下半部分。

两个问题:

- **日历放错了地方**。它是「总览记录情况」的入口,更适合放在主页(用户每天第一眼看的页面),而不是埋在趋势 Tab 里。
- **趋势能力太弱**。`SeriesBucket.build` **只按 `seriesKey` 分桶**,因此只有 8 个长期监测预设(血压/血糖/体温…)和自定义指标能成图。所有**没有 seriesKey 的指标**——报告里解析出来的化验项、VL 快拍、自由输入——即使在多份报告里反复出现(如「血红蛋白」体检了 3 次),也**完全看不到趋势**。

### 目标

1. **健康日历移到主页**:主页新增一张紧凑的「健康日历」卡(当前周的横条 + 本月记录摘要),点击展开完整的月/年总览页(可切月视图/年视图、看当日详情)。
2. **趋势 Tab 重构**:对**任何出现 ≥2 次的指标**(不限于长期监测预设)做时间序列查看。趋势页变成一个「可成趋势的指标」总览列表(分长期监测 / 化验指标两段),点任一项进入详情页:大图表 + 参考范围带 + 统计摘要(最新/最高/最低/平均/对比上次)+ 时间范围筛选 + 数据点列表(点击跳当日详情)。

### 非目标(本次不做)

- **AI 趋势解读**:需要 AIRuntime + TrendService 跑通,风险大、与本次「时间序列查看」正交。本次预留 UI 位但不接 LLM,留作后续。
- 不改 SwiftData schema(无 @Model 字段变更,规避迁移丢数据风险)。
- 不改 `Localizable.xcstrings`(新文案用 `String(appLoc: "中文")`,无对应词条时优雅回退到中文 key,符合既有大量用法;避免 xcstrings 噪声 diff)。
- 不动 TabBar 5 槽骨架、不动录入流程。

## 2. 架构总览

```
主页 HomeView
  └─ HomeCalendarCard(自包含 @Query)   ← 新增
        当前周横条 + "本月 N 天有记录" + chevron
        tap → fullScreenCover(CalendarOverviewView)   ← 新增(从 TrendsView 抽出)

趋势 TrendsView(重写)
  └─ TrendSeriesList:两段 section
        ├─ 长期监测(kind=.monitor:seriesKey 分桶,含血压合并/自定义)
        └─ 化验指标趋势(kind=.lab:按 name+unit 分桶,≥2 点)
        每行 TrendRow:名称 + 最新值/状态 + mini sparkline + 条数·跨度
        tap → TrendDetailView(bucket)   ← 新增
              大图表 + 参考范围带 + 时间范围 chips + 统计摘要 + 数据点列表
              数据点 tap → DayDetailSheet(date)(复用)
```

数据层只扩展 `SeriesBucket.build`,UI 层新增 4 个文件、改 2 个文件、删 1 段。

## 3. 数据层:`SeriesBucket` 扩展

文件:`Features/Trends/SeriesBucket.swift`(改)

### 3.1 新增 `kind` 区分两段

```swift
enum SeriesKind { case monitor, lab }   // monitor=长期监测预设/自定义/血压;lab=按名分组的化验项

struct SeriesBucket: Identifiable {
    let id: String
    let title: String
    let unit: String
    let lines: [SeriesLine]
    let latestDate: Date
    let kind: SeriesKind          // 新增
    let sourceIndicatorIDs: [String]   // 新增:本桶包含的 Indicator persistentModelID 字符串,供详情页定位来源
    // ... SeriesLine / Point 不变
}
```

### 3.2 `build` 流程改为两段

1. **seriesKey 段(原逻辑,kind=.monitor)**:血压合并、单系列预设、自定义。这些桶里的 Indicator 标记为「已消费」。
2. **name 段(新,kind=.lab)**:对**所有没有 seriesKey** 的 Indicator,按 `normalizedKey(name, unit)` 分桶;每桶 ≥ `minPoints` 才保留。参考范围从该桶**最新一条** Indicator 的 `range` 字符串解析。
3. 两段合并返回,各自按 `latestDate` 倒序。详情/列表按 `kind` 分段。

```swift
// name 归一化:trim + 小写 + 折叠内部空白;unit 同样 trim。key = "name|unit"
static func normalizedKey(name: String, unit: String) -> String

// 解析参考范围字符串 → ClosedRange<Double>?
// 支持 "3.9-6.1" / "3.9~6.1" / "3.9 - 6.1";单边("<5.2"/">40"/"≤120")暂返回 nil(图不画带,正常)
static func parseRange(_ raw: String) -> ClosedRange<Double>?
```

> **去重**:有 seriesKey 的指标只进 monitor 段;无 seriesKey 的只进 lab 段。即使同名也不混。
> **状态着色**:lab 段每个 Point 的 `status` 直接取 Indicator.status(已由 VL/录入判定),无需重算。

## 4. UI:健康日历移至主页

### 4.1 `CalendarOverviewView`(新文件 `Features/Calendar/CalendarOverviewView.swift`)

把现 `TrendsView` 的日历部分**原样抽出**为独立页:`modeSwitch`(月/年)+ `anchorBar`(◀ 年月 ▶)+ `calendarBody`(`CalendarMonthGrid`/`CalendarYearGrid`)+ `legend` + 月视图下的 `dayDetailInline`。

- 自带 `@Query`(indicators/reports/diaries/symptoms/profiles/customMetrics)。
- 接收可选 `initialDate`(从主页某天进入时定位选中)。
- 包在 `NavigationStack`,标题「健康日历」,右上「完成」关闭(用于 fullScreenCover)。
- `CalendarMonthGrid` / `CalendarYearGrid` / `CalendarMarkers` / `DayDetailSheet` **不改**,直接复用。

### 4.2 `HomeCalendarCard`(新文件 `Features/Home/HomeCalendarCard.swift`)

自包含组件(对齐 `TodayRemindersCard` 模式):

- 自带 `@Query`,`CalendarData.build` 计算标记。
- **当前周横条**:周一→周日 7 个紧凑日格(日期数字 + 标记圆点,复用 `DayMarks` 颜色规则:异常红 / 报告灰 / 正常绿 / 日记浅灰;有进行中症状则该格底色淡 amber)。今天高亮。
- 顶部标题「健康日历」+ 右侧「本月 N 天有记录 ›」。
- 整卡可点 → `fullScreenCover(CalendarOverviewView())`;点某一天 → 带 `initialDate` 进入。
- 样式走 `.tjCard()`,放在主页 `greeting` 之后、`TodayRemindersCard` 之前。

### 4.3 `HomeView` 改动

`body` 的 VStack 在 `greeting` 后插入 `HomeCalendarCard()`。其余不动。

## 5. UI:趋势 Tab 重构

### 5.1 `TrendsView`(重写)

移除所有日历相关代码(已迁到主页)。新结构:

- header「趋势」。
- 若无可成趋势的桶 → 空状态(「还没有可成趋势的指标 / 同一指标记录满 2 次后会出现在这里」)。
- 否则两段:
  - **长期监测**(`kind == .monitor`):标题 + 计数。
  - **化验指标趋势**(`kind == .lab`):标题 + 计数。
  - 每段 `ForEach` 渲染 `TrendRow`,点击 push/present `TrendDetailView`。
- 导航:`TrendsView` 包 `NavigationStack`,行用 `NavigationLink` 进详情(趋势 Tab 当前无 NavigationStack,新增之)。

### 5.2 `TrendRow`(新文件 `Features/Trends/TrendRow.swift`)

紧凑行:

- 左:指标名 + 「N 条 · 近 X 个月」副标题。
- 中:mini sparkline(小号 `Chart`,height≈36,无坐标轴,单/双线,异常点红)。
- 右:最新值 + 单位(异常红)+ chevron。
- `.tjCard(bordered: true)`。

### 5.3 `TrendDetailView`(新文件 `Features/Trends/TrendDetailView.swift`)

接收 `bucket: SeriesBucket`,自带 `@Query` 用于数据点→来源跳转。

- **大图表**(height≈220):复用 `SeriesChartCard` 的绘制逻辑(参考范围带 + catmullRom 折线 + 点 + 双线图例),但加坐标轴、按所选时间范围裁剪 domain。
- **时间范围 chips**:全部 / 近1年 / 近6月 / 近3月(仅当跨度 > 该范围才显示对应 chip)。切换裁剪图表点 + 重算 domain + 重算统计。
- **统计摘要卡**:最新值(带状态)/ 对比上次(Δ 绝对值+百分比+升降箭头,跨参考范围边界标红)/ 最低 / 最高 / 平均 / 记录数 / 时间跨度。文案模板拼装,不走 LLM。
- **AI 解读占位**:一行灰字「AI 解读即将上线」(预留,不接 LLM)。
- **数据点列表**(倒序):日期 + 值+单位 + 状态箭头/徽章;`onTapGesture` → `DayDetailSheet(date:)`(复用现有 sheet,给出当天来源上下文)。
- 标题 = bucket.title。

血压(双线)在详情页:统计摘要按「收缩/舒张」分别给最新值;列表每行显示「收缩/舒张」两值。

## 6. 受影响文件清单

**新增**
- `Features/Calendar/CalendarOverviewView.swift`
- `Features/Home/HomeCalendarCard.swift`
- `Features/Trends/TrendRow.swift`
- `Features/Trends/TrendDetailView.swift`

**修改**
- `Features/Trends/SeriesBucket.swift`(加 kind / sourceIndicatorIDs / name 段 / parseRange）
- `Features/Trends/TrendsView.swift`(删日历,重写为趋势列表 + NavigationStack)
- `Features/Home/HomeView.swift`(插入 HomeCalendarCard)
- `康康.xcodeproj/project.pbxproj`(新文件加入 target — 若用 file-system-synchronized group 则免改;需确认)

**不改**:`CalendarMonthGrid/YearGrid/Markers/DayDetailSheet`、`SeriesChartCard`(详情页复用其绘制思路,可抽 helper 或直接内置)、Models、xcstrings、RootView/TabBar、录入流程。

## 7. 验证

- 构建无错误/无新警告(`DEVELOPER_DIR` 指完整 Xcode,touch 强制重编 — 见记忆 build-from-cli)。
- 主页:日历卡显示当前周标记;点卡进总览;月/年切换;点某天→当日详情正确。
- 趋势:制造同名指标 ≥2 条(如手动录两次「血红蛋白」或两份报告同含一项)→ 出现在「化验指标趋势」段;预设监测仍在「长期监测」段;详情图表/统计/数据点跳转正确;血压双线正常。
- 空状态:全新库时两个页面都给出友好空态。

## 8. 风险与回退

- **range 解析覆盖不全**:单边区间("<5.2")暂不画带,图仍可用 —— 可接受,后续增强。
- **lab 段噪声**:同名但单位不同的指标会分成两桶(key 含 unit)—— 正确行为。若用户名字录入不一致(「血红蛋白」vs「Hb」)会分开 —— demo 可接受,不做模糊归并。
- **pbxproj**:若新文件未自动入 target,构建会报 missing symbol;届时手动加 build file 引用。
