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 "指标" case .report: return "报告" case .symptom: return "症状" case .diary: return "日记" } } 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 ) } 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) · 共 \(r.pageCount) 页", trailing: abnormal > 0 ? "\(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: "文字日记", 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 = "症状 · 持续中" trailing = "持续 \(formatDuration(s.duration))" } else { subtitle = "症状 · 已结束" trailing = "持续 \(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 "指标 · \(report.title)" } return "异常项快拍" } 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 ? "(空日记)" : trimmed } }