Files
kangkang/康康/Features/Monitor/MonitorMetric.swift
link2026 9a6d21100b feat(monitor): add UserProfile + MonitorMetric catalog + Indicator.seriesKey
数据层(spec 2026-05-26):
- UserProfile @Model:核心 4 项 + 健康背景 + 用药,SwiftData 单例(loadOrCreate)
- Indicator 加 seriesKey: String?,标识长期指标分组('bp.systolic' 等)
- MonitorMetric enum 8 case:血压(2 field 拆 2 Indicator)/ 空腹+餐后血糖 /
  体重 / 体温 / 心率 / SpO2 / 身高
- effectiveRange(for:profile:) 实现 1 条 Profile-aware 规则:
  age >= 65 时 bp.systolic 上限 140→150
- KangkangApp schema 加 UserProfile.self

测试 17 个全绿(UserProfile 6 + MonitorMetric 11);schema 烟测扩 2(seriesKey roundtrip + UserProfile persist)。
UI 层 + Timeline 合并下个 commit。
2026-05-26 07:40:42 +08:00

163 lines
6.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// `IndicatorRecordSheet` grid `MonitorMetric.allCases`
///
/// metric 1 2 Field; 2 Indicator( capturedAt + seriesKey),
/// 1 `effectiveRange(for:profile:)` Profile ( 1 :
/// 140150)
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 {
switch self {
case .bloodPressure: return "血压"
case .fastingGlucose: return "空腹血糖"
case .postprandialGlucose: return "餐后血糖"
case .weight: return "体重"
case .temperature: return "体温"
case .heartRate: return "心率"
case .spo2: return "血氧"
case .height: return "身高"
}
}
/// SF Symbolgrid
var icon: String {
switch self {
case .bloodPressure: return "heart.fill"
case .fastingGlucose: return "drop.fill"
case .postprandialGlucose: return "drop.circle.fill"
case .weight: return "scalemass.fill"
case .temperature: return "thermometer.medium"
case .heartRate: return "waveform.path.ecg"
case .spo2: return "lungs.fill"
case .height: return "ruler.fill"
}
}
var fields: [Field] {
switch self {
case .bloodPressure:
return [
Field(seriesKey: "bp.systolic",
label: "收缩压",
unit: "mmHg",
placeholder: "120",
baseRange: 90...140),
Field(seriesKey: "bp.diastolic",
label: "舒张压",
unit: "mmHg",
placeholder: "80",
baseRange: 60...90),
]
case .fastingGlucose:
return [Field(seriesKey: "glucose.fasting",
label: "空腹血糖",
unit: "mmol/L",
placeholder: "5.0",
baseRange: 3.9...6.1)]
case .postprandialGlucose:
return [Field(seriesKey: "glucose.postprandial",
label: "餐后 2h",
unit: "mmol/L",
placeholder: "6.5",
baseRange: 0...7.8)]
case .weight:
return [Field(seriesKey: "weight",
label: "体重",
unit: "kg",
placeholder: "68",
baseRange: nil)]
case .temperature:
return [Field(seriesKey: "temperature",
label: "体温",
unit: "°C",
placeholder: "36.5",
baseRange: 36.0...37.2)]
case .heartRate:
return [Field(seriesKey: "heart_rate",
label: "心率",
unit: "bpm",
placeholder: "72",
baseRange: 60...100)]
case .spo2:
return [Field(seriesKey: "spo2",
label: "血氧",
unit: "%",
placeholder: "98",
baseRange: 95...100)]
case .height:
return [Field(seriesKey: "height",
label: "身高",
unit: "cm",
placeholder: "175",
baseRange: nil)]
}
}
}
extension MonitorMetric {
struct Field: Identifiable, Hashable {
let seriesKey: String
let label: String
let unit: String
let placeholder: String
let baseRange: ClosedRange<Double>?
var id: String { seriesKey }
/// IndicatorRecordSheet 90-140 mmHg
func rangeText(_ range: ClosedRange<Double>?) -> String {
guard let r = range else { return "无参考范围" }
let lower = format(r.lowerBound)
let upper = format(r.upperBound)
// baseRange 0...7.8,<7.8
if r.lowerBound == 0 { return "<\(upper) \(unit)" }
return "\(lower)\(upper) \(unit)"
}
private func format(_ v: Double) -> String {
v.truncatingRemainder(dividingBy: 1) == 0
? String(format: "%.0f", v)
: String(format: "%.1f", v)
}
}
/// field profile
/// 1 :age 65 bp.systolic 140 150
/// profile nil() baseRange
func effectiveRange(for field: Field, profile: UserProfile?) -> ClosedRange<Double>? {
if let age = profile?.age, age >= 65, field.seriesKey == "bp.systolic" {
return 90...150
}
return field.baseRange
}
/// effectiveRange , value status
/// value high; low; normal; 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
}
/// IndicatorRecordSheet (67):
/// effectiveRange baseRange true
func isRangePersonalized(for field: Field, profile: UserProfile?) -> Bool {
guard let p = profile else { return false }
let base = field.baseRange
let eff = effectiveRange(for: field, profile: p)
return base != eff
}
}