ProfileView.swift 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import SwiftUI
  2. /// Account details opened from Settings.
  3. struct ProfileView: View {
  4. @ObservedObject var authManager: AuthManager = .shared
  5. @State private var showAuthModal = false
  6. @State private var showLogoutConfirmation = false
  7. @State private var usernameInput = ""
  8. @State private var emailInput = ""
  9. @State private var phoneInput = ""
  10. @State private var profileMessage: String?
  11. @State private var profileMessageIsError = false
  12. var body: some View {
  13. ZStack {
  14. Color.spaceBlack.ignoresSafeArea()
  15. ScrollView {
  16. VStack(spacing: 20) {
  17. // 1. Account Profile Header
  18. accountHeaderCard
  19. .padding(.top, 12)
  20. // 2. Inline profile editing (if logged in)
  21. if authManager.isLoggedIn {
  22. profileEditorCard
  23. }
  24. // Footer
  25. footer
  26. .padding(.top, 12)
  27. .padding(.bottom, 40)
  28. }
  29. .padding(.horizontal, 16)
  30. }
  31. }
  32. .navigationTitle("账号")
  33. .navigationBarTitleDisplayMode(.inline)
  34. .sheet(isPresented: $showAuthModal) {
  35. AuthModalView(authManager: authManager)
  36. }
  37. .onAppear(perform: loadProfileInputs)
  38. .confirmationDialog("确定要退出登录吗?", isPresented: $showLogoutConfirmation, titleVisibility: .visible) {
  39. Button("退出登录", role: .destructive) {
  40. authManager.logout()
  41. }
  42. Button("取消", role: .cancel) {}
  43. }
  44. }
  45. // MARK: - 1. Account Header
  46. private var accountHeaderCard: some View {
  47. VStack(spacing: 16) {
  48. if let user = authManager.currentUser {
  49. // Authenticated Profile State
  50. HStack(spacing: 16) {
  51. ZStack {
  52. Circle()
  53. .fill(Color.primary.opacity(0.12))
  54. .frame(width: 60, height: 60)
  55. Image(systemName: "person.crop.circle.fill")
  56. .font(.system(size: 34))
  57. .foregroundStyle(Color.primary)
  58. }
  59. VStack(alignment: .leading, spacing: 6) {
  60. Text(user.username)
  61. .font(.system(size: 18, weight: .bold))
  62. .foregroundStyle(Color.primary)
  63. Text(user.email ?? user.phoneNumber ?? ("用户 ID: " + String(user.id.prefix(12))))
  64. .font(.system(size: 12))
  65. .foregroundStyle(Color.secondary)
  66. }
  67. Spacer()
  68. Button {
  69. showLogoutConfirmation = true
  70. } label: {
  71. Text("退出")
  72. .font(.system(size: 12, weight: .regular))
  73. .foregroundStyle(Color.recordingRed)
  74. .padding(.horizontal, 10)
  75. .padding(.vertical, 5)
  76. .overlay(
  77. RoundedRectangle(cornerRadius: 6)
  78. .stroke(Color.recordingRed.opacity(0.4), lineWidth: 1)
  79. )
  80. }
  81. }
  82. } else {
  83. // Guest / Unauthenticated State
  84. VStack(spacing: 14) {
  85. HStack(spacing: 14) {
  86. ZStack {
  87. Circle()
  88. .fill(Color.primary.opacity(0.06))
  89. .frame(width: 52, height: 52)
  90. Image(systemName: "person.crop.circle.badge.plus")
  91. .font(.system(size: 24))
  92. .foregroundStyle(Color.secondary)
  93. }
  94. VStack(alignment: .leading, spacing: 4) {
  95. Text("未登录账号")
  96. .font(.system(size: 16, weight: .semibold))
  97. .foregroundStyle(Color.primary)
  98. Text("登录/注册后支持全功能体验与 BLE 硬件绑定")
  99. .font(.system(size: 12))
  100. .foregroundStyle(Color.secondary)
  101. }
  102. Spacer()
  103. }
  104. Button {
  105. showAuthModal = true
  106. } label: {
  107. Text("注册 / 登录账号")
  108. .font(.system(size: 14, weight: .semibold))
  109. .foregroundStyle(Color.spaceBlack)
  110. .frame(maxWidth: .infinity)
  111. .padding(.vertical, 11)
  112. .background(Color.primary)
  113. .cornerRadius(8)
  114. }
  115. }
  116. }
  117. }
  118. .padding(16)
  119. .background(Color.cardBackground.opacity(0.25))
  120. .businessBorder(cornerRadius: 12)
  121. }
  122. // MARK: - 2. Inline Profile Editing
  123. private var profileEditorCard: some View {
  124. VStack(alignment: .leading, spacing: 14) {
  125. Text("基本资料")
  126. .font(.system(size: 15, weight: .semibold))
  127. .foregroundStyle(Color.primary)
  128. profileField(label: "账号名", text: $usernameInput, icon: "person")
  129. profileField(label: "电子邮箱", text: $emailInput, icon: "envelope", keyboard: .emailAddress)
  130. profileField(label: "手机号码", text: $phoneInput, icon: "phone", keyboard: .phonePad)
  131. if let profileMessage {
  132. Text(profileMessage)
  133. .font(.system(size: 12))
  134. .foregroundStyle(profileMessageIsError ? Color.recordingRed : Color.green)
  135. }
  136. Button {
  137. saveProfile()
  138. } label: {
  139. Group {
  140. if authManager.isProcessing {
  141. ProgressView().tint(Color.spaceBlack)
  142. } else {
  143. Text("保存基本资料")
  144. .font(.system(size: 13, weight: .medium))
  145. }
  146. }
  147. .foregroundStyle(Color.spaceBlack)
  148. .frame(maxWidth: .infinity)
  149. .padding(.vertical, 10)
  150. .background(Color.primary)
  151. .cornerRadius(8)
  152. }
  153. .disabled(authManager.isProcessing)
  154. NavigationLink {
  155. ChangePasswordModalView(authManager: authManager)
  156. } label: {
  157. HStack {
  158. Image(systemName: "key")
  159. Text("修改安全密码")
  160. Spacer()
  161. Image(systemName: "chevron.right")
  162. .font(.system(size: 11, weight: .semibold))
  163. .foregroundStyle(Color.secondary)
  164. }
  165. .font(.system(size: 13, weight: .medium))
  166. .foregroundStyle(Color.primary)
  167. .padding(.vertical, 10)
  168. }
  169. }
  170. .padding(16)
  171. .background(Color.cardBackground.opacity(0.25))
  172. .businessBorder(cornerRadius: 12)
  173. }
  174. // MARK: - Actions
  175. private func profileField(
  176. label: String,
  177. text: Binding<String>,
  178. icon: String,
  179. keyboard: UIKeyboardType = .default
  180. ) -> some View {
  181. VStack(alignment: .leading, spacing: 6) {
  182. Text(label)
  183. .font(.system(size: 12))
  184. .foregroundStyle(Color.secondary)
  185. HStack(spacing: 10) {
  186. Image(systemName: icon)
  187. .foregroundStyle(Color.secondary)
  188. .frame(width: 18)
  189. TextField(label, text: text)
  190. .textInputAutocapitalization(.never)
  191. .autocorrectionDisabled()
  192. .keyboardType(keyboard)
  193. }
  194. .font(.system(size: 14))
  195. .padding(.horizontal, 12)
  196. .padding(.vertical, 11)
  197. .background(Color.cardBackground.opacity(0.3))
  198. .businessBorder(cornerRadius: 8)
  199. }
  200. }
  201. private func loadProfileInputs() {
  202. guard let user = authManager.currentUser else { return }
  203. usernameInput = user.username
  204. emailInput = user.email ?? ""
  205. phoneInput = user.phoneNumber ?? ""
  206. }
  207. private func saveProfile() {
  208. profileMessage = nil
  209. authManager.updateProfile(
  210. username: usernameInput,
  211. email: emailInput,
  212. phoneNumber: phoneInput,
  213. avatarURL: nil
  214. ) { success, error in
  215. profileMessageIsError = !success
  216. profileMessage = success ? "基本资料已保存" : (error ?? "保存失败")
  217. if success {
  218. loadProfileInputs()
  219. }
  220. }
  221. }
  222. // MARK: - Footer
  223. private var footer: some View {
  224. VStack(spacing: 4) {
  225. Text("CELESTIA TRACE USER & DEVICE")
  226. .font(.system(size: 10, weight: .bold, design: .monospaced))
  227. .foregroundStyle(Color.secondary.opacity(0.4))
  228. .tracking(2)
  229. Text("安全本地注册与极简日志空间")
  230. .font(.system(size: 9))
  231. .foregroundStyle(Color.secondary.opacity(0.3))
  232. }
  233. }
  234. }
  235. #Preview {
  236. NavigationStack {
  237. ProfileView()
  238. }
  239. }