docs(spec): add Monitor + Profile design v1 (approved)
long-term formatted indicators(.indicator 入口预设 + 自由)+ 个人资料 (年龄/性别/身高/血型/健康背景/用药)+ Profile-aware reference range (老人血压 90-150 替代 90-140)。详见 spec §2-§5。
This commit is contained in:
434
docs/superpowers/specs/2026-05-26-monitor-and-profile-design.md
Normal file
434
docs/superpowers/specs/2026-05-26-monitor-and-profile-design.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# Monitor + Profile · 设计 v1
|
||||||
|
|
||||||
|
> 长期格式化指标录入(`.indicator` 入口预设 + 自由)+ 个人资料(年龄、性别、健康背景、用药)
|
||||||
|
>
|
||||||
|
> 日期:2026-05-26 · 状态:approved by user,进入实施
|
||||||
|
> 关联:[CLAUDE.md](../../../CLAUDE.md) §5 §7 §10;[W2 retro](../retros/2026-05-31-w2.md) 计划外完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
### 1.1 当前缺口
|
||||||
|
|
||||||
|
康康现有的 4 个记录 kind(`quick` 拍照、`archive` 归档、`diary` 文字、`symptom` 持续症状)都是**事件型**——一次性记录,不假设后续会重复同一指标。但血压/血糖/体重这类**长期监测**类需求:
|
||||||
|
|
||||||
|
- 用户每天/每周测,数值规律地重复
|
||||||
|
- 需要趋势(W4-W5 计划的 Trends 页)
|
||||||
|
- 不需要拍照(已是格式化数字)
|
||||||
|
- 参考范围依赖个人 demographic(老人血压标准放宽)
|
||||||
|
|
||||||
|
同时,App 启动以来一直没有用户基础信息持久化的位置。LLM 给出趋势解读时缺乏 demographic context("LDL 偏高"对 35 岁健康男和 70 岁糖尿病患者风险完全不同)。
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
|
||||||
|
- **一个统一的"手动录入指标"入口**:用户已加 `.indicator` case,本设计把 7 个预设(血压/血糖/体重/...)和「自由输入」合并进这个 sheet
|
||||||
|
- **个人资料卡**:在「我的」加一张资料卡,push 进 Form 编辑页,4 项核心 + 健康背景 + 用药
|
||||||
|
- **联动**:参考范围按 Profile 个性化(目前规则只覆盖"老人血压"一例,后续可扩)
|
||||||
|
|
||||||
|
### 1.3 非目标(YAGNI)
|
||||||
|
|
||||||
|
- ❌ Trends 页升级(本次只打通数据通路,留给 W4-W5)
|
||||||
|
- ❌ 提醒/通知功能(到点测量推送)
|
||||||
|
- ❌ HealthKit 导入
|
||||||
|
- ❌ 多 Profile / 给家人记
|
||||||
|
- ❌ AppLock / Face ID(W5 末统一实现)
|
||||||
|
- ❌ 单位切换(kg/lb,mmol/L vs mg/dL)
|
||||||
|
- ❌ 紧急联系人
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 数据模型
|
||||||
|
|
||||||
|
### 2.1 Indicator 扩字段
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@Model final class Indicator {
|
||||||
|
// 现有字段不动:name/value/unit/range/statusRaw/note/capturedAt/report/asset/pinned
|
||||||
|
|
||||||
|
var seriesKey: String? // 新增。预设录入填 "bp.systolic" / "glucose.fasting" / ...
|
||||||
|
// 自由输入和 VL/Report 来源的 Indicator 都为 nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么用 String 而非 enum**:`seriesKey` 跨设备/版本要稳定,enum 改名会破坏老数据;String 用命名空间约定(`bp.*` / `glucose.*`)即可。
|
||||||
|
|
||||||
|
**为什么不新建 @Model**:复用 Indicator 让 Trends/Timeline/ReportCompareService 一次写完受益,避免分裂查询路径。
|
||||||
|
|
||||||
|
### 2.2 UserProfile @Model
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@Model final class UserProfile {
|
||||||
|
// —— 核心 4 项 ——
|
||||||
|
var birthYear: Int? // 1990 等。隐私考虑只存"年",不存月日
|
||||||
|
var biologicalSexRaw: String // "" / "male" / "female"
|
||||||
|
var heightCM: Int?
|
||||||
|
var bloodTypeRaw: String // "" / "A" / "B" / "AB" / "O"
|
||||||
|
|
||||||
|
// —— 健康背景 ——
|
||||||
|
var allergies: [String] // 自由文本数组
|
||||||
|
var chronicConditions: [String] // 预设 + 自定义混合
|
||||||
|
var familyHistory: [String] // 自由文本数组
|
||||||
|
|
||||||
|
// —— 当前用药 ——
|
||||||
|
var currentMedications: [String]
|
||||||
|
|
||||||
|
var updatedAt: Date
|
||||||
|
|
||||||
|
init(birthYear: Int? = nil, /* ... */) { /* ... */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserProfile {
|
||||||
|
enum Sex: String { case male, female, undisclosed = "" }
|
||||||
|
var sex: Sex { Sex(rawValue: biologicalSexRaw) ?? .undisclosed }
|
||||||
|
|
||||||
|
/// 当前年龄(无 birthYear 时 nil)
|
||||||
|
var age: Int? {
|
||||||
|
guard let y = birthYear else { return nil }
|
||||||
|
return Calendar.current.component(.year, from: .now) - y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 单例策略
|
||||||
|
|
||||||
|
UserProfile 全 App 单一实例,通过 helper 保证:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum UserProfileStore {
|
||||||
|
@MainActor
|
||||||
|
static func loadOrCreate(in ctx: ModelContext) -> UserProfile {
|
||||||
|
let descriptor = FetchDescriptor<UserProfile>()
|
||||||
|
if let existing = try? ctx.fetch(descriptor).first { return existing }
|
||||||
|
let new = UserProfile()
|
||||||
|
ctx.insert(new)
|
||||||
|
try? ctx.save()
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
任何 View 用 `@Query` 拉,空了再调 loadOrCreate。MeView 启动时调一次,确保后续 @Query 必拿到。
|
||||||
|
|
||||||
|
### 2.4 Schema 注册
|
||||||
|
|
||||||
|
`KangkangApp.swift` 的 schema 加入 `UserProfile.self`。Indicator 加字段是 additive change,SwiftData 自动迁移(给老 row 的 seriesKey 填 nil)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. MonitorMetric Catalog
|
||||||
|
|
||||||
|
`Features/Monitor/MonitorMetric.swift`,8 个预设(血压算 1 个 case,内部展开 2 条 Indicator):
|
||||||
|
|
||||||
|
```swift
|
||||||
|
enum MonitorMetric: String, CaseIterable, Identifiable {
|
||||||
|
case bloodPressure // bp.systolic + bp.diastolic
|
||||||
|
case fastingGlucose // glucose.fasting
|
||||||
|
case postprandialGlucose // glucose.postprandial
|
||||||
|
case weight // weight
|
||||||
|
case temperature // temperature
|
||||||
|
case heartRate // heart_rate
|
||||||
|
case spo2 // spo2
|
||||||
|
case height // height(录入后回写 UserProfile.heightCM)
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: String { /* "血压" / "空腹血糖" / ... */ }
|
||||||
|
var icon: String { /* SF Symbol */ }
|
||||||
|
var fields: [Field] // 1 或 2 个字段定义
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MonitorMetric {
|
||||||
|
struct Field {
|
||||||
|
let seriesKey: String // 如 "bp.systolic"
|
||||||
|
let label: String // 如 "收缩压"
|
||||||
|
let unit: String // 如 "mmHg"
|
||||||
|
let placeholder: String // 如 "120"
|
||||||
|
let baseRange: ClosedRange<Double>? // nil 表示不算 status(体重/身高)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返回该 metric 在给定 profile 下的参考范围(可能跟 baseRange 不同)
|
||||||
|
func effectiveRange(for field: Field, profile: UserProfile?) -> ClosedRange<Double>? {
|
||||||
|
// 目前唯一规则:bp 在 age >= 65 时上限放宽到 150 / 90
|
||||||
|
if let age = profile?.age, age >= 65,
|
||||||
|
field.seriesKey == "bp.systolic" {
|
||||||
|
return 90...150
|
||||||
|
}
|
||||||
|
if let age = profile?.age, age >= 65,
|
||||||
|
field.seriesKey == "bp.diastolic" {
|
||||||
|
return 60...90 // 暂不调
|
||||||
|
}
|
||||||
|
return field.baseRange
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据值算 status(value 在范围内 → normal,上 → high,下 → low,无范围 → normal)
|
||||||
|
static func status(value: Double, in range: ClosedRange<Double>?) -> IndicatorStatus {
|
||||||
|
guard let r = range else { return .normal }
|
||||||
|
if value > r.upperBound { return .high }
|
||||||
|
if value < r.lowerBound { return .low }
|
||||||
|
return .normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Profile-aware 规则
|
||||||
|
|
||||||
|
本次仅实现 1 条规则(老人收缩压上限 140→150),目的是**展示联动机制**,不追求医学完备。未来扩规则只改 `effectiveRange` 函数,不动调用方。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. UI
|
||||||
|
|
||||||
|
### 4.1 IndicatorRecordSheet(替代之前提的 MonitorRecordSheet)
|
||||||
|
|
||||||
|
`Features/Indicator/IndicatorRecordSheet.swift`,被 RootView 在 `.indicator` case 弹出。
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
|
||||||
|
```
|
||||||
|
[拖动条]
|
||||||
|
"记录指标 · 本地处理"
|
||||||
|
|
||||||
|
[2 列 grid]
|
||||||
|
┌─────────┐ ┌─────────┐
|
||||||
|
│ 血压 │ │ 空腹血糖│
|
||||||
|
│ 收/舒 │ │ 3.9-6.1 │
|
||||||
|
└─────────┘ └─────────┘
|
||||||
|
┌─────────┐ ┌─────────┐
|
||||||
|
│ 体重 │ │ 体温 │
|
||||||
|
└─────────┘ └─────────┘
|
||||||
|
... (共 7 预设)
|
||||||
|
┌─────────┐ ┌─────────┐
|
||||||
|
│ 心率 │ │ + 自由 │
|
||||||
|
└─────────┘ └─────────┘
|
||||||
|
|
||||||
|
—— 选中 metric 后,grid 下方展开 ——
|
||||||
|
|
||||||
|
【血压】参考范围:90-140 / 60-90 mmHg(成人通用)
|
||||||
|
[收缩压 _____ mmHg]
|
||||||
|
[舒张压 _____ mmHg]
|
||||||
|
status chip 实时显示
|
||||||
|
|
||||||
|
[保存按钮]
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键交互**:
|
||||||
|
- 进入 sheet 时无选中,grid 全展示
|
||||||
|
- 点预设 → 高亮卡片 + 下方展开输入区
|
||||||
|
- 切换 metric → 数值清空(避免血压数值串到血糖)
|
||||||
|
- 选「+ 自由输入」→ 展开 4 个字段:名称 / 数值 / 单位 / 参考范围(string)
|
||||||
|
- 保存:
|
||||||
|
- 血压 → 2 条 Indicator(同 capturedAt + 各自 seriesKey)
|
||||||
|
- 单字段预设 → 1 条 Indicator(seriesKey 填)
|
||||||
|
- 身高预设 → 1 条 Indicator + 回写 UserProfile.heightCM
|
||||||
|
- 自由输入 → 1 条 Indicator(seriesKey 为 nil,name 用户输入)
|
||||||
|
|
||||||
|
**Profile-aware 提示**:
|
||||||
|
- 若 `effectiveRange` 跟 `baseRange` 不同,参考范围一行末尾小字:"按你的年龄(67)调整"
|
||||||
|
- 若 `effectiveRange` 与 baseRange 相同 / 无 Profile,正常显示
|
||||||
|
|
||||||
|
### 4.2 MeView 改造
|
||||||
|
|
||||||
|
```
|
||||||
|
[ScrollView]
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 个人资料 更多 →│
|
||||||
|
│ 38岁 · 男 · 175cm · A型 │
|
||||||
|
│ (未设置时:"点这里完善你的资料") │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
↓ tap push
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 模型管理 未配置 → │ (W6 stub)
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Face ID 启动锁 关闭 → │ (W5 stub)
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 关于 → │ (链接到隐私承诺 placeholder)
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
DebugAIRunner
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
stub 卡片本次只放占位 + 文案,push 进去是空页或 placeholder。
|
||||||
|
|
||||||
|
### 4.3 ProfileEditView
|
||||||
|
|
||||||
|
`Features/Profile/ProfileEditView.swift`,Form 风格:
|
||||||
|
|
||||||
|
```
|
||||||
|
导航标题:个人资料
|
||||||
|
|
||||||
|
—— 基本 ——
|
||||||
|
出生年份 [picker 1900-2026]
|
||||||
|
性别 [男 / 女 / 不愿透露 segmented]
|
||||||
|
身高 [TextField + cm]
|
||||||
|
血型 [A / B / AB / O / 不知道 picker]
|
||||||
|
|
||||||
|
—— 健康背景 ——
|
||||||
|
过敏史 [chips + add field]
|
||||||
|
慢病 [8 预设 chips 多选 + 自定义 add]
|
||||||
|
家族史 [chips + add field]
|
||||||
|
|
||||||
|
—— 当前用药 ——
|
||||||
|
[列表 + add row + 行内 swipe-to-delete]
|
||||||
|
|
||||||
|
(保存即时,无显式 Save 按钮——边改边写)
|
||||||
|
```
|
||||||
|
|
||||||
|
慢病 8 预设:`高血压 / 糖尿病 / 冠心病 / 高血脂 / 甲状腺疾病 / 哮喘 / 慢性肾病 / 抑郁/焦虑`
|
||||||
|
|
||||||
|
### 4.4 Timeline 行内合并(顺手)
|
||||||
|
|
||||||
|
`Features/Timeline/TimelineEntry.swift`,`from(indicator:)` 增加配对逻辑:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
static func from(indicators: [Indicator]) -> [TimelineEntry] {
|
||||||
|
// 旧版只 map,新版需要查找 bp.systolic 配对 bp.diastolic
|
||||||
|
// 算法:同 capturedAt(到秒)+ bp.* prefix → 合并;其他单条直接 map
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ArchiveListView 和 HomeView 的 `mapped` 表达式从 `indicators.map(...)` 改为 `TimelineEntry.from(indicators:)`(批处理)。
|
||||||
|
|
||||||
|
合并后的 TimelineEntry:
|
||||||
|
- title: "血压"
|
||||||
|
- subtitle: "120 / 80 mmHg"
|
||||||
|
- trailing: 异常时显示"偏高"或"正常"
|
||||||
|
|
||||||
|
非 bp.* 的 series 不合并,逐条显示("空腹血糖 5.4 mmol/L" / "体重 68 kg")。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 联动:Profile ↔ Monitor
|
||||||
|
|
||||||
|
### 5.1 调用路径
|
||||||
|
|
||||||
|
```
|
||||||
|
IndicatorRecordSheet
|
||||||
|
↓ @Query UserProfile (单例)
|
||||||
|
MonitorMetric.effectiveRange(for: field, profile: profile)
|
||||||
|
↓
|
||||||
|
- 显示个性化参考范围
|
||||||
|
- 保存时 MonitorMetric.status(value:, in: effectiveRange) 算 statusRaw
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 未来扩展点
|
||||||
|
|
||||||
|
`effectiveRange` 是唯一规则入口,扩规则只动这个函数。规则示例(本次不实现):
|
||||||
|
- 性别 → 血红蛋白、肌酐参考范围不同
|
||||||
|
- 慢病 → 糖尿病患者血糖目标更严
|
||||||
|
- 年龄分段 → 儿童体温、心率范围
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 测试
|
||||||
|
|
||||||
|
### 6.1 新建 `康康Tests/UserProfileTests.swift`
|
||||||
|
|
||||||
|
- `freshProfileHasNilDemographics()` — 新建 profile,字段都 nil/空数组
|
||||||
|
- `ageComputedFromBirthYear()` — 1985 → 41 岁(2026 当前年)
|
||||||
|
- `sexParsesEnumFromRaw()` — male/female/空 → 三种 enum
|
||||||
|
- `loadOrCreateReturnsExistingSingleton()` — 第二次 call 不创建新 row
|
||||||
|
- `arrayFieldsRoundtripThroughSwiftData()` — chronicConditions 存读
|
||||||
|
|
||||||
|
### 6.2 新建 `康康Tests/MonitorMetricTests.swift`
|
||||||
|
|
||||||
|
- `allMetricsHaveAtLeastOneField()`
|
||||||
|
- `bpHasTwoFields()`
|
||||||
|
- `statusHighWhenAboveUpper()` / `statusLowWhenBelowLower()` / `statusNormalWhenInside()` / `statusNormalWhenRangeNil()`
|
||||||
|
- `bpUpperBoundShiftsForElderly()` — age 67 时 bp.systolic 上限 = 150
|
||||||
|
- `bpUpperBoundUnchangedWhenNoProfile()` — profile 为 nil 时上限 = 140
|
||||||
|
- `nonBPSeriesUnaffectedByProfile()` — 血糖范围不随年龄变
|
||||||
|
|
||||||
|
### 6.3 扩 `康康Tests/ModelsSchemaTests.swift`
|
||||||
|
|
||||||
|
- `userProfileSchemaPersistsAcrossSave()`
|
||||||
|
- `indicatorSeriesKeyRoundtrip()`
|
||||||
|
- `cascadeStillWorksWithSeriesKey()` — Report 删除时,关联 Indicator(无论 seriesKey)都删
|
||||||
|
|
||||||
|
### 6.4 扩 `康康Tests/TimelineGroupingTests.swift`
|
||||||
|
|
||||||
|
- `bpSystolicAndDiastolicMergeIntoSingleEntry()`
|
||||||
|
- `nonBPSeriesStayAsSeparateEntries()`
|
||||||
|
- `bpAtDifferentTimesDoNotMerge()` — capturedAt 差 > 5 秒不合并
|
||||||
|
|
||||||
|
预期总测试数:11(profile 5)+ 7(metric)+ 3(schema)+ 3(timeline)= 18 个新测试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 不变项与守恒检查
|
||||||
|
|
||||||
|
- ✅ §10.1 不引入云服务 — 完全本地
|
||||||
|
- ✅ §10.2 不自实现密码学 — SwiftData store 已有 file protection
|
||||||
|
- ✅ §10.3 UI 不直接调 AIRuntime — 本设计不涉及 AI
|
||||||
|
- ✅ §10.4 AIRuntime actor — 不涉及
|
||||||
|
- ✅ §10.5 VL/LLM prompt — 不涉及
|
||||||
|
- ⚠️ §10.6 新功能必须问"清单里有吗" — Monitor 和 Profile 都是清单外,**已跟用户确认加入**
|
||||||
|
- ✅ §10.7 不重构现有骨架 — 不动 RootView / RecordSheet 骨架(只补 case 处理),不动 DesignSystem
|
||||||
|
- ✅ §10.8 C2 ≠ B3 — 不涉及
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 文件清单
|
||||||
|
|
||||||
|
### 新建(6)
|
||||||
|
|
||||||
|
| 路径 | 职责 |
|
||||||
|
|---|---|
|
||||||
|
| `康康/Models/UserProfile.swift` | UserProfile @Model + Sex enum + age computed + loadOrCreate helper |
|
||||||
|
| `康康/Features/Monitor/MonitorMetric.swift` | 8 metric catalog + effectiveRange + status 算法 |
|
||||||
|
| `康康/Features/Indicator/IndicatorRecordSheet.swift` | 预设 grid + 自由输入合一的录入 sheet |
|
||||||
|
| `康康/Features/Profile/ProfileEditView.swift` | Form 编辑页 |
|
||||||
|
| `康康Tests/UserProfileTests.swift` | 5 测试 |
|
||||||
|
| `康康Tests/MonitorMetricTests.swift` | 7 测试 |
|
||||||
|
|
||||||
|
### 修改(7)
|
||||||
|
|
||||||
|
| 路径 | 改什么 |
|
||||||
|
|---|---|
|
||||||
|
| `康康/Models/Models.swift` | Indicator 加 `seriesKey: String?`,初始化器加默认值 nil |
|
||||||
|
| `康康/App/KangkangApp.swift` | schema 加 `UserProfile.self` |
|
||||||
|
| `康康/Features/Me/MeView.swift` | 加 ProfileCard + 3 个 stub 卡片 |
|
||||||
|
| `康康/RootView.swift` | `.indicator` case 接 IndicatorRecordSheet 弹出 |
|
||||||
|
| `康康/Features/Timeline/TimelineEntry.swift` | 加 `from(indicators:)` 批处理 + bp 配对 |
|
||||||
|
| `康康Tests/ModelsSchemaTests.swift` | 3 个新测试 |
|
||||||
|
| `康康Tests/TimelineGroupingTests.swift` | 3 个新测试 |
|
||||||
|
|
||||||
|
### 文档(2)
|
||||||
|
|
||||||
|
| 路径 | 改什么 |
|
||||||
|
|---|---|
|
||||||
|
| `CLAUDE.md` | §5 加 UserProfile @Model + Indicator seriesKey;§7 IA 加 Profile 入口;§11 时间表加备注;§10.6 例外清单加 Monitor + Profile |
|
||||||
|
| `docs/superpowers/specs/2026-05-26-monitor-and-profile-design.md` | 本文件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 验收
|
||||||
|
|
||||||
|
- [ ] App build & test 全绿,0 警告
|
||||||
|
- [ ] DEBUG 启动 → 我的 → 个人资料 → 填年龄 + 性别 + 身高 + 血型,push back 显示在 ProfileCard
|
||||||
|
- [ ] DEBUG 启动 → + 号 → 指标记录 → 选血压 → 输 145/85 → 保存 → 在首页时间线看到合并的"血压 145/85"行
|
||||||
|
- [ ] 把 UserProfile birthYear 改成 1955(70 岁) → 再次进血压录入 → 顶部小字显示"按你的年龄(70)调整",参考范围 90-150 / 60-90
|
||||||
|
- [ ] 录入身高 175 → 个人资料卡片自动显示 175cm
|
||||||
|
- [ ] 18 个新测试全绿
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 估时
|
||||||
|
|
||||||
|
- 数据层(UserProfile + Indicator.seriesKey + schema 注册):20 分钟
|
||||||
|
- MonitorMetric catalog + effectiveRange:20 分钟
|
||||||
|
- IndicatorRecordSheet UI:25 分钟
|
||||||
|
- ProfileEditView + MeView 改造:25 分钟
|
||||||
|
- Timeline 合并:15 分钟
|
||||||
|
- 18 测试:30 分钟
|
||||||
|
- CLAUDE.md + 提交整理:15 分钟
|
||||||
|
|
||||||
|
**总计 ~150 分钟**(2.5 小时)。
|
||||||
Reference in New Issue
Block a user