|
@@ -11,11 +11,20 @@ struct AuthModalView: View {
|
|
|
@State private var passwordInput: String = ""
|
|
@State private var passwordInput: String = ""
|
|
|
@State private var confirmPasswordInput: String = ""
|
|
@State private var confirmPasswordInput: String = ""
|
|
|
@State private var errorMessage: String?
|
|
@State private var errorMessage: String?
|
|
|
|
|
+ @State private var isSubmitting = false
|
|
|
|
|
+ @FocusState private var focusedField: AuthField?
|
|
|
|
|
|
|
|
enum AuthTab {
|
|
enum AuthTab {
|
|
|
case login
|
|
case login
|
|
|
case register
|
|
case register
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private enum AuthField: Hashable {
|
|
|
|
|
+ case username
|
|
|
|
|
+ case identifier
|
|
|
|
|
+ case password
|
|
|
|
|
+ case confirmPassword
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
var body: some View {
|
|
var body: some View {
|
|
|
ZStack {
|
|
ZStack {
|
|
@@ -65,21 +74,31 @@ struct AuthModalView: View {
|
|
|
inputField(
|
|
inputField(
|
|
|
icon: "person",
|
|
icon: "person",
|
|
|
placeholder: "设置昵称 (如: 星痕探索者)",
|
|
placeholder: "设置昵称 (如: 星痕探索者)",
|
|
|
- text: $usernameInput
|
|
|
|
|
|
|
+ text: $usernameInput,
|
|
|
|
|
+ field: .username,
|
|
|
|
|
+ contentType: .nickname,
|
|
|
|
|
+ keyboardType: .default,
|
|
|
|
|
+ submitLabel: .next
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
inputField(
|
|
inputField(
|
|
|
icon: "envelope",
|
|
icon: "envelope",
|
|
|
placeholder: "手机号或电子邮箱",
|
|
placeholder: "手机号或电子邮箱",
|
|
|
- text: $identifierInput
|
|
|
|
|
|
|
+ text: $identifierInput,
|
|
|
|
|
+ field: .identifier,
|
|
|
|
|
+ contentType: .username,
|
|
|
|
|
+ submitLabel: .next
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
inputField(
|
|
inputField(
|
|
|
icon: "lock",
|
|
icon: "lock",
|
|
|
placeholder: "密码 (至少 6 位)",
|
|
placeholder: "密码 (至少 6 位)",
|
|
|
text: $passwordInput,
|
|
text: $passwordInput,
|
|
|
- isSecure: true
|
|
|
|
|
|
|
+ field: .password,
|
|
|
|
|
+ isSecure: true,
|
|
|
|
|
+ contentType: selectedTab == .login ? .password : .newPassword,
|
|
|
|
|
+ submitLabel: selectedTab == .login ? .go : .next
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
if selectedTab == .register {
|
|
if selectedTab == .register {
|
|
@@ -87,7 +106,10 @@ struct AuthModalView: View {
|
|
|
icon: "lock.shield",
|
|
icon: "lock.shield",
|
|
|
placeholder: "再次确认密码",
|
|
placeholder: "再次确认密码",
|
|
|
text: $confirmPasswordInput,
|
|
text: $confirmPasswordInput,
|
|
|
- isSecure: true
|
|
|
|
|
|
|
+ field: .confirmPassword,
|
|
|
|
|
+ isSecure: true,
|
|
|
|
|
+ contentType: .newPassword,
|
|
|
|
|
+ submitLabel: .go
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -108,11 +130,12 @@ struct AuthModalView: View {
|
|
|
Button {
|
|
Button {
|
|
|
handleAuthSubmit()
|
|
handleAuthSubmit()
|
|
|
} label: {
|
|
} label: {
|
|
|
- if authManager.isProcessing {
|
|
|
|
|
- ProgressView()
|
|
|
|
|
- .tint(Color.spaceBlack)
|
|
|
|
|
- } else {
|
|
|
|
|
- Text(selectedTab == .login ? "安全登录" : "注册并自动登录")
|
|
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ if isSubmitting {
|
|
|
|
|
+ ProgressView()
|
|
|
|
|
+ .tint(Color.spaceBlack)
|
|
|
|
|
+ }
|
|
|
|
|
+ Text(submitButtonTitle)
|
|
|
.font(.system(size: 15, weight: .semibold))
|
|
.font(.system(size: 15, weight: .semibold))
|
|
|
.foregroundStyle(Color.spaceBlack)
|
|
.foregroundStyle(Color.spaceBlack)
|
|
|
}
|
|
}
|
|
@@ -123,9 +146,10 @@ struct AuthModalView: View {
|
|
|
.cornerRadius(8)
|
|
.cornerRadius(8)
|
|
|
.padding(.horizontal, 24)
|
|
.padding(.horizontal, 24)
|
|
|
.padding(.bottom, 32)
|
|
.padding(.bottom, 32)
|
|
|
- .disabled(authManager.isProcessing)
|
|
|
|
|
|
|
+ .disabled(isSubmitting)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ .interactiveDismissDisabled(isSubmitting)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func tabButton(title: String, tab: AuthTab) -> some View {
|
|
private func tabButton(title: String, tab: AuthTab) -> some View {
|
|
@@ -133,6 +157,7 @@ struct AuthModalView: View {
|
|
|
withAnimation(.snappy) {
|
|
withAnimation(.snappy) {
|
|
|
selectedTab = tab
|
|
selectedTab = tab
|
|
|
errorMessage = nil
|
|
errorMessage = nil
|
|
|
|
|
+ focusedField = nil
|
|
|
}
|
|
}
|
|
|
} label: {
|
|
} label: {
|
|
|
Text(title)
|
|
Text(title)
|
|
@@ -148,7 +173,16 @@ struct AuthModalView: View {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private func inputField(icon: String, placeholder: String, text: Binding<String>, isSecure: Bool = false) -> some View {
|
|
|
|
|
|
|
+ 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) {
|
|
HStack(spacing: 12) {
|
|
|
Image(systemName: icon)
|
|
Image(systemName: icon)
|
|
|
.font(.system(size: 14))
|
|
.font(.system(size: 14))
|
|
@@ -158,11 +192,23 @@ struct AuthModalView: View {
|
|
|
if isSecure {
|
|
if isSecure {
|
|
|
SecureField(placeholder, text: text)
|
|
SecureField(placeholder, text: text)
|
|
|
.font(.system(size: 14))
|
|
.font(.system(size: 14))
|
|
|
|
|
+ .textContentType(contentType)
|
|
|
|
|
+ .keyboardType(keyboardType)
|
|
|
|
|
+ .textInputAutocapitalization(.never)
|
|
|
|
|
+ .autocorrectionDisabled()
|
|
|
|
|
+ .focused($focusedField, equals: field)
|
|
|
|
|
+ .submitLabel(submitLabel)
|
|
|
|
|
+ .onSubmit { handleSubmit(from: field) }
|
|
|
} else {
|
|
} else {
|
|
|
TextField(placeholder, text: text)
|
|
TextField(placeholder, text: text)
|
|
|
.font(.system(size: 14))
|
|
.font(.system(size: 14))
|
|
|
- .autocapitalization(.none)
|
|
|
|
|
- .disableAutocorrection(true)
|
|
|
|
|
|
|
+ .textContentType(contentType)
|
|
|
|
|
+ .keyboardType(keyboardType)
|
|
|
|
|
+ .textInputAutocapitalization(.never)
|
|
|
|
|
+ .autocorrectionDisabled()
|
|
|
|
|
+ .focused($focusedField, equals: field)
|
|
|
|
|
+ .submitLabel(submitLabel)
|
|
|
|
|
+ .onSubmit { handleSubmit(from: field) }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.padding(.horizontal, 14)
|
|
.padding(.horizontal, 14)
|
|
@@ -170,21 +216,48 @@ struct AuthModalView: View {
|
|
|
.background(Color.cardBackground.opacity(0.3))
|
|
.background(Color.cardBackground.opacity(0.3))
|
|
|
.businessBorder(cornerRadius: 8)
|
|
.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() {
|
|
private func handleAuthSubmit() {
|
|
|
|
|
+ guard !isSubmitting else { return }
|
|
|
errorMessage = nil
|
|
errorMessage = nil
|
|
|
|
|
|
|
|
if selectedTab == .login {
|
|
if selectedTab == .login {
|
|
|
- guard !identifierInput.trimmingCharacters(in: .whitespaces).isEmpty else {
|
|
|
|
|
|
|
+ let identifier = identifierInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ guard !identifier.isEmpty else {
|
|
|
errorMessage = "请输入手机号或邮箱"
|
|
errorMessage = "请输入手机号或邮箱"
|
|
|
|
|
+ focusedField = .identifier
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
guard passwordInput.count >= 6 else {
|
|
guard passwordInput.count >= 6 else {
|
|
|
errorMessage = "密码不能少于 6 位"
|
|
errorMessage = "密码不能少于 6 位"
|
|
|
|
|
+ focusedField = .password
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- authManager.login(identifier: identifierInput, password: passwordInput) { success, err in
|
|
|
|
|
|
|
+ focusedField = nil
|
|
|
|
|
+ isSubmitting = true
|
|
|
|
|
+ authManager.login(identifier: identifier, password: passwordInput) { success, err in
|
|
|
|
|
+ isSubmitting = false
|
|
|
if success {
|
|
if success {
|
|
|
dismiss()
|
|
dismiss()
|
|
|
} else {
|
|
} else {
|
|
@@ -195,22 +268,30 @@ struct AuthModalView: View {
|
|
|
let name = usernameInput.trimmingCharacters(in: .whitespaces)
|
|
let name = usernameInput.trimmingCharacters(in: .whitespaces)
|
|
|
guard !name.isEmpty else {
|
|
guard !name.isEmpty else {
|
|
|
errorMessage = "请输入用户昵称"
|
|
errorMessage = "请输入用户昵称"
|
|
|
|
|
+ focusedField = .username
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- guard !identifierInput.trimmingCharacters(in: .whitespaces).isEmpty else {
|
|
|
|
|
|
|
+ let identifier = identifierInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ guard !identifier.isEmpty else {
|
|
|
errorMessage = "请输入注册手机号或邮箱"
|
|
errorMessage = "请输入注册手机号或邮箱"
|
|
|
|
|
+ focusedField = .identifier
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
guard passwordInput.count >= 6 else {
|
|
guard passwordInput.count >= 6 else {
|
|
|
errorMessage = "密码不能少于 6 位"
|
|
errorMessage = "密码不能少于 6 位"
|
|
|
|
|
+ focusedField = .password
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
guard passwordInput == confirmPasswordInput else {
|
|
guard passwordInput == confirmPasswordInput else {
|
|
|
errorMessage = "两次输入的密码不一致"
|
|
errorMessage = "两次输入的密码不一致"
|
|
|
|
|
+ focusedField = .confirmPassword
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- authManager.register(username: name, identifier: identifierInput, password: passwordInput) { success, err in
|
|
|
|
|
|
|
+ focusedField = nil
|
|
|
|
|
+ isSubmitting = true
|
|
|
|
|
+ authManager.register(username: name, identifier: identifier, password: passwordInput) { success, err in
|
|
|
|
|
+ isSubmitting = false
|
|
|
if success {
|
|
if success {
|
|
|
dismiss()
|
|
dismiss()
|
|
|
} else {
|
|
} else {
|