Files
kangkang/康康/Features/Trends/SeriesBucket.swift
link2026 37b47b2076 docs(claude): sync §5/§7/§10 with Monitor+Profile; fix SeriesBucket SwiftData import
- §5 schema 重写为 7 @Model 完整列表(含 UserProfile + Indicator.seriesKey)
- §7 IA 改成 5 槽 TabBar(2 内容 + 中间 + + 2 设置),记录入口 5 个 kind
- §10.6 红线例外清单加 Monitor + Profile(Symptom 也补上)
- SeriesBucket.swift 缺 import SwiftData(persistentModelID 报错)

全套测试 50 case pass / 0 fail / 0 warning。
2026-05-26 07:53:16 +08:00

154 lines
5.1 KiB
Swift

import SwiftUI
import SwiftData
import Foundation
/// Trends 线
/// (//...)= 1 SeriesLine; = + 2 线
struct SeriesBucket: Identifiable {
let id: String
let title: String
let unit: String
let lines: [SeriesLine]
let latestDate: Date
struct SeriesLine: Identifiable {
let id: String
let seriesKey: String
let label: String?
let color: Color
let points: [Point]
let referenceRange: ClosedRange<Double>?
var latestPoint: Point? { points.last }
}
struct Point: Identifiable, Hashable {
let id: String
let date: Date
let value: Double
let status: IndicatorStatus
}
}
extension SeriesBucket {
/// Indicator( seriesKey ) SeriesBucket
/// seriesKey capturedAt ;BP key bucket;
/// `minPoints` , 2(线)
static func build(from indicators: [Indicator],
profile: UserProfile? = nil,
minPoints: Int = 2) -> [SeriesBucket] {
var buckets: [String: [Indicator]] = [:]
for i in indicators {
guard let key = i.seriesKey, !key.isEmpty else { continue }
buckets[key, default: []].append(i)
}
//
let bpKeys: Set<String> = ["bp.systolic", "bp.diastolic"]
let bpIndicators = bpKeys.flatMap { buckets[$0] ?? [] }
let bpHasEnoughPoints = bpIndicators.filter { $0.seriesKey == "bp.systolic" }.count >= minPoints
var results: [SeriesBucket] = []
if bpHasEnoughPoints {
results.append(buildBP(from: buckets, profile: profile))
}
for k in bpKeys { buckets.removeValue(forKey: k) }
for (key, items) in buckets {
guard items.count >= minPoints else { continue }
if let bucket = buildSingle(key: key, items: items, profile: profile) {
results.append(bucket)
}
}
return results.sorted { $0.latestDate > $1.latestDate }
}
private static func buildSingle(key: String,
items: [Indicator],
profile: UserProfile?) -> SeriesBucket? {
let sorted = items.sorted { $0.capturedAt < $1.capturedAt }
guard let latest = sorted.last else { return nil }
let metric = monitorMetric(for: key)
let field = metric?.fields.first { $0.seriesKey == key }
let title = metric?.displayName ?? sorted.first?.name ?? key
let unit = field?.unit ?? sorted.first?.unit ?? ""
let range = field.flatMap { metric?.effectiveRange(for: $0, profile: profile) }
let line = SeriesLine(
id: key,
seriesKey: key,
label: nil,
color: Tj.Palette.ink,
points: sorted.compactMap { point(from: $0) },
referenceRange: range
)
return SeriesBucket(
id: key,
title: title,
unit: unit,
lines: [line],
latestDate: latest.capturedAt
)
}
private static func buildBP(from buckets: [String: [Indicator]],
profile: UserProfile?) -> SeriesBucket {
let m = MonitorMetric.bloodPressure
let sysField = m.fields[0]
let diaField = m.fields[1]
let sysItems = (buckets["bp.systolic"] ?? []).sorted { $0.capturedAt < $1.capturedAt }
let diaItems = (buckets["bp.diastolic"] ?? []).sorted { $0.capturedAt < $1.capturedAt }
let sysLine = SeriesLine(
id: "bp.systolic",
seriesKey: "bp.systolic",
label: "收缩",
color: Tj.Palette.brick,
points: sysItems.compactMap { point(from: $0) },
referenceRange: m.effectiveRange(for: sysField, profile: profile)
)
let diaLine = SeriesLine(
id: "bp.diastolic",
seriesKey: "bp.diastolic",
label: "舒张",
color: Tj.Palette.leaf,
points: diaItems.compactMap { point(from: $0) },
referenceRange: m.effectiveRange(for: diaField, profile: profile)
)
let latest = max(
sysItems.last?.capturedAt ?? .distantPast,
diaItems.last?.capturedAt ?? .distantPast
)
return SeriesBucket(
id: "bp",
title: "血压",
unit: "mmHg",
lines: [sysLine, diaLine],
latestDate: latest
)
}
private static func point(from i: Indicator) -> Point? {
guard let v = Double(i.value.trimmingCharacters(in: .whitespaces)) else { return nil }
return Point(
id: "\(i.persistentModelID)",
date: i.capturedAt,
value: v,
status: i.status
)
}
private static func monitorMetric(for seriesKey: String) -> MonitorMetric? {
MonitorMetric.allCases.first { m in
m.fields.contains { $0.seriesKey == seriesKey }
}
}
}