| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- import SwiftUI
- /// Modal sheet for server-backed account login and registration.
- struct AuthModalView: View {
- @Environment(\.dismiss) private var dismiss
- @ObservedObject var authManager: AuthManager = .shared
-
- @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(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<String>,
- 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()
- }
|