Files
kangkang/康康/Features/Trends/CalendarYearGrid.swift
link2026 40155de709 ```
feat(AI): 优化AIRuntime任务取消机制并增强安全保护

- 在AI推理流中添加Task.checkCancellation()检查,使消费者取消时能快速退出
- 为异步流添加onTermination回调以取消内部Task,与LLMSession一致
- 实现SwiftData store的completeUnlessOpen文件保护,提升数据安全性
- 在store备份过程中同样应用加密保护

feat(home): 优化主页交互体验并统一详情查看功能

- 在主页"最近记录"中点击任意条目可打开只读详情sheet
- 将时间线详情解析逻辑统一收敛到TimelineDetail.resolve方法
- 修复血压条目的精确反查逻辑,避免时间窗匹配错误

feat(archive): 新增提醒任务汇总卡并完善档案库功能

- 在档案库页面新增提醒任务汇总卡,显示总数和启用状态
- 添加按更新时间倒序合并的提醒标题预览功能
- 实现RemindersListView导航路由,统一管理提醒任务
- 优化导出列表显示,优先使用中文标签展示

feat(me): 优化个人中心界面并改进语言设置体验

- 将个人中心标题改为内容文字渲染,解决导航栏背景问题
- 为语言选择器添加个性化图标,使用本族语代表字区分
- 修复语言设置视图的图标显示逻辑

feat(timeline): 新增记录详情页删除功能并优化图表显示

- 在时间线详情页添加永久删除按钮和确认弹窗
- 实现完整的删除逻辑,包括SwiftData硬删和Vault原图unlink
- 修复系列图表的数值范围计算,处理同值数据的对称留白
- 优化血压图表合并逻辑,只保留有数据点的线条

refactor(calendar): 修复DST切换导致的月份天数计算错误

- 使用calendar.range(of:.day,in:.month)替代日期间隔计算
- 避免在夏令时切换月份出现天数偏差问题

fix(ui): 修复多个UI组件的交互响应区域问题

- 为纯描边按钮和胶囊添加contentShape以扩大点击区域
- 修复提醒行展开按钮尺寸,保证不同提醒类型的垂直对齐
```
2026-05-31 09:25:49 +08:00

113 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.current
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 {
anchor.formatted(.dateTime.month())
}
private var days: [Date] {
guard let interval = calendar.dateInterval(of: .month, for: anchor) else { return [] }
// range(of:.day,in:.month) , DST 1
let count = calendar.range(of: .day, in: .month, for: anchor)?.count ?? 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)
)
}
}