AuthModalView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. @State private var isSubmitting = false
  13. @FocusState private var focusedField: AuthField?
  14. enum AuthTab {
  15. case login
  16. case register
  17. }
  18. private enum AuthField: Hashable {
  19. case username
  20. case identifier
  21. case password
  22. case confirmPassword
  23. }
  24. var body: some View {
  25. ZStack {
  26. Color.spaceBlack.ignoresSafeArea()
  27. VStack(spacing: 20) {
  28. // Header
  29. HStack {
  30. Spacer()
  31. Button {
  32. dismiss()
  33. } label: {
  34. Image(systemName: "xmark.circle.fill")
  35. .font(.system(size: 22))
  36. .foregroundStyle(Color.secondary.opacity(0.6))
  37. }
  38. }
  39. .padding(.horizontal, 16)
  40. .padding(.top, 16)
  41. // Title
  42. VStack(spacing: 6) {
  43. Text(selectedTab == .login ? "账号登录" : "创建新账号")
  44. .font(.system(size: 22, weight: .bold))
  45. .foregroundStyle(Color.primary)
  46. Text(selectedTab == .login ? "登录以同步录音并管理专属设备" : "注册后可将本机记录安全同步至云端")
  47. .font(.system(size: 12))
  48. .foregroundStyle(Color.secondary)
  49. .multilineTextAlignment(.center)
  50. }
  51. .padding(.horizontal, 24)
  52. // Tab Picker
  53. HStack(spacing: 0) {
  54. tabButton(title: "登录", tab: .login)
  55. tabButton(title: "注册账号", tab: .register)
  56. }
  57. .background(Color.cardBackground.opacity(0.4))
  58. .cornerRadius(8)
  59. .padding(.horizontal, 24)
  60. .padding(.top, 8)
  61. // Input Fields
  62. VStack(spacing: 14) {
  63. if selectedTab == .register {
  64. inputField(
  65. icon: "person",
  66. placeholder: "设置昵称 (如: 星痕探索者)",
  67. text: $usernameInput,
  68. field: .username,
  69. contentType: .nickname,
  70. keyboardType: .default,
  71. submitLabel: .next
  72. )
  73. }
  74. inputField(
  75. icon: "envelope",
  76. placeholder: "手机号或电子邮箱",
  77. text: $identifierInput,
  78. field: .identifier,
  79. contentType: .username,
  80. submitLabel: .next
  81. )
  82. inputField(
  83. icon: "lock",
  84. placeholder: "密码 (至少 6 位)",
  85. text: $passwordInput,
  86. field: .password,
  87. isSecure: true,
  88. contentType: selectedTab == .login ? .password : .newPassword,
  89. submitLabel: selectedTab == .login ? .go : .next
  90. )
  91. if selectedTab == .register {
  92. inputField(
  93. icon: "lock.shield",
  94. placeholder: "再次确认密码",
  95. text: $confirmPasswordInput,
  96. field: .confirmPassword,
  97. isSecure: true,
  98. contentType: .newPassword,
  99. submitLabel: .go
  100. )
  101. }
  102. if let error = errorMessage {
  103. Text(error)
  104. .font(.system(size: 12))
  105. .foregroundStyle(Color.recordingRed)
  106. .frame(maxWidth: .infinity, alignment: .leading)
  107. .padding(.horizontal, 4)
  108. }
  109. }
  110. .padding(.horizontal, 24)
  111. .padding(.top, 12)
  112. Spacer()
  113. // Submit Button
  114. Button {
  115. handleAuthSubmit()
  116. } label: {
  117. HStack(spacing: 8) {
  118. if isSubmitting {
  119. ProgressView()
  120. .tint(Color.spaceBlack)
  121. }
  122. Text(submitButtonTitle)
  123. .font(.system(size: 15, weight: .semibold))
  124. .foregroundStyle(Color.spaceBlack)
  125. }
  126. }
  127. .frame(maxWidth: .infinity)
  128. .padding(.vertical, 14)
  129. .background(Color.primary)
  130. .cornerRadius(8)
  131. .padding(.horizontal, 24)
  132. .padding(.bottom, 32)
  133. .disabled(isSubmitting)
  134. }
  135. }
  136. .interactiveDismissDisabled(isSubmitting)
  137. }
  138. private func tabButton(title: String, tab: AuthTab) -> some View {
  139. Button {
  140. withAnimation(.snappy) {
  141. selectedTab = tab
  142. errorMessage = nil
  143. focusedField = nil
  144. }
  145. } label: {
  146. Text(title)
  147. .font(.system(size: 14, weight: selectedTab == tab ? .semibold : .regular))
  148. .foregroundStyle(selectedTab == tab ? Color.primary : Color.secondary)
  149. .frame(maxWidth: .infinity)
  150. .padding(.vertical, 10)
  151. .background(
  152. selectedTab == tab ? Color.cardBackground : Color.clear
  153. )
  154. .cornerRadius(6)
  155. .padding(2)
  156. }
  157. }
  158. private func inputField(
  159. icon: String,
  160. placeholder: String,
  161. text: Binding<String>,
  162. field: AuthField,
  163. isSecure: Bool = false,
  164. contentType: UITextContentType?,
  165. keyboardType: UIKeyboardType = .asciiCapable,
  166. submitLabel: SubmitLabel
  167. ) -> some View {
  168. HStack(spacing: 12) {
  169. Image(systemName: icon)
  170. .font(.system(size: 14))
  171. .foregroundStyle(Color.secondary)
  172. .frame(width: 20)
  173. if isSecure {
  174. SecureField(placeholder, text: text)
  175. .font(.system(size: 14))
  176. .textContentType(contentType)
  177. .keyboardType(keyboardType)
  178. .textInputAutocapitalization(.never)
  179. .autocorrectionDisabled()
  180. .focused($focusedField, equals: field)
  181. .submitLabel(submitLabel)
  182. .onSubmit { handleSubmit(from: field) }
  183. } else {
  184. TextField(placeholder, text: text)
  185. .font(.system(size: 14))
  186. .textContentType(contentType)
  187. .keyboardType(keyboardType)
  188. .textInputAutocapitalization(.never)
  189. .autocorrectionDisabled()
  190. .focused($focusedField, equals: field)
  191. .submitLabel(submitLabel)
  192. .onSubmit { handleSubmit(from: field) }
  193. }
  194. }
  195. .padding(.horizontal, 14)
  196. .padding(.vertical, 12)
  197. .background(Color.cardBackground.opacity(0.3))
  198. .businessBorder(cornerRadius: 8)
  199. }
  200. private var submitButtonTitle: String {
  201. if isSubmitting {
  202. return selectedTab == .login ? "正在登录…" : "正在注册…"
  203. }
  204. return selectedTab == .login ? "安全登录" : "注册并自动登录"
  205. }
  206. private func handleSubmit(from field: AuthField) {
  207. switch field {
  208. case .username:
  209. focusedField = .identifier
  210. case .identifier:
  211. focusedField = .password
  212. case .password where selectedTab == .register:
  213. focusedField = .confirmPassword
  214. case .password, .confirmPassword:
  215. handleAuthSubmit()
  216. }
  217. }
  218. private func handleAuthSubmit() {
  219. guard !isSubmitting else { return }
  220. errorMessage = nil
  221. if selectedTab == .login {
  222. let identifier = identifierInput.trimmingCharacters(in: .whitespacesAndNewlines)
  223. guard !identifier.isEmpty else {
  224. errorMessage = "请输入手机号或邮箱"
  225. focusedField = .identifier
  226. return
  227. }
  228. guard passwordInput.count >= 6 else {
  229. errorMessage = "密码不能少于 6 位"
  230. focusedField = .password
  231. return
  232. }
  233. focusedField = nil
  234. isSubmitting = true
  235. authManager.login(identifier: identifier, password: passwordInput) { success, err in
  236. isSubmitting = false
  237. if success {
  238. dismiss()
  239. } else {
  240. errorMessage = err ?? "登录失败"
  241. }
  242. }
  243. } else {
  244. let name = usernameInput.trimmingCharacters(in: .whitespaces)
  245. guard !name.isEmpty else {
  246. errorMessage = "请输入用户昵称"
  247. focusedField = .username
  248. return
  249. }
  250. let identifier = identifierInput.trimmingCharacters(in: .whitespacesAndNewlines)
  251. guard !identifier.isEmpty else {
  252. errorMessage = "请输入注册手机号或邮箱"
  253. focusedField = .identifier
  254. return
  255. }
  256. guard passwordInput.count >= 6 else {
  257. errorMessage = "密码不能少于 6 位"
  258. focusedField = .password
  259. return
  260. }
  261. guard passwordInput == confirmPasswordInput else {
  262. errorMessage = "两次输入的密码不一致"
  263. focusedField = .confirmPassword
  264. return
  265. }
  266. focusedField = nil
  267. isSubmitting = true
  268. authManager.register(username: name, identifier: identifier, password: passwordInput) { success, err in
  269. isSubmitting = false
  270. if success {
  271. dismiss()
  272. } else {
  273. errorMessage = err ?? "注册失败"
  274. }
  275. }
  276. }
  277. }
  278. }
  279. #Preview {
  280. AuthModalView()
  281. }