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) }