# 趋势大改 + 健康日历移至主页 — 设计文档 > 日期: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? // 支持 "3.9-6.1" / "3.9~6.1" / "3.9 - 6.1";单边("<5.2"/">40"/"≤120")暂返回 nil(图不画带,正常) static func parseRange(_ raw: String) -> ClosedRange? ``` > **去重**:有 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 引用。