import SwiftUI import SwiftData struct HomeView: View { /// 跳记录页;传 filter 时预选对应分类 chip(报告档案卡传 `.report`,统计磁贴按类别预选)。 var onTapArchive: (TimelineKind?) -> Void = { _ in } @Query(sort: \Indicator.capturedAt, order: .reverse) private var indicators: [Indicator] @Query(sort: \Report.reportDate, order: .reverse) private var reports: [Report] @Query(sort: \DiaryEntry.createdAt, order: .reverse) private var diaries: [DiaryEntry] @Query(sort: \Symptom.startedAt, order: .reverse) private var symptoms: [Symptom] @Query private var profiles: [UserProfile] @Query private var customMetrics: [CustomMonitorMetric] /// 点迷你趋势卡 → 打开同类聚合详情(历次翻页 + 趋势,与档案库 C1 同款)。 @State private var selectedGroup: IndicatorGroup? private var profile: UserProfile? { profiles.first } /// 主页只挑前 3 条最有代表性的趋势:长期监测优先,其次化验指标。数据不足时整段隐藏。 @MainActor private var featuredBuckets: [SeriesBucket] { let all = SeriesBucket.build(from: indicators, profile: profile, customMetrics: customMetrics) let monitor = all.filter { $0.kind == .monitor } let lab = all.filter { $0.kind == .lab } return Array((monitor + lab).prefix(3)) } private var ongoingSymptomCount: Int { symptoms.filter { $0.endedAt == nil }.count } var body: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { greeting .padding(.top, 4) .padding(.bottom, 18) HomeCalendarCard() .padding(.bottom, 18) overviewSection .padding(.bottom, 18) let buckets = featuredBuckets if !buckets.isEmpty { trendsSection(buckets) .padding(.bottom, 18) } TodayRemindersCard() OngoingSymptomsCard() .padding(.bottom, 18) archiveSection } .padding(.horizontal, 20) .padding(.bottom, 20) } .background(Tj.Palette.sand.ignoresSafeArea()) .sheet(item: $selectedGroup) { group in IndicatorSeriesDetailView(group: group) } } // MARK: - 问候 private var greeting: some View { let t = TimeOfDay.current return HStack(alignment: .center, spacing: 14) { // 时段徽章:暖色圆底 + 对应图标(晨/午/夜),随时段自动切换。 ZStack { Circle().fill(Tj.Palette.sand2) Image(systemName: t.icon) .font(.tjScaled( 22)) .foregroundStyle(Tj.Palette.amber) } .frame(width: 52, height: 52) VStack(alignment: .leading, spacing: 2) { Text(todayLine) .font(.tjScaled( 11)) .tracking(1) .foregroundStyle(Tj.Palette.text3) // 衬线问候,编辑感更强。 Text(t.word) .font(.tjScaled( 28, weight: .semibold, design: .serif)) .foregroundStyle(Tj.Palette.text) Text(t.subtitle) .font(.tjScaled( 12)) .foregroundStyle(Tj.Palette.text2) } Spacer(minLength: 8) TjLockChip() .padding(.top, 2) } } private var todayLine: String { let now = Date() let day = now.formatted(.dateTime.month().day()) let weekday = now.formatted(.dateTime.weekday(.abbreviated)) return "\(day) · \(weekday)" } /// 一天三段:驱动问候语、副标题、徽章图标,保证三者一致。 private enum TimeOfDay { case morning, afternoon, evening static var current: TimeOfDay { switch Calendar.current.component(.hour, from: Date()) { case 5..<12: return .morning case 12..<18: return .afternoon default: return .evening } } var word: String { switch self { case .morning: return String(appLoc: "早安") case .afternoon: return String(appLoc: "下午好") case .evening: return String(appLoc: "晚上好") } } var subtitle: String { switch self { case .morning: return String(appLoc: "新的一天,慢慢来") case .afternoon: return String(appLoc: "记得起身活动一下") case .evening: return String(appLoc: "夜深了,记得早点休息") } } var icon: String { switch self { case .morning: return "sun.max.fill" case .afternoon: return "sun.haze.fill" case .evening: return "moon.stars.fill" } } } // MARK: - 数据概览磁贴(2×2,大数字 + 图标,点进对应分类) private var overviewSection: some View { LazyVGrid(columns: [GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12)], spacing: 12) { statTile(icon: "doc.fill", value: reports.count, label: String(appLoc: "报告"), tint: Tj.Palette.ink) { onTapArchive(.report) } statTile(icon: "drop.fill", value: indicators.count, label: String(appLoc: "指标"), tint: Tj.Palette.brick) { onTapArchive(.indicator) } statTile(icon: "pencil", value: diaries.count, label: String(appLoc: "日记"), tint: Tj.Palette.leaf) { onTapArchive(.diary) } statTile(icon: "waveform.path.ecg", value: symptoms.count, label: ongoingSymptomCount > 0 ? String(appLoc: "症状 · \(ongoingSymptomCount) 进行中") : String(appLoc: "症状"), tint: Tj.Palette.amber) { onTapArchive(.symptom) } } } private func statTile(icon: String, value: Int, label: String, tint: Color, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: 12) { ZStack { Circle().fill(tint.opacity(0.15)) Image(systemName: icon) .font(.tjScaled( 16, weight: .semibold)) .foregroundStyle(tint) } .frame(width: 40, height: 40) VStack(alignment: .leading, spacing: 1) { Text("\(value)") .font(.tjScaled( 22, weight: .bold, design: .rounded)) .foregroundStyle(Tj.Palette.text) Text(label) .font(.tjScaled( 11)) .foregroundStyle(Tj.Palette.text3) .lineLimit(1) .minimumScaleFactor(0.85) } Spacer(minLength: 0) } .padding(12) .frame(maxWidth: .infinity) .tjCard() .contentShape(Rectangle()) } .buttonStyle(.plain) } // MARK: - 健康趋势(迷你折线图,复用趋势页 TrendRow) private func trendsSection(_ buckets: [SeriesBucket]) -> some View { VStack(alignment: .leading, spacing: 10) { Text("健康趋势") .font(.tjH2()) .foregroundStyle(Tj.Palette.text) VStack(spacing: 12) { ForEach(buckets) { bucket in Button { selectedGroup = group(for: bucket) } label: { TrendRow(bucket: bucket) } .buttonStyle(.plain) } } } } /// SeriesBucket → 聚合详情的 IndicatorGroup(与趋势页分组语义一致)。 private func group(for bucket: SeriesBucket) -> IndicatorGroup { if bucket.id == "bp" { return .bloodPressure } if bucket.id.hasPrefix("lab:") { return .lab(key: String(bucket.id.dropFirst(4))) } return .series(key: bucket.id) } // MARK: - 影像档案入口 private var archiveSection: some View { VStack(alignment: .leading, spacing: 10) { Text("影像档案").font(.tjH2()).foregroundStyle(Tj.Palette.text) Button { onTapArchive(.report) } label: { HStack(spacing: 14) { TjPlaceholder(label: String(appLoc: "档案 · \(reports.count)")) .frame(width: 56, height: 56) VStack(alignment: .leading, spacing: 2) { Text("我的报告档案") .font(.tjScaled( 14, weight: .semibold)) .foregroundStyle(Tj.Palette.text) Text("\(reports.count) 份 · \(indicators.count) 项指标 · 端侧加密") .font(.tjScaled( 11)) .foregroundStyle(Tj.Palette.text3) } Spacer() Image(systemName: "chevron.right") .font(.tjScaled( 14, weight: .medium)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .tjCard(bordered: true) } .buttonStyle(.plain) } } } #Preview { HomeView() }