ChronoFeedRow.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. if event.isContinuationMarker {
  113. return "record.circle"
  114. }
  115. switch event.eventType {
  116. case "PHOTO": return "camera"
  117. case "NOTE": return "doc.text"
  118. case "MARKER": return "flag"
  119. case "VOICE": return "waveform"
  120. default: return "circle"
  121. }
  122. }
  123. private var typeLabel: String {
  124. if event.isContinuationMarker {
  125. return "续录"
  126. }
  127. switch event.eventType {
  128. case "PHOTO": return "照片"
  129. case "NOTE": return "笔记"
  130. case "MARKER": return "标记"
  131. case "VOICE": return "音频"
  132. default: return "事件"
  133. }
  134. }
  135. }
  136. // MARK: - Preview
  137. #Preview {
  138. VStack(spacing: 12) {
  139. let photoEvent: CelestiaTimelineEvent = {
  140. let e = CelestiaTimelineEvent(relativeTimeMs: 32000, eventType: "PHOTO")
  141. return e
  142. }()
  143. let noteEvent: CelestiaTimelineEvent = {
  144. let e = CelestiaTimelineEvent(relativeTimeMs: 65000, eventType: "NOTE")
  145. e.textContent = "看到了非常壮观的星空,银河清晰可见"
  146. return e
  147. }()
  148. let markerEvent: CelestiaTimelineEvent = {
  149. let e = CelestiaTimelineEvent(relativeTimeMs: 120000, eventType: "MARKER")
  150. e.textContent = "重要时刻标记"
  151. return e
  152. }()
  153. ChronoFeedRow(event: photoEvent)
  154. ChronoFeedRow(event: noteEvent)
  155. ChronoFeedRow(event: markerEvent)
  156. }
  157. .padding(16)
  158. .background(Color.spaceBlack)
  159. }