Files
kangkang/康康/Features/Trends/CalendarYearGrid.swift
link2026 37b47b2076 docs(claude): sync §5/§7/§10 with Monitor+Profile; fix SeriesBucket SwiftData import
- §5 schema 重写为 7 @Model 完整列表(含 UserProfile + Indicator.seriesKey)
- §7 IA 改成 5 槽 TabBar(2 内容 + 中间 + + 2 设置),记录入口 5 个 kind
- §10.6 红线例外清单加 Monitor + Profile(Symptom 也补上)
- SeriesBucket.swift 缺 import SwiftData(persistentModelID 报错)

全套测试 50 case pass / 0 fail / 0 warning。
2026-05-26 07:53:16 +08:00

115 lines
3.8 KiB
Swift

import SwiftUI
struct CalendarYearGrid: View {
let year: Int
let data: CalendarData
let onTapMonth: (Date) -> Void
private let calendar: Calendar = {
var c = Calendar(identifier: .gregorian)
c.firstWeekday = 2
c.locale = Locale(identifier: "zh_CN")
return c
}()
private var monthAnchors: [Date] {
(1...12).compactMap { m in
var comps = DateComponents()
comps.year = year; comps.month = m; comps.day = 1
return calendar.date(from: comps)
}
}
private let columns = Array(repeating: GridItem(.flexible(), spacing: 14), count: 3)
var body: some View {
LazyVGrid(columns: columns, spacing: 18) {
ForEach(monthAnchors, id: \.self) { anchor in
Button {
onTapMonth(anchor)
} label: {
MiniMonth(anchor: anchor, data: data, calendar: calendar)
}
.buttonStyle(.plain)
}
}
}
}
private struct MiniMonth: View {
let anchor: Date
let data: CalendarData
let calendar: Calendar
private var monthLabel: String {
let f = DateFormatter()
f.locale = Locale(identifier: "zh_CN")
f.dateFormat = "M 月"
return f.string(from: anchor)
}
private var days: [Date] {
guard let interval = calendar.dateInterval(of: .month, for: anchor) else { return [] }
let count = calendar.dateComponents([.day], from: interval.start, to: interval.end).day ?? 30
return (0..<count).compactMap { calendar.date(byAdding: .day, value: $0, to: interval.start) }
}
private var leadingPadding: Int {
guard let first = days.first else { return 0 }
return (calendar.component(.weekday, from: first) - calendar.firstWeekday + 7) % 7
}
private let microColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 7)
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(monthLabel)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
LazyVGrid(columns: microColumns, spacing: 2) {
ForEach(0..<leadingPadding, id: \.self) { _ in
Color.clear.frame(height: 8)
}
ForEach(days, id: \.self) { d in
dot(for: d)
}
}
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
}
private func dot(for date: Date) -> some View {
let marks = data.marks(for: date, calendar: calendar)
let ranges = data.ranges(touching: date, calendar: calendar)
let color: Color = {
if marks.abnormalCount > 0 { return Tj.Palette.brick }
if let topSeverity = ranges.map(\.severity).max() {
switch topSeverity {
case 1, 2: return Tj.Palette.leaf
case 3: return Tj.Palette.amber
default: return Tj.Palette.brick
}
}
if marks.hasAnyEvent { return Tj.Palette.text3.opacity(0.6) }
return Tj.Palette.lineSoft
}()
let isToday = calendar.isDateInToday(date)
return RoundedRectangle(cornerRadius: 2, style: .continuous)
.fill(color)
.frame(height: 8)
.overlay(
RoundedRectangle(cornerRadius: 2, style: .continuous)
.strokeBorder(Tj.Palette.ink, lineWidth: isToday ? 1 : 0)
)
}
}