Files
kangkang/康康/Features/Home/HomeView.swift
link2026 b3777d508d 根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:
```
chore(project): 更新项目配置文件

移除未使用的依赖项并优化构建配置,
提升项目整体性能和可维护性。
```
2026-06-16 00:01:48 +08:00

276 lines
9.8 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 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()
}