Files
kangkang/康康/Features/Archive/ArchiveListView.swift
link2026 77a4ee1c37 缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成:

```
<type>(<scope>): <subject>

<body>
```

其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
2026-06-07 14:17:18 +08:00

306 lines
11 KiB
Swift

import SwiftUI
import SwiftData
struct ArchiveListView: 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]
@Query(sort: \HealthExport.createdAt, order: .reverse)
private var exports: [HealthExport]
@Query(sort: \CustomReminder.updatedAt, order: .reverse)
private var customReminders: [CustomReminder]
@Query(sort: \MetricReminder.updatedAt, order: .reverse)
private var metricReminders: [MetricReminder]
/// push `navigationDestination(item:)`
/// `navigationDestination(isPresented:)` SwiftUI ()
private enum Route: Hashable { case exports, reminders }
@State private var filter: TimelineKind? = nil
@State private var endingSymptom: Symptom?
@State private var selectedEntry: TimelineEntry?
@State private var route: Route?
@MainActor
private var allEntries: [TimelineEntry] {
let mapped =
TimelineEntry.from(indicators: indicators) +
reports.map(TimelineEntry.from(report:)) +
diaries.map(TimelineEntry.from(diary:)) +
symptoms.map(TimelineEntry.from(symptom:))
let filtered = filter.map { kind in mapped.filter { $0.kind == kind } } ?? mapped
return filtered.sorted { $0.date > $1.date }
}
private var grouped: [(section: DateSection, items: [TimelineEntry])] {
TimelineGrouping.group(allEntries)
}
private var totalCount: Int { allEntries.count }
var body: some View {
NavigationStack {
content
.navigationDestination(item: $route) { route in
switch route {
case .exports: HealthExportListView()
case .reminders: RemindersListView()
}
}
}
}
private var content: some View {
VStack(alignment: .leading, spacing: 0) {
header
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, 14)
if reminderTotal > 0 {
reminderBoard
.padding(.horizontal, 20)
.padding(.bottom, 14)
}
filterChips
.padding(.bottom, 14)
if allEntries.isEmpty {
emptyState
} else {
ScrollView(showsIndicators: false) {
LazyVStack(alignment: .leading, spacing: 18, pinnedViews: [.sectionHeaders]) {
ForEach(grouped, id: \.section) { group in
Section {
VStack(spacing: 10) {
ForEach(group.items) { entry in
rowView(for: entry)
}
}
.padding(.horizontal, 20)
} header: {
sectionHeader(group.section, count: group.items.count)
}
}
}
.padding(.bottom, 24)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Tj.Palette.sand.ignoresSafeArea())
.sheet(item: $endingSymptom) { sym in
SymptomEndSheet(symptom: sym)
}
.sheet(item: $selectedEntry) { entry in
if let d = detail(for: entry) {
TimelineEntryDetailView(detail: d)
}
}
}
@ViewBuilder
private func rowView(for entry: TimelineEntry) -> some View {
if entry.kind == .symptom, entry.isOngoing,
let sym = symptoms.first(where: { "symptom-\($0.persistentModelID)" == entry.id }) {
// : sheet(沿)
Button {
endingSymptom = sym
} label: {
TimelineRow(entry: entry)
}
.buttonStyle(.plain)
} else {
// (///):
Button {
if detail(for: entry) != nil { selectedEntry = entry }
} label: {
TimelineRow(entry: entry)
}
.buttonStyle(.plain)
}
}
/// 线 `TimelineDetail.resolve`(/)
private func detail(for entry: TimelineEntry) -> TimelineDetail? {
TimelineDetail.resolve(for: entry,
indicators: indicators, reports: reports,
diaries: diaries, symptoms: symptoms)
}
private var header: some View {
HStack(alignment: .lastTextBaseline) {
Text("记录")
.font(.tjTitle(26))
.foregroundStyle(Tj.Palette.text)
Text(totalCount == 0 ? "" : String(appLoc: "\(totalCount)"))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
if !exports.isEmpty {
Button { route = .exports } label: {
HStack(spacing: 6) {
Image(systemName: "clock.arrow.circlepath")
.font(.tjScaled( 12, weight: .semibold))
Text("导出历史")
.font(.tjScaled( 13, weight: .semibold))
}
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Capsule().fill(Tj.Palette.ink))
}
.buttonStyle(.plain)
}
}
}
// MARK: -
/// ( + ),
private var reminderTotal: Int { customReminders.count + metricReminders.count }
private var reminderEnabledCount: Int {
customReminders.filter(\.enabled).count + metricReminders.filter(\.enabled).count
}
/// updatedAt , 3 (,)
private var reminderTitlePreview: [String] {
let merged: [(title: String, at: Date)] =
customReminders.map { ($0.title, $0.updatedAt) } +
metricReminders.map { ($0.displayName, $0.updatedAt) }
return merged.sorted { $0.at > $1.at }.prefix(3).map(\.title)
}
private var reminderCountLabel: String {
reminderEnabledCount == reminderTotal
? String(appLoc: "\(reminderTotal) 个提醒任务")
: String(appLoc: "\(reminderTotal) 个提醒任务 · \(reminderEnabledCount) 个开启中")
}
private var reminderTitleLine: String {
let joined = reminderTitlePreview.joined(separator: " · ")
return reminderTotal > reminderTitlePreview.count ? joined + "" : joined
}
/// (RemindersListView);
private var reminderBoard: some View {
Button { route = .reminders } label: {
HStack(spacing: 12) {
ZStack {
Circle().fill(reminderEnabledCount > 0 ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.tjScaled( 16))
.foregroundStyle(reminderEnabledCount > 0 ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminderCountLabel)
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
if !reminderTitlePreview.isEmpty {
Text(reminderTitleLine)
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
}
}
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
.contentShape(Rectangle())
.tjCard()
}
.buttonStyle(.plain)
}
private var filterChips: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
chip(label: String(appLoc: "全部"), selected: filter == nil) { filter = nil }
ForEach(TimelineKind.allCases) { kind in
chip(label: kind.label, selected: filter == kind) {
filter = filter == kind ? nil : kind
}
}
}
.padding(.horizontal, 20)
}
}
private func chip(label: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(label)
.font(.tjScaled( 13, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(
Capsule().fill(selected ? Tj.Palette.ink : Tj.Palette.paper)
)
.overlay(
Capsule().strokeBorder(Tj.Palette.line, lineWidth: selected ? 0 : 1)
)
}
.buttonStyle(.plain)
}
private func sectionHeader(_ section: DateSection, count: Int) -> some View {
HStack {
Text(section.label)
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text2)
Rectangle()
.fill(Tj.Palette.lineSoft)
.frame(height: 1)
Text("\(count)")
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Tj.Palette.sand)
}
private var emptyState: some View {
VStack(spacing: 14) {
Spacer()
TjPlaceholder(label: String(appLoc: "还没有任何记录\n点底部 + 号开始"))
.frame(width: 240, height: 140)
Text(filter == nil ? String(appLoc: "记录会按时间归类显示") : String(appLoc: "这个类别下没有记录"))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
.frame(maxWidth: .infinity)
}
}
#Preview {
ArchiveListView()
.modelContainer(for: [
Indicator.self, Report.self, DiaryEntry.self, Symptom.self, Asset.self,
HealthExport.self, ChatTurn.self, UserProfile.self,
MetricReminder.self, CustomMonitorMetric.self
], inMemory: true)
}