ProfileView.swift 17 KB

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