import SwiftUI // MARK: - ChronoFeedRow /// A card component for the chronological event feed. /// Displays a timeline event with time badge, type icon, content, /// and a clean, wireframe bordered layout suitable for business scenarios. struct ChronoFeedRow: View { let event: CelestiaTimelineEvent var photoEvents: [CelestiaTimelineEvent] = [] var onTap: (() -> Void)? var body: some View { Button { HapticManager.trigger(.tapFeedback) onTap?() } label: { HStack(spacing: 12) { // Left: Time Badge (Minimalist outline) timeBadge // Center: Content eventContent Spacer(minLength: 4) // Right: Chevron Image(systemName: "chevron.right") .font(.system(size: 9, weight: .light)) .foregroundStyle(Color.secondary.opacity(0.5)) } .padding(12) .background(Color.cardBackground.opacity(0.15)) .businessBorder(cornerRadius: 8) } .buttonStyle(.plain) } // MARK: - Time Badge private var timeBadge: some View { Text(event.relativeTimeFormatted) .font(.system(size: 11, weight: .regular, design: .monospaced)) .foregroundStyle(Color.primary) .padding(.horizontal, 8) .padding(.vertical, 4) .businessBorder(cornerRadius: 4) } // MARK: - Event Content private var eventContent: some View { HStack(spacing: 10) { // Event icon (Clean monochrome outline) Image(systemName: eventIconName) .font(.system(size: 12, weight: .light)) .foregroundStyle(Color.primary) .frame(width: 24, height: 24) .businessBorder(cornerRadius: 12) VStack(alignment: .leading, spacing: 3) { // Type label Text(typeLabel) .font(.system(size: 9, weight: .medium)) .foregroundStyle(Color.secondary) // Content switch event.eventType { case "PHOTO": photoView case "NOTE", "MARKER": if let text = event.textContent { Text(text) .font(.system(size: 13, weight: .regular)) .foregroundStyle(Color.primary.opacity(0.9)) .lineLimit(2) .multilineTextAlignment(.leading) } default: Text("事件") .font(.system(size: 13)) .foregroundStyle(Color.secondary) } if let location = displayLocation { Label(location.name, systemImage: "location.fill") .font(.system(size: 10, weight: .medium)) .foregroundStyle(Color.secondary) .lineLimit(1) } } } } // MARK: - Photo Placeholder private var photoPlaceholder: some View { HStack(spacing: 6) { Image(systemName: "photo") .font(.system(size: 10, weight: .light)) .foregroundStyle(Color.secondary) Text("照片片段") .font(.system(size: 11)) .foregroundStyle(Color.secondary) } .padding(.horizontal, 8) .padding(.vertical, 4) .businessBorder(cornerRadius: 4) } @ViewBuilder private var photoView: some View { let events = displayPhotoEvents let images = events.compactMap(resolveImage(for:)) if !images.isEmpty { HStack(spacing: 8) { ZStack(alignment: .leading) { ForEach(Array(images.prefix(3).enumerated()), id: \.offset) { index, image in Image(uiImage: image) .resizable() .scaledToFill() .frame(width: 48, height: 40) .clipShape(RoundedRectangle(cornerRadius: 4)) .overlay { RoundedRectangle(cornerRadius: 4) .stroke(Color.lineBorder, lineWidth: 1) } .offset(x: CGFloat(index) * 12) } } .frame(width: images.count > 1 ? 72 : 48, height: 40, alignment: .leading) VStack(alignment: .leading, spacing: 3) { Text(events.count > 1 ? "\(events.count) 张照片" : "照片记录") .font(.system(size: 11)) .foregroundStyle(Color.secondary) if let note = firstPhotoNote { Text(note) .font(.system(size: 12)) .foregroundStyle(Color.primary.opacity(0.9)) .lineLimit(1) } } } } else { photoPlaceholder } } // MARK: - Computed Properties private var eventIconName: String { if event.isContinuationMarker { return "record.circle" } switch event.eventType { case "PHOTO": return "camera" case "NOTE": return "doc.text" case "MARKER": return "flag" case "VOICE": return "waveform" default: return "circle" } } private var typeLabel: String { if event.isContinuationMarker { return "续录" } switch event.eventType { case "PHOTO": return "照片" case "NOTE": return "笔记" case "MARKER": return "标记" case "VOICE": return "音频" default: return "事件" } } private var displayPhotoEvents: [CelestiaTimelineEvent] { photoEvents.isEmpty ? [event] : photoEvents } private var firstPhotoNote: String? { displayPhotoEvents .compactMap(\.textContent) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .first { !$0.isEmpty } } private var displayLocation: TimelineLocation? { if event.eventType == "PHOTO" { return displayPhotoEvents.compactMap(\.location).first } return event.location } private func resolveImage(for event: CelestiaTimelineEvent) -> UIImage? { guard let url = AudioPathHelper.resolveURL(for: event.localFilePath) else { return nil } return UIImage(contentsOfFile: url.path) } } // MARK: - Preview #Preview { VStack(spacing: 12) { let photoEvent: CelestiaTimelineEvent = { let e = CelestiaTimelineEvent(relativeTimeMs: 32000, eventType: "PHOTO") return e }() let noteEvent: CelestiaTimelineEvent = { let e = CelestiaTimelineEvent(relativeTimeMs: 65000, eventType: "NOTE") e.textContent = "看到了非常壮观的星空,银河清晰可见" return e }() let markerEvent: CelestiaTimelineEvent = { let e = CelestiaTimelineEvent(relativeTimeMs: 120000, eventType: "MARKER") e.textContent = "重要时刻标记" return e }() ChronoFeedRow(event: photoEvent) ChronoFeedRow(event: noteEvent) ChronoFeedRow(event: markerEvent) } .padding(16) .background(Color.spaceBlack) }