Files
kangkang/康康/Features/Archive/HealthExportListView.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

138 lines
4.6 KiB
Swift

import SwiftUI
import SwiftData
/// ArchiveListView strip
struct HealthExportListView: View {
@Environment(\.modelContext) private var ctx
@Query(sort: \HealthExport.createdAt, order: .reverse)
private var exports: [HealthExport]
@State private var selected: HealthExport?
var body: some View {
VStack(alignment: .leading, spacing: 0) {
header
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, 14)
if exports.isEmpty {
empty
} else {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(exports) { exp in
Button {
selected = exp
} label: {
HealthExportRow(export: exp)
}
.buttonStyle(.plain)
.contextMenu {
Button(role: .destructive) {
delete(exp)
} label: {
Label("删除", systemImage: "trash")
}
}
}
}
.padding(.horizontal, 20)
.padding(.bottom, 24)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Tj.Palette.sand.ignoresSafeArea())
.navigationTitle("我的导出")
.navigationBarTitleDisplayMode(.inline)
.sheet(item: $selected) { exp in
HealthExportDetailView(export: exp)
}
}
private var header: some View {
HStack(alignment: .lastTextBaseline) {
Text("我的导出")
.font(.tjTitle(24))
.foregroundStyle(Tj.Palette.text)
Text(exports.isEmpty ? "" : String(appLoc: "\(exports.count)"))
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
TjLockChip()
}
}
private var empty: some View {
VStack(spacing: 12) {
Spacer()
TjPlaceholder(label: String(appLoc: "还没有导出过\n回到记录页右上角生成一份"))
.frame(width: 240, height: 140)
Spacer()
}
.frame(maxWidth: .infinity)
}
private func delete(_ exp: HealthExport) {
ctx.delete(exp)
try? ctx.save()
}
}
///
struct HealthExportRow: View {
let export: HealthExport
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .top) {
Text(export.promptPreview)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(2)
.multilineTextAlignment(.leading)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
HStack(spacing: 8) {
Text(Self.relativeDate(export.createdAt))
.font(.system(size: 11))
.foregroundStyle(Tj.Palette.text3)
if export.decodeRate > 0 {
Text(String(format: "%.1f tok/s", export.decodeRate))
.font(.system(size: 10, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
}
Spacer()
if let label = export.inferredLabelCN ?? export.inferredIntent {
TjBadge(text: label, style: .neutral)
}
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.tjCard()
}
static func relativeDate(_ d: Date) -> String {
let f = RelativeDateTimeFormatter()
f.locale = Locale.current
f.unitsStyle = .full
return f.localizedString(for: d, relativeTo: .now)
}
}
#Preview {
NavigationStack {
HealthExportListView()
}
.modelContainer(for: [
Indicator.self, Report.self, DiaryEntry.self, Asset.self,
ChatTurn.self, Symptom.self, UserProfile.self,
MetricReminder.self, CustomMonitorMetric.self, HealthExport.self
], inMemory: true)
}