177 lines
6.2 KiB
Swift
177 lines
6.2 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
/// 主页「健康日历」卡:当前一周横条 + 本月记录摘要。
|
|
/// 点整卡或某一天 → 打开 CalendarOverviewView 看月/年总览。自包含 @Query(对齐 TodayRemindersCard)。
|
|
struct HomeCalendarCard: View {
|
|
@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]
|
|
|
|
/// 打开总览时定位的日期(nil = 不展示)。
|
|
@State private var openDay: SelectedDay?
|
|
|
|
private let calendar: Calendar = {
|
|
var c = Calendar(identifier: .gregorian)
|
|
c.firstWeekday = 2
|
|
c.locale = Locale.current
|
|
return c
|
|
}()
|
|
|
|
@MainActor
|
|
private var data: CalendarData {
|
|
CalendarData.build(
|
|
indicators: indicators,
|
|
reports: reports,
|
|
diaries: diaries,
|
|
symptoms: symptoms
|
|
)
|
|
}
|
|
|
|
/// 本周一 → 本周日。
|
|
private var weekDays: [Date] {
|
|
let today = calendar.startOfDay(for: .now)
|
|
let weekdayIndex = (calendar.component(.weekday, from: today) - calendar.firstWeekday + 7) % 7
|
|
guard let monday = calendar.date(byAdding: .day, value: -weekdayIndex, to: today) else {
|
|
return []
|
|
}
|
|
return (0..<7).compactMap { calendar.date(byAdding: .day, value: $0, to: monday) }
|
|
}
|
|
|
|
/// 本月有记录的天数(指标/报告/日记/症状任一)。
|
|
private var daysWithRecordsThisMonth: Int {
|
|
guard let interval = calendar.dateInterval(of: .month, for: .now) else { return 0 }
|
|
let count = calendar.range(of: .day, in: .month, for: .now)?.count ?? 30
|
|
var n = 0
|
|
for i in 0..<count {
|
|
guard let d = calendar.date(byAdding: .day, value: i, to: interval.start) else { continue }
|
|
if data.marks(for: d, calendar: calendar).hasAnyEvent ||
|
|
!data.ranges(touching: d, calendar: calendar).isEmpty {
|
|
n += 1
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
header
|
|
weekStrip
|
|
}
|
|
.padding(14)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.tjCard(bordered: true)
|
|
.padding(.bottom, 18)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { openDay = SelectedDay(date: .now) }
|
|
.fullScreenCover(item: $openDay) { day in
|
|
CalendarOverviewView(initialDate: day.date, onClose: { openDay = nil })
|
|
}
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text("健康日历")
|
|
.font(.tjH2())
|
|
.foregroundStyle(Tj.Palette.text)
|
|
Spacer()
|
|
HStack(spacing: 3) {
|
|
Text(summaryLine)
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
Image(systemName: "chevron.right")
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var summaryLine: String {
|
|
let n = daysWithRecordsThisMonth
|
|
return n > 0 ? String(appLoc: "本月 \(n) 天有记录") : String(appLoc: "本月暂无记录")
|
|
}
|
|
|
|
private var weekStrip: some View {
|
|
HStack(spacing: 6) {
|
|
ForEach(weekDays, id: \.self) { day in
|
|
dayCell(day)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dayCell(_ day: Date) -> some View {
|
|
let marks = data.marks(for: day, calendar: calendar)
|
|
let ranges = data.ranges(touching: day, calendar: calendar)
|
|
let isToday = calendar.isDateInToday(day)
|
|
let hasSymptom = !ranges.isEmpty
|
|
|
|
return Button {
|
|
openDay = SelectedDay(date: day)
|
|
} label: {
|
|
VStack(spacing: 5) {
|
|
Text(weekdayLabel(day))
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
.fill(cellFill(isToday: isToday, hasSymptom: hasSymptom))
|
|
if isToday {
|
|
RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
.strokeBorder(Tj.Palette.ink, lineWidth: 1.2)
|
|
}
|
|
Text("\(calendar.component(.day, from: day))")
|
|
.font(.system(size: 14, weight: isToday ? .bold : .regular))
|
|
.foregroundStyle(isToday ? Tj.Palette.ink : Tj.Palette.text)
|
|
}
|
|
.frame(height: 38)
|
|
marksDots(marks)
|
|
.frame(height: 5)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func marksDots(_ marks: DayMarks) -> some View {
|
|
HStack(spacing: 2) {
|
|
if marks.abnormalCount > 0 {
|
|
dot(Tj.Palette.brick)
|
|
} else if marks.normalCount > 0 {
|
|
dot(Tj.Palette.leaf)
|
|
}
|
|
if marks.reportCount > 0 { dot(Tj.Palette.ink2) }
|
|
if marks.diaryCount > 0 { dot(Tj.Palette.text3.opacity(0.7)) }
|
|
}
|
|
}
|
|
|
|
private func dot(_ color: Color) -> some View {
|
|
Circle().fill(color).frame(width: 4, height: 4)
|
|
}
|
|
|
|
private func cellFill(isToday: Bool, hasSymptom: Bool) -> Color {
|
|
if hasSymptom { return Tj.Palette.amber.opacity(0.18) }
|
|
if isToday { return Tj.Palette.sand2 }
|
|
return Tj.Palette.sand2.opacity(0.5)
|
|
}
|
|
|
|
private func weekdayLabel(_ day: Date) -> String {
|
|
let labels = [
|
|
String(appLoc: "一"), String(appLoc: "二"), String(appLoc: "三"),
|
|
String(appLoc: "四"), String(appLoc: "五"), String(appLoc: "六"),
|
|
String(appLoc: "日")
|
|
]
|
|
let idx = (calendar.component(.weekday, from: day) - calendar.firstWeekday + 7) % 7
|
|
return labels[idx]
|
|
}
|
|
}
|