ChronoFeedRow.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import SwiftUI
  2. // MARK: - ChronoFeedRow
  3. /// A card component for the chronological event feed.
  4. /// Displays a timeline event with time badge, type icon, content,
  5. /// and a clean, wireframe bordered layout suitable for business scenarios.
  6. struct ChronoFeedRow: View {
  7. let event: CelestiaTimelineEvent
  8. var onTap: (() -> Void)?
  9. var body: some View {
  10. Button {
  11. HapticManager.trigger(.tapFeedback)
  12. onTap?()
  13. } label: {
  14. HStack(spacing: 12) {
  15. // Left: Time Badge (Minimalist outline)
  16. timeBadge
  17. // Center: Content
  18. eventContent
  19. Spacer(minLength: 4)
  20. // Right: Chevron
  21. Image(systemName: "chevron.right")
  22. .font(.system(size: 9, weight: .light))
  23. .foregroundStyle(Color.secondary.opacity(0.5))
  24. }
  25. .padding(12)
  26. .background(Color.cardBackground.opacity(0.15))
  27. .businessBorder(cornerRadius: 8)
  28. }
  29. .buttonStyle(.plain)
  30. }
  31. // MARK: - Time Badge
  32. private var timeBadge: some View {
  33. Text(event.relativeTimeFormatted)
  34. .font(.system(size: 11, weight: .regular, design: .monospaced))
  35. .foregroundStyle(Color.primary)
  36. .padding(.horizontal, 8)
  37. .padding(.vertical, 4)
  38. .businessBorder(cornerRadius: 4)
  39. }
  40. // MARK: - Event Content
  41. private var eventContent: some View {
  42. HStack(spacing: 10) {
  43. // Event icon (Clean monochrome outline)
  44. Image(systemName: eventIconName)
  45. .font(.system(size: 12, weight: .light))
  46. .foregroundStyle(Color.primary)
  47. .frame(width: 24, height: 24)
  48. .businessBorder(cornerRadius: 12)
  49. VStack(alignment: .leading, spacing: 3) {
  50. // Type label
  51. Text(typeLabel)
  52. .font(.system(size: 9, weight: .medium))
  53. .foregroundStyle(Color.secondary)
  54. // Content
  55. switch event.eventType {
  56. case "PHOTO":
  57. photoView
  58. case "NOTE", "MARKER":
  59. if let text = event.textContent {
  60. Text(text)
  61. .font(.system(size: 13, weight: .regular))
  62. .foregroundStyle(Color.primary.opacity(0.9))
  63. .lineLimit(2)
  64. .multilineTextAlignment(.leading)
  65. }
  66. default:
  67. Text("事件")
  68. .font(.system(size: 13))
  69. .foregroundStyle(Color.secondary)
  70. }
  71. }
  72. }
  73. }
  74. // MARK: - Photo Placeholder
  75. private var photoPlaceholder: some View {
  76. HStack(spacing: 6) {
  77. Image(systemName: "photo")
  78. .font(.system(size: 10, weight: .light))
  79. .foregroundStyle(Color.secondary)
  80. Text("照片片段")
  81. .font(.system(size: 11))
  82. .foregroundStyle(Color.secondary)
  83. }
  84. .padding(.horizontal, 8)
  85. .padding(.vertical, 4)
  86. .businessBorder(cornerRadius: 4)
  87. }
  88. @ViewBuilder
  89. private var photoView: some View {
  90. if let path = event.localFilePath,
  91. let uiImage = UIImage(contentsOfFile: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(path).path) {
  92. HStack(spacing: 8) {
  93. Image(uiImage: uiImage)
  94. .resizable()
  95. .scaledToFill()
  96. .frame(width: 60, height: 45)
  97. .clipShape(RoundedRectangle(cornerRadius: 4))
  98. .overlay(
  99. RoundedRectangle(cornerRadius: 4)
  100. .stroke(Color.lineBorder, lineWidth: 1)
  101. )
  102. Text("照片记录")
  103. .font(.system(size: 11))
  104. .foregroundStyle(Color.secondary)
  105. }
  106. } else {
  107. photoPlaceholder
  108. }
  109. }
  110. // MARK: - Computed Properties
  111. private var eventIconName: String {
  112. switch event.eventType {
  113. case "PHOTO": return "camera"
  114. case "NOTE": return "doc.text"
  115. case "MARKER": return "flag"
  116. case "VOICE": return "waveform"
  117. default: return "circle"
  118. }
  119. }
  120. private var typeLabel: String {
  121. switch event.eventType {
  122. case "PHOTO": return "照片"
  123. case "NOTE": return "笔记"
  124. case "MARKER": return "标记"
  125. case "VOICE": return "音频"
  126. default: return "事件"
  127. }
  128. }
  129. }
  130. // MARK: - Preview
  131. #Preview {
  132. VStack(spacing: 12) {
  133. let photoEvent: CelestiaTimelineEvent = {
  134. let e = CelestiaTimelineEvent(relativeTimeMs: 32000, eventType: "PHOTO")
  135. return e
  136. }()
  137. let noteEvent: CelestiaTimelineEvent = {
  138. let e = CelestiaTimelineEvent(relativeTimeMs: 65000, eventType: "NOTE")
  139. e.textContent = "看到了非常壮观的星空,银河清晰可见"
  140. return e
  141. }()
  142. let markerEvent: CelestiaTimelineEvent = {
  143. let e = CelestiaTimelineEvent(relativeTimeMs: 120000, eventType: "MARKER")
  144. e.textContent = "重要时刻标记"
  145. return e
  146. }()
  147. ChronoFeedRow(event: photoEvent)
  148. ChronoFeedRow(event: noteEvent)
  149. ChronoFeedRow(event: markerEvent)
  150. }
  151. .padding(16)
  152. .background(Color.spaceBlack)
  153. }