ProfileView.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. import SwiftUI
  2. /// Main "我" (Mine) Tab View for user authentication, profile management, cloud sync status, and BLE device management.
  3. struct ProfileView: View {
  4. @ObservedObject var authManager: AuthManager = .shared
  5. @ObservedObject var bleManager: BLEManager = .shared
  6. @State private var showAuthModal = false
  7. @State private var showEditProfileModal = false
  8. @State private var showChangePasswordModal = false
  9. @State private var showScanDeviceModal = false
  10. @State private var showSyncAuthPrompt = false
  11. @State private var showLogoutConfirmation = false
  12. @State private var syncToastMessage: String?
  13. var body: some View {
  14. NavigationStack {
  15. ZStack {
  16. Color.spaceBlack.ignoresSafeArea()
  17. ScrollView {
  18. VStack(spacing: 20) {
  19. // 1. Account Profile Header
  20. accountHeaderCard
  21. .padding(.top, 12)
  22. // 2. User Management Actions (if logged in)
  23. if authManager.isLoggedIn {
  24. userManagementActionsCard
  25. }
  26. // 3. Audio Cloud Sync Card
  27. audioSyncCard
  28. // 4. Device Binding & Management
  29. DeviceListView(
  30. bleManager: bleManager,
  31. authManager: authManager,
  32. onAddDeviceClicked: handleAddDevice
  33. )
  34. // 5. Account Settings & Security
  35. accountInfoSection
  36. // Footer
  37. footer
  38. .padding(.top, 12)
  39. .padding(.bottom, 40)
  40. }
  41. .padding(.horizontal, 16)
  42. }
  43. }
  44. .navigationTitle("个人中心")
  45. .navigationBarTitleDisplayMode(.inline)
  46. .sheet(isPresented: $showAuthModal) {
  47. AuthModalView(authManager: authManager)
  48. }
  49. .sheet(isPresented: $showEditProfileModal) {
  50. EditProfileModalView(authManager: authManager)
  51. }
  52. .sheet(isPresented: $showChangePasswordModal) {
  53. ChangePasswordModalView(authManager: authManager)
  54. }
  55. .sheet(isPresented: $showScanDeviceModal) {
  56. DeviceScanView(bleManager: bleManager, authManager: authManager)
  57. }
  58. .sheet(isPresented: $showSyncAuthPrompt) {
  59. SyncAuthPromptModal(onLoginClick: {
  60. showAuthModal = true
  61. })
  62. }
  63. .confirmationDialog("确定要退出登录吗?", isPresented: $showLogoutConfirmation, titleVisibility: .visible) {
  64. Button("退出登录", role: .destructive) {
  65. authManager.logout()
  66. }
  67. Button("取消", role: .cancel) {}
  68. }
  69. }
  70. }
  71. // MARK: - 1. Account Header
  72. private var accountHeaderCard: some View {
  73. VStack(spacing: 16) {
  74. if let user = authManager.currentUser {
  75. // Authenticated Profile State
  76. HStack(spacing: 16) {
  77. ZStack {
  78. Circle()
  79. .fill(Color.primary.opacity(0.12))
  80. .frame(width: 60, height: 60)
  81. Image(systemName: "person.crop.circle.fill")
  82. .font(.system(size: 34))
  83. .foregroundStyle(Color.primary)
  84. }
  85. VStack(alignment: .leading, spacing: 6) {
  86. HStack {
  87. Text(user.username)
  88. .font(.system(size: 18, weight: .bold))
  89. .foregroundStyle(Color.primary)
  90. Text("本地授权")
  91. .font(.system(size: 9, weight: .bold, design: .monospaced))
  92. .foregroundStyle(Color.spaceBlack)
  93. .padding(.horizontal, 6)
  94. .padding(.vertical, 2)
  95. .background(Color.primary)
  96. .cornerRadius(4)
  97. }
  98. Text(user.email ?? user.phoneNumber ?? ("用户 ID: " + String(user.id.prefix(12))))
  99. .font(.system(size: 12))
  100. .foregroundStyle(Color.secondary)
  101. }
  102. Spacer()
  103. Button {
  104. showLogoutConfirmation = true
  105. } label: {
  106. Text("退出")
  107. .font(.system(size: 12, weight: .regular))
  108. .foregroundStyle(Color.recordingRed)
  109. .padding(.horizontal, 10)
  110. .padding(.vertical, 5)
  111. .overlay(
  112. RoundedRectangle(cornerRadius: 6)
  113. .stroke(Color.recordingRed.opacity(0.4), lineWidth: 1)
  114. )
  115. }
  116. }
  117. } else {
  118. // Guest / Unauthenticated State
  119. VStack(spacing: 14) {
  120. HStack(spacing: 14) {
  121. ZStack {
  122. Circle()
  123. .fill(Color.primary.opacity(0.06))
  124. .frame(width: 52, height: 52)
  125. Image(systemName: "person.crop.circle.badge.plus")
  126. .font(.system(size: 24))
  127. .foregroundStyle(Color.secondary)
  128. }
  129. VStack(alignment: .leading, spacing: 4) {
  130. Text("未登录账号")
  131. .font(.system(size: 16, weight: .semibold))
  132. .foregroundStyle(Color.primary)
  133. Text("登录/注册后支持全功能体验与 BLE 硬件绑定")
  134. .font(.system(size: 12))
  135. .foregroundStyle(Color.secondary)
  136. }
  137. Spacer()
  138. }
  139. Button {
  140. showAuthModal = true
  141. } label: {
  142. Text("注册 / 登录账号")
  143. .font(.system(size: 14, weight: .semibold))
  144. .foregroundStyle(Color.spaceBlack)
  145. .frame(maxWidth: .infinity)
  146. .padding(.vertical, 11)
  147. .background(Color.primary)
  148. .cornerRadius(8)
  149. }
  150. }
  151. }
  152. }
  153. .padding(16)
  154. .background(Color.cardBackground.opacity(0.25))
  155. .businessBorder(cornerRadius: 12)
  156. }
  157. // MARK: - 2. User Management Actions
  158. private var userManagementActionsCard: some View {
  159. VStack(alignment: .leading, spacing: 14) {
  160. HStack(spacing: 8) {
  161. Image(systemName: "slider.horizontal.3")
  162. .font(.system(size: 14, weight: .light))
  163. .foregroundStyle(Color.secondary)
  164. Text("用户资料管理")
  165. .font(.system(size: 15, weight: .semibold))
  166. .foregroundStyle(Color.primary)
  167. }
  168. HStack(spacing: 12) {
  169. Button {
  170. showEditProfileModal = true
  171. } label: {
  172. HStack(spacing: 6) {
  173. Image(systemName: "square.and.pencil")
  174. .font(.system(size: 12))
  175. Text("编辑基本资料")
  176. .font(.system(size: 13, weight: .medium))
  177. }
  178. .foregroundStyle(Color.primary)
  179. .frame(maxWidth: .infinity)
  180. .padding(.vertical, 10)
  181. .background(Color.cardBackground.opacity(0.4))
  182. .cornerRadius(8)
  183. .overlay(
  184. RoundedRectangle(cornerRadius: 8)
  185. .stroke(Color.primary.opacity(0.2), lineWidth: 1)
  186. )
  187. }
  188. Button {
  189. showChangePasswordModal = true
  190. } label: {
  191. HStack(spacing: 6) {
  192. Image(systemName: "key.fill")
  193. .font(.system(size: 12))
  194. Text("修改安全密码")
  195. .font(.system(size: 13, weight: .medium))
  196. }
  197. .foregroundStyle(Color.primary)
  198. .frame(maxWidth: .infinity)
  199. .padding(.vertical, 10)
  200. .background(Color.cardBackground.opacity(0.4))
  201. .cornerRadius(8)
  202. .overlay(
  203. RoundedRectangle(cornerRadius: 8)
  204. .stroke(Color.primary.opacity(0.2), lineWidth: 1)
  205. )
  206. }
  207. }
  208. }
  209. .padding(16)
  210. .background(Color.cardBackground.opacity(0.25))
  211. .businessBorder(cornerRadius: 12)
  212. }
  213. // MARK: - 3. Audio Cloud Sync Card
  214. private var audioSyncCard: some View {
  215. VStack(alignment: .leading, spacing: 14) {
  216. HStack {
  217. HStack(spacing: 8) {
  218. Image(systemName: "icloud.and.arrow.up")
  219. .font(.system(size: 14, weight: .light))
  220. .foregroundStyle(Color.secondary)
  221. Text("录音云端同步")
  222. .font(.system(size: 15, weight: .semibold))
  223. .foregroundStyle(Color.primary)
  224. }
  225. Spacer()
  226. if authManager.isLoggedIn {
  227. HStack(spacing: 4) {
  228. Circle()
  229. .fill(Color.green)
  230. .frame(width: 6, height: 6)
  231. Text("同步功能就绪")
  232. .font(.system(size: 11))
  233. .foregroundStyle(Color.secondary)
  234. }
  235. } else {
  236. Text("需要登录")
  237. .font(.system(size: 11))
  238. .foregroundStyle(Color.recordingRed)
  239. }
  240. }
  241. VStack(alignment: .leading, spacing: 8) {
  242. HStack {
  243. Text("上次同步时间")
  244. .font(.system(size: 12))
  245. .foregroundStyle(Color.secondary)
  246. Spacer()
  247. if let lastDate = authManager.lastSyncDate {
  248. Text(lastDate.formatted(date: .numeric, time: .shortened))
  249. .font(.system(size: 12, design: .monospaced))
  250. .foregroundStyle(Color.primary)
  251. } else {
  252. Text("暂无同步记录")
  253. .font(.system(size: 12, design: .monospaced))
  254. .foregroundStyle(Color.secondary)
  255. }
  256. }
  257. if let toast = syncToastMessage {
  258. HStack(spacing: 6) {
  259. Image(systemName: "checkmark.circle.fill")
  260. .foregroundStyle(Color.primary)
  261. Text(toast)
  262. .font(.system(size: 12))
  263. .foregroundStyle(Color.primary)
  264. }
  265. .padding(.top, 2)
  266. }
  267. }
  268. Button {
  269. triggerAudioSync()
  270. } label: {
  271. HStack(spacing: 8) {
  272. if authManager.isSyncing {
  273. ProgressView()
  274. .tint(Color.primary)
  275. } else {
  276. Image(systemName: "arrow.triangle.2.circlepath")
  277. .font(.system(size: 13))
  278. }
  279. Text(authManager.isSyncing ? "正在同步音频与记录..." : "立即同步录音文件")
  280. .font(.system(size: 13, weight: .medium))
  281. }
  282. .foregroundStyle(Color.primary)
  283. .frame(maxWidth: .infinity)
  284. .padding(.vertical, 10)
  285. .overlay(
  286. RoundedRectangle(cornerRadius: 8)
  287. .stroke(Color.primary.opacity(0.3), lineWidth: 1)
  288. )
  289. }
  290. .disabled(authManager.isSyncing)
  291. }
  292. .padding(16)
  293. .background(Color.cardBackground.opacity(0.25))
  294. .businessBorder(cornerRadius: 12)
  295. }
  296. // MARK: - 5. Account Settings Section
  297. private var accountInfoSection: some View {
  298. VStack(alignment: .leading, spacing: 14) {
  299. HStack(spacing: 8) {
  300. Image(systemName: "shield.checkerboard")
  301. .font(.system(size: 14, weight: .light))
  302. .foregroundStyle(Color.secondary)
  303. Text("安全与云端合规")
  304. .font(.system(size: 15, weight: .semibold))
  305. .foregroundStyle(Color.primary)
  306. }
  307. VStack(spacing: 10) {
  308. infoRow(label: "数据存储模式", value: "沙盒 SQLite / UserDefaults")
  309. infoRow(label: "后端接口状态", value: "已适配 Local & Remote 两套架构")
  310. infoRow(label: "数据加密机制", value: "AES-256 本地私钥保护")
  311. }
  312. }
  313. .padding(16)
  314. .background(Color.cardBackground.opacity(0.25))
  315. .businessBorder(cornerRadius: 12)
  316. }
  317. private func infoRow(label: String, value: String) -> some View {
  318. HStack {
  319. Text(label)
  320. .font(.system(size: 12))
  321. .foregroundStyle(Color.secondary)
  322. Spacer()
  323. Text(value)
  324. .font(.system(size: 12, weight: .regular, design: .monospaced))
  325. .foregroundStyle(Color.primary)
  326. }
  327. }
  328. // MARK: - Actions
  329. private func handleAddDevice() {
  330. if authManager.isLoggedIn {
  331. showScanDeviceModal = true
  332. } else {
  333. showAuthModal = true
  334. }
  335. }
  336. private func triggerAudioSync() {
  337. authManager.syncRecordings {
  338. showSyncAuthPrompt = true
  339. } onComplete: { success in
  340. if success {
  341. withAnimation {
  342. syncToastMessage = "录音数据与元数据已成功同步!"
  343. }
  344. DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
  345. syncToastMessage = nil
  346. }
  347. }
  348. }
  349. }
  350. // MARK: - Footer
  351. private var footer: some View {
  352. VStack(spacing: 4) {
  353. Text("CELESTIA TRACE USER & DEVICE")
  354. .font(.system(size: 10, weight: .bold, design: .monospaced))
  355. .foregroundStyle(Color.secondary.opacity(0.4))
  356. .tracking(2)
  357. Text("安全本地注册与极简日志空间")
  358. .font(.system(size: 9))
  359. .foregroundStyle(Color.secondary.opacity(0.3))
  360. }
  361. }
  362. }
  363. #Preview {
  364. ProfileView()
  365. }