import SwiftUI /// Modal sheet for server-backed account login and registration. struct AuthModalView: View { @Environment(\.dismiss) private var dismiss @ObservedObject var authManager: AuthManager = .shared var guidanceMessage: String? = nil @State private var selectedTab: AuthTab = .login @State private var usernameInput: String = "" @State private var identifierInput: String = "" @State private var passwordInput: String = "" @State private var confirmPasswordInput: String = "" @State private var errorMessage: String? @State private var isSubmitting = false @FocusState private var focusedField: AuthField? enum AuthTab { case login case register } private enum AuthField: Hashable { case username case identifier case password case confirmPassword } var body: some View { ZStack { Color.spaceBlack.ignoresSafeArea() VStack(spacing: 20) { // Header HStack { Spacer() Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 22)) .foregroundStyle(Color.secondary.opacity(0.6)) } } .padding(.horizontal, 16) .padding(.top, 16) // Title VStack(spacing: 6) { Text(selectedTab == .login ? "账号登录" : "创建新账号") .font(.system(size: 22, weight: .bold)) .foregroundStyle(Color.primary) Text( guidanceMessage ?? (selectedTab == .login ? "登录以同步录音并管理专属设备" : "注册后可将本机记录安全同步至云端") ) .font(.system(size: 12)) .foregroundStyle(Color.secondary) .multilineTextAlignment(.center) } .padding(.horizontal, 24) // Tab Picker HStack(spacing: 0) { tabButton(title: "登录", tab: .login) tabButton(title: "注册账号", tab: .register) } .background(Color.cardBackground.opacity(0.4)) .cornerRadius(8) .padding(.horizontal, 24) .padding(.top, 8) // Input Fields VStack(spacing: 14) { if selectedTab == .register { inputField( icon: "person", placeholder: "设置昵称 (如: 星痕探索者)", text: $usernameInput, field: .username, contentType: .nickname, keyboardType: .default, submitLabel: .next ) } inputField( icon: "envelope", placeholder: "手机号或电子邮箱", text: $identifierInput, field: .identifier, contentType: .username, submitLabel: .next ) inputField( icon: "lock", placeholder: "密码 (至少 6 位)", text: $passwordInput, field: .password, isSecure: true, contentType: selectedTab == .login ? .password : .newPassword, submitLabel: selectedTab == .login ? .go : .next ) if selectedTab == .register { inputField( icon: "lock.shield", placeholder: "再次确认密码", text: $confirmPasswordInput, field: .confirmPassword, isSecure: true, contentType: .newPassword, submitLabel: .go ) } if let error = errorMessage { Text(error) .font(.system(size: 12)) .foregroundStyle(Color.recordingRed) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 4) } } .padding(.horizontal, 24) .padding(.top, 12) Spacer() // Submit Button Button { handleAuthSubmit() } label: { HStack(spacing: 8) { if isSubmitting { ProgressView() .tint(Color.spaceBlack) } Text(submitButtonTitle) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(Color.spaceBlack) } } .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(Color.primary) .cornerRadius(8) .padding(.horizontal, 24) .padding(.bottom, 32) .disabled(isSubmitting) } } .interactiveDismissDisabled(isSubmitting) } private func tabButton(title: String, tab: AuthTab) -> some View { Button { withAnimation(.snappy) { selectedTab = tab errorMessage = nil focusedField = nil } } label: { Text(title) .font(.system(size: 14, weight: selectedTab == tab ? .semibold : .regular)) .foregroundStyle(selectedTab == tab ? Color.primary : Color.secondary) .frame(maxWidth: .infinity) .padding(.vertical, 10) .background( selectedTab == tab ? Color.cardBackground : Color.clear ) .cornerRadius(6) .padding(2) } } private func inputField( icon: String, placeholder: String, text: Binding, field: AuthField, isSecure: Bool = false, contentType: UITextContentType?, keyboardType: UIKeyboardType = .asciiCapable, submitLabel: SubmitLabel ) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 14)) .foregroundStyle(Color.secondary) .frame(width: 20) if isSecure { SecureField(placeholder, text: text) .font(.system(size: 14)) .textContentType(contentType) .keyboardType(keyboardType) .textInputAutocapitalization(.never) .autocorrectionDisabled() .focused($focusedField, equals: field) .submitLabel(submitLabel) .onSubmit { handleSubmit(from: field) } } else { TextField(placeholder, text: text) .font(.system(size: 14)) .textContentType(contentType) .keyboardType(keyboardType) .textInputAutocapitalization(.never) .autocorrectionDisabled() .focused($focusedField, equals: field) .submitLabel(submitLabel) .onSubmit { handleSubmit(from: field) } } } .padding(.horizontal, 14) .padding(.vertical, 12) .background(Color.cardBackground.opacity(0.3)) .businessBorder(cornerRadius: 8) } private var submitButtonTitle: String { if isSubmitting { return selectedTab == .login ? "正在登录…" : "正在注册…" } return selectedTab == .login ? "安全登录" : "注册并自动登录" } private func handleSubmit(from field: AuthField) { switch field { case .username: focusedField = .identifier case .identifier: focusedField = .password case .password where selectedTab == .register: focusedField = .confirmPassword case .password, .confirmPassword: handleAuthSubmit() } } private func handleAuthSubmit() { guard !isSubmitting else { return } errorMessage = nil if selectedTab == .login { let identifier = identifierInput.trimmingCharacters(in: .whitespacesAndNewlines) guard !identifier.isEmpty else { errorMessage = "请输入手机号或邮箱" focusedField = .identifier return } guard passwordInput.count >= 6 else { errorMessage = "密码不能少于 6 位" focusedField = .password return } focusedField = nil isSubmitting = true authManager.login(identifier: identifier, password: passwordInput) { success, err in isSubmitting = false if success { dismiss() } else { errorMessage = err ?? "登录失败" } } } else { let name = usernameInput.trimmingCharacters(in: .whitespaces) guard !name.isEmpty else { errorMessage = "请输入用户昵称" focusedField = .username return } let identifier = identifierInput.trimmingCharacters(in: .whitespacesAndNewlines) guard !identifier.isEmpty else { errorMessage = "请输入注册手机号或邮箱" focusedField = .identifier return } guard passwordInput.count >= 6 else { errorMessage = "密码不能少于 6 位" focusedField = .password return } guard passwordInput == confirmPasswordInput else { errorMessage = "两次输入的密码不一致" focusedField = .confirmPassword return } focusedField = nil isSubmitting = true authManager.register(username: name, identifier: identifier, password: passwordInput) { success, err in isSubmitting = false if success { dismiss() } else { errorMessage = err ?? "注册失败" } } } } } #Preview { AuthModalView() }