import SwiftUI import SwiftData import Foundation enum TimelineKind: String, CaseIterable, Identifiable { case indicator, report, symptom, diary var id: String { rawValue } var label: String { switch self { case .indicator: return String(appLoc: "指标") case .report: return String(appLoc: "报告") case .symptom: return String(appLoc: "症状") case .diary: return String(appLoc: "日记") } } var icon: String { switch self { case .indicator: return "drop.fill" case .report: return "doc.fill" case .symptom: return "waveform.path.ecg" case .diary: return "pencil" } } var accent: Color { switch self { case .indicator: return Tj.Palette.brick case .report: return Tj.Palette.ink2 case .symptom: return Tj.Palette.amber case .diary: return Tj.Palette.leaf } } } struct TimelineEntry: Identifiable, Hashable { let id: String let kind: TimelineKind let date: Date let title: String let subtitle: String let trailing: String? let trailingIsAlert: Bool let isOngoing: Bool static func from(indicator i: Indicator) -> TimelineEntry { TimelineEntry( id: "indicator-\(i.persistentModelID)", kind: .indicator, date: i.capturedAt, title: i.name, subtitle: typeSubtitle(for: i), trailing: indicatorValue(i), trailingIsAlert: i.status != .normal, isOngoing: false ) } /// 批处理 Indicator 列表,把 bp.systolic + bp.diastolic 同 capturedAt 合并成 /// 一条 "血压 120/80 mmHg" timeline entry。其他 series 逐条 from(indicator:)。 /// 合并条件:capturedAt 差 ≤ 5 秒(防止跨次混淆)。 static func from(indicators: [Indicator]) -> [TimelineEntry] { var entries: [TimelineEntry] = [] var consumed = Set() // 先找 bp.systolic,配 bp.diastolic for sys in indicators where sys.seriesKey == "bp.systolic" { if consumed.contains(sys.persistentModelID) { continue } guard let dia = indicators.first(where: { $0.seriesKey == "bp.diastolic" && !consumed.contains($0.persistentModelID) && abs($0.capturedAt.timeIntervalSince(sys.capturedAt)) <= 5 }) else { continue } consumed.insert(sys.persistentModelID) consumed.insert(dia.persistentModelID) entries.append(mergedBP(systolic: sys, diastolic: dia)) } // 剩下的 indicator(含未配对的 systolic/diastolic、其他 series、自由输入) for i in indicators where !consumed.contains(i.persistentModelID) { entries.append(from(indicator: i)) } return entries } private static func mergedBP(systolic sys: Indicator, diastolic dia: Indicator) -> TimelineEntry { let abnormal = sys.status != .normal || dia.status != .normal return TimelineEntry( id: "bp-\(sys.persistentModelID)-\(dia.persistentModelID)", kind: .indicator, date: sys.capturedAt, title: String(appLoc: "血压"), subtitle: String(appLoc: "长期监测"), trailing: "\(sys.value)/\(dia.value) mmHg" + (abnormal ? " ↑" : ""), trailingIsAlert: abnormal, isOngoing: false ) } static func from(report r: Report) -> TimelineEntry { let abnormal = r.indicators.filter { $0.status != .normal }.count return TimelineEntry( id: "report-\(r.persistentModelID)", kind: .report, date: r.reportDate, title: r.title, subtitle: "\(r.type.label) · " + String(appLoc: "共 \(r.pageCount) 页"), trailing: abnormal > 0 ? String(appLoc: "\(abnormal) 项偏高") : nil, trailingIsAlert: abnormal > 0, isOngoing: false ) } static func from(diary d: DiaryEntry) -> TimelineEntry { TimelineEntry( id: "diary-\(d.persistentModelID)", kind: .diary, date: d.createdAt, title: d.content.firstLine(), subtitle: String(appLoc: "文字日记"), trailing: nil, trailingIsAlert: false, isOngoing: false ) } static func from(symptom s: Symptom) -> TimelineEntry { let ongoing = s.isOngoing let date = s.endedAt ?? s.startedAt let subtitle: String let trailing: String? if ongoing { subtitle = String(appLoc: "症状 · 持续中") trailing = String(appLoc: "持续 \(formatDuration(s.duration))") } else { subtitle = String(appLoc: "症状 · 已结束") trailing = String(appLoc: "持续 \(formatDuration(s.duration))") } return TimelineEntry( id: "symptom-\(s.persistentModelID)", kind: .symptom, date: date, title: s.name, subtitle: subtitle, trailing: trailing, trailingIsAlert: ongoing, isOngoing: ongoing ) } private static func typeSubtitle(for i: Indicator) -> String { if let report = i.report { return String(appLoc: "指标 · \(report.title)") } return String(appLoc: "异常项快拍") } private static func indicatorValue(_ i: Indicator) -> String { let unit = i.unit.isEmpty ? "" : " \(i.unit)" let arrow: String switch i.status { case .high: arrow = " ↑" case .low: arrow = " ↓" case .normal: arrow = "" } return "\(i.value)\(unit)\(arrow)" } } private extension String { func firstLine() -> String { let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) if let line = trimmed.split(whereSeparator: \.isNewline).first { let s = String(line) return s.count > 40 ? String(s.prefix(40)) + "…" : s } return trimmed.isEmpty ? String(appLoc: "(空日记)") : trimmed } }