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] @State private var filter: TimelineKind? = nil @MainActor private var allEntries: [TimelineEntry] { let mapped = indicators.map(TimelineEntry.from(indicator:)) + 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 { VStack(alignment: .leading, spacing: 0) { header .padding(.horizontal, 20) .padding(.top, 8) .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 TimelineRow(entry: 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()) } private var header: some View { HStack(alignment: .lastTextBaseline) { Text("记录") .font(.tjTitle(26)) .foregroundStyle(Tj.Palette.text) Text(totalCount == 0 ? "" : "\(totalCount) 条") .font(.system(size: 12)) .foregroundStyle(Tj.Palette.text3) Spacer() } } private var filterChips: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { chip(label: "全部", 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(.system(size: 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(.system(size: 12, weight: .semibold)) .tracking(0.5) .foregroundStyle(Tj.Palette.text2) Rectangle() .fill(Tj.Palette.lineSoft) .frame(height: 1) Text("\(count)") .font(.system(size: 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: "还没有任何记录\n点底部 + 号开始") .frame(width: 240, height: 140) Text(filter == nil ? "记录会按时间归类显示" : "这个类别下没有记录") .font(.system(size: 13)) .foregroundStyle(Tj.Palette.text3) Spacer() } .frame(maxWidth: .infinity) } } #Preview { ArchiveListView() .modelContainer(for: [ Indicator.self, Report.self, DiaryEntry.self, Symptom.self, Asset.self ], inMemory: true) }