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。
This commit is contained in:
114
康康/Features/Trends/CalendarYearGrid.swift
Normal file
114
康康/Features/Trends/CalendarYearGrid.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user