9.8 KiB
趋势大改 + 健康日历移至主页 — 设计文档
日期:2026-06-07 · 状态:已定方案(用户授权直接实现,免确认)
1. 背景与目标
当前「趋势」Tab(TrendsView.swift)把两件事混在一起:
- 健康日历(月/年视图 + 当日详情)—— 占据页面上半部分。
- 长期监测折线图(
seriesSection)—— 页面下半部分。
两个问题:
- 日历放错了地方。它是「总览记录情况」的入口,更适合放在主页(用户每天第一眼看的页面),而不是埋在趋势 Tab 里。
- 趋势能力太弱。
SeriesBucket.build只按seriesKey分桶,因此只有 8 个长期监测预设(血压/血糖/体温…)和自定义指标能成图。所有没有 seriesKey 的指标——报告里解析出来的化验项、VL 快拍、自由输入——即使在多份报告里反复出现(如「血红蛋白」体检了 3 次),也完全看不到趋势。
目标
- 健康日历移到主页:主页新增一张紧凑的「健康日历」卡(当前周的横条 + 本月记录摘要),点击展开完整的月/年总览页(可切月视图/年视图、看当日详情)。
- 趋势 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 区分两段
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 流程改为两段
- seriesKey 段(原逻辑,kind=.monitor):血压合并、单系列预设、自定义。这些桶里的 Indicator 标记为「已消费」。
- name 段(新,kind=.lab):对所有没有 seriesKey 的 Indicator,按
normalizedKey(name, unit)分桶;每桶 ≥minPoints才保留。参考范围从该桶最新一条 Indicator 的range字符串解析。 - 两段合并返回,各自按
latestDate倒序。详情/列表按kind分段。
// 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/presentTrendDetailView。
- 长期监测(
- 导航:
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.swiftFeatures/Home/HomeCalendarCard.swiftFeatures/Trends/TrendRow.swiftFeatures/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 引用。