AuthModalView.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import SwiftUI
  2. /// Modal sheet for server-backed account login and registration.
  3. struct AuthModalView: View {
  4. @Environment(\.dismiss) private var dismiss
  5. @ObservedObject var authManager: AuthManager = .shared
  6. @State private var selectedTab: AuthTab = .login
  7. @State private var usernameInput: String = ""
  8. @State private var identifierInput: String = ""
  9. @State private var passwordInput: String = ""
  10. @State private var confirmPasswordInput: String = ""
  11. @State private var errorMessage: String?
  12. enum AuthTab {
  13. case login
  14. case register
  15. }
  16. var body: some View {
  17. ZStack {
  18. Color.spaceBlack.ignoresSafeArea()
  19. VStack(spacing: 20) {
  20. // Header
  21. HStack {
  22. Spacer()
  23. Button {
  24. dismiss()
  25. } label: {
  26. Image(systemName: "xmark.circle.fill")
  27. .font(.system(size: 22))
  28. .foregroundStyle(Color.secondary.opacity(0.6))
  29. }
  30. }
  31. .padding(.horizontal, 16)
  32. .padding(.top, 16)
  33. // Title
  34. VStack(spacing: 6) {
  35. Text(selectedTab == .login ? "账号登录" : "创建新账号")
  36. .font(.system(size: 22, weight: .bold))
  37. .foregroundStyle(Color.primary)
  38. Text(selectedTab == .login ? "登录以同步录音并管理专属设备" : "注册后可将本机记录安全同步至云端")
  39. .font(.system(size: 12))
  40. .foregroundStyle(Color.secondary)
  41. .multilineTextAlignment(.center)
  42. }
  43. .padding(.horizontal, 24)
  44. // Tab Picker
  45. HStack(spacing: 0) {
  46. tabButton(title: "登录", tab: .login)
  47. tabButton(title: "注册账号", tab: .register)
  48. }
  49. .background(Color.cardBackground.opacity(0.4))
  50. .cornerRadius(8)
  51. .padding(.horizontal, 24)
  52. .padding(.top, 8)
  53. // Input Fields
  54. VStack(spacing: 14) {
  55. if selectedTab == .register {
  56. inputField(
  57. icon: "person",
  58. placeholder: "设置昵称 (如: 星痕探索者)",
  59. text: $usernameInput
  60. )
  61. }
  62. inputField(
  63. icon: "envelope",
  64. placeholder: "手机号或电子邮箱",
  65. text: $identifierInput
  66. )
  67. inputField(
  68. icon: "lock",
  69. placeholder: "密码 (至少 6 位)",
  70. text: $passwordInput,
  71. isSecure: true
  72. )
  73. if selectedTab == .register {
  74. inputField(
  75. icon: "lock.shield",
  76. placeholder: "再次确认密码",
  77. text: $confirmPasswordInput,
  78. isSecure: true
  79. )
  80. }
  81. if let error = errorMessage {
  82. Text(error)
  83. .font(.system(size: 12))
  84. .foregroundStyle(Color.recordingRed)
  85. .frame(maxWidth: .infinity, alignment: .leading)
  86. .padding(.horizontal, 4)
  87. }
  88. }
  89. .padding(.horizontal, 24)
  90. .padding(.top, 12)
  91. Spacer()
  92. // Submit Button
  93. Button {
  94. handleAuthSubmit()
  95. } label: {
  96. if authManager.isProcessing {
  97. ProgressView()
  98. .tint(Color.spaceBlack)
  99. } else {
  100. Text(selectedTab == .login ? "安全登录" : "注册并自动登录")
  101. .font(.system(size: 15, weight: .semibold))
  102. .foregroundStyle(Color.spaceBlack)
  103. }
  104. }
  105. .frame(maxWidth: .infinity)
  106. .padding(.vertical, 14)
  107. .background(Color.primary)
  108. .cornerRadius(8)
  109. .padding(.horizontal, 24)
  110. .padding(.bottom, 32)
  111. .disabled(authManager.isProcessing)
  112. }
  113. }
  114. }
  115. private func tabButton(title: String, tab: AuthTab) -> some View {
  116. Button {
  117. withAnimation(.snappy) {
  118. selectedTab = tab
  119. errorMessage = nil
  120. }
  121. } label: {
  122. Text(title)
  123. .font(.system(size: 14, weight: selectedTab == tab ? .semibold : .regular))
  124. .foregroundStyle(selectedTab == tab ? Color.primary : Color.secondary)
  125. .frame(maxWidth: .infinity)
  126. .padding(.vertical, 10)
  127. .background(
  128. selectedTab == tab ? Color.cardBackground : Color.clear
  129. )
  130. .cornerRadius(6)
  131. .padding(2)
  132. }
  133. }
  134. private func inputField(icon: String, placeholder: String, text: Binding<String>, isSecure: Bool = false) -> some View {
  135. HStack(spacing: 12) {
  136. Image(systemName: icon)
  137. .font(.system(size: 14))
  138. .foregroundStyle(Color.secondary)
  139. .frame(width: 20)
  140. if isSecure {
  141. SecureField(placeholder, text: text)
  142. .font(.system(size: 14))
  143. } else {
  144. TextField(placeholder, text: text)
  145. .font(.system(size: 14))
  146. .autocapitalization(.none)
  147. .disableAutocorrection(true)
  148. }
  149. }
  150. .padding(.horizontal, 14)
  151. .padding(.vertical, 12)
  152. .background(Color.cardBackground.opacity(0.3))
  153. .businessBorder(cornerRadius: 8)
  154. }
  155. private func handleAuthSubmit() {
  156. errorMessage = nil
  157. if selectedTab == .login {
  158. guard !identifierInput.trimmingCharacters(in: .whitespaces).isEmpty else {
  159. errorMessage = "请输入手机号或邮箱"
  160. return
  161. }
  162. guard passwordInput.count >= 6 else {
  163. errorMessage = "密码不能少于 6 位"
  164. return
  165. }
  166. authManager.login(identifier: identifierInput, password: passwordInput) { success, err in
  167. if success {
  168. dismiss()
  169. } else {
  170. errorMessage = err ?? "登录失败"
  171. }
  172. }
  173. } else {
  174. let name = usernameInput.trimmingCharacters(in: .whitespaces)
  175. guard !name.isEmpty else {
  176. errorMessage = "请输入用户昵称"
  177. return
  178. }
  179. guard !identifierInput.trimmingCharacters(in: .whitespaces).isEmpty else {
  180. errorMessage = "请输入注册手机号或邮箱"
  181. return
  182. }
  183. guard passwordInput.count >= 6 else {
  184. errorMessage = "密码不能少于 6 位"
  185. return
  186. }
  187. guard passwordInput == confirmPasswordInput else {
  188. errorMessage = "两次输入的密码不一致"
  189. return
  190. }
  191. authManager.register(username: name, identifier: identifierInput, password: passwordInput) { success, err in
  192. if success {
  193. dismiss()
  194. } else {
  195. errorMessage = err ?? "注册失败"
  196. }
  197. }
  198. }
  199. }
  200. }
  201. #Preview {
  202. AuthModalView()
  203. }