AuthModalView.swift 11 KB

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