DeviceListView.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import SwiftUI
  2. /// Component embedded in ProfileView displaying user's bound BLE recording devices and empty state.
  3. struct DeviceListView: View {
  4. @ObservedObject var bleManager: BLEManager = .shared
  5. @ObservedObject var authManager: AuthManager = .shared
  6. var onAddDeviceClicked: () -> Void
  7. @State private var deviceToUnbind: BoundDevice?
  8. private var userBoundDevices: [BoundDevice] {
  9. guard let userId = authManager.currentUser?.id else { return [] }
  10. return bleManager.devices(forUserId: userId)
  11. }
  12. var body: some View {
  13. VStack(alignment: .leading, spacing: 14) {
  14. // Header Row
  15. HStack {
  16. HStack(spacing: 8) {
  17. Image(systemName: "mic.badge.plus")
  18. .font(.system(size: 14, weight: .light))
  19. .foregroundStyle(Color.secondary)
  20. Text("我的录音设备")
  21. .font(.system(size: 15, weight: .semibold))
  22. .foregroundStyle(Color.primary)
  23. }
  24. Spacer()
  25. Button {
  26. onAddDeviceClicked()
  27. } label: {
  28. HStack(spacing: 4) {
  29. Image(systemName: "plus")
  30. .font(.system(size: 11, weight: .bold))
  31. Text("添加设备")
  32. .font(.system(size: 12, weight: .medium))
  33. }
  34. .foregroundStyle(Color.primary)
  35. .padding(.horizontal, 10)
  36. .padding(.vertical, 5)
  37. .background(Color.primary.opacity(0.08))
  38. .cornerRadius(6)
  39. }
  40. }
  41. // Content State
  42. if userBoundDevices.isEmpty {
  43. emptyDeviceCard
  44. } else {
  45. VStack(spacing: 10) {
  46. ForEach(userBoundDevices) { device in
  47. boundDeviceCard(device)
  48. }
  49. }
  50. }
  51. }
  52. .confirmationDialog(
  53. "确定要解绑设备「\(deviceToUnbind?.name ?? "")」吗?",
  54. isPresented: Binding(
  55. get: { deviceToUnbind != nil },
  56. set: { if !$0 { deviceToUnbind = nil } }
  57. ),
  58. titleVisibility: .visible
  59. ) {
  60. Button("解绑设备", role: .destructive) {
  61. if let dev = deviceToUnbind {
  62. bleManager.unbindDevice(id: dev.id)
  63. }
  64. }
  65. Button("取消", role: .cancel) { }
  66. } message: {
  67. Text("解绑后该设备将从你的账号名下移除。")
  68. }
  69. }
  70. // MARK: - Empty Device Card
  71. private var emptyDeviceCard: some View {
  72. VStack(spacing: 14) {
  73. ZStack {
  74. Circle()
  75. .fill(Color.primary.opacity(0.04))
  76. .frame(width: 56, height: 56)
  77. Image(systemName: "mic.slash")
  78. .font(.system(size: 24, weight: .light))
  79. .foregroundStyle(Color.secondary.opacity(0.6))
  80. }
  81. VStack(spacing: 4) {
  82. Text("暂未绑定录音设备")
  83. .font(.system(size: 14, weight: .medium))
  84. .foregroundStyle(Color.primary)
  85. Text("通过低功耗蓝牙绑定设备后,可同步硬件录音及状态管理")
  86. .font(.system(size: 11))
  87. .foregroundStyle(Color.secondary)
  88. .multilineTextAlignment(.center)
  89. }
  90. Button {
  91. onAddDeviceClicked()
  92. } label: {
  93. HStack(spacing: 6) {
  94. Image(systemName: "wave.3.right")
  95. .font(.system(size: 12))
  96. Text("搜索并绑定蓝牙设备")
  97. .font(.system(size: 13, weight: .medium))
  98. }
  99. .foregroundStyle(Color.spaceBlack)
  100. .padding(.horizontal, 16)
  101. .padding(.vertical, 9)
  102. .background(Color.primary)
  103. .cornerRadius(6)
  104. }
  105. .padding(.top, 4)
  106. }
  107. .frame(maxWidth: .infinity)
  108. .padding(.vertical, 24)
  109. .padding(.horizontal, 16)
  110. .background(Color.cardBackground.opacity(0.2))
  111. .businessBorder(cornerRadius: 10)
  112. }
  113. // MARK: - Bound Device Card
  114. private func boundDeviceCard(_ device: BoundDevice) -> some View {
  115. HStack(spacing: 14) {
  116. // Icon
  117. ZStack {
  118. RoundedRectangle(cornerRadius: 8)
  119. .fill(Color.primary.opacity(0.06))
  120. .frame(width: 44, height: 44)
  121. Image(systemName: "mic.fill")
  122. .font(.system(size: 18))
  123. .foregroundStyle(Color.primary)
  124. }
  125. // Device Details
  126. VStack(alignment: .leading, spacing: 4) {
  127. HStack(spacing: 8) {
  128. Text(device.name)
  129. .font(.system(size: 14, weight: .semibold))
  130. .foregroundStyle(Color.primary)
  131. // Connection Badge
  132. HStack(spacing: 4) {
  133. Circle()
  134. .fill(device.isConnected ? Color.green : Color.gray)
  135. .frame(width: 6, height: 6)
  136. Text(device.isConnected ? "已连接" : "未连接")
  137. .font(.system(size: 10, weight: .medium))
  138. .foregroundStyle(device.isConnected ? Color.green : Color.secondary)
  139. }
  140. }
  141. HStack(spacing: 12) {
  142. if let battery = device.batteryLevel {
  143. HStack(spacing: 3) {
  144. Image(systemName: "battery.75")
  145. .font(.system(size: 10))
  146. Text("\(battery)%")
  147. .font(.system(size: 11, design: .monospaced))
  148. }
  149. .foregroundStyle(Color.secondary)
  150. }
  151. if let fw = device.firmwareVersion {
  152. Text("固件: \(fw)")
  153. .font(.system(size: 11, design: .monospaced))
  154. .foregroundStyle(Color.secondary)
  155. }
  156. }
  157. }
  158. Spacer()
  159. // Unbind Button
  160. Button {
  161. deviceToUnbind = device
  162. } label: {
  163. Text("解绑")
  164. .font(.system(size: 12, weight: .regular))
  165. .foregroundStyle(Color.recordingRed)
  166. .padding(.horizontal, 10)
  167. .padding(.vertical, 5)
  168. .overlay(
  169. RoundedRectangle(cornerRadius: 6)
  170. .stroke(Color.recordingRed.opacity(0.3), lineWidth: 1)
  171. )
  172. }
  173. }
  174. .padding(14)
  175. .background(Color.cardBackground.opacity(0.35))
  176. .businessBorder(cornerRadius: 10)
  177. }
  178. }
  179. #Preview {
  180. DeviceListView(onAddDeviceClicked: {})
  181. }