
- 将LocalizedString替换为硬编码字符串,提升代码可读性。 - 重构输入框组件,使用CustomInputField以统一输入框样式和逻辑。 - 更新按钮文本和样式,确保一致性和视觉效果。 - 调整布局和间距,优化用户界面体验。 - 增加验证码输入框的获取按钮功能,提升交互性。
286 lines
10 KiB
Swift
286 lines
10 KiB
Swift
import SwiftUI
|
||
import ComposableArchitecture
|
||
import Combine
|
||
|
||
struct EMailLoginView: View {
|
||
let store: StoreOf<EMailLoginFeature>
|
||
let onBack: () -> Void
|
||
@Binding var showEmailLogin: Bool // 新增:绑定父视图的显示状态
|
||
|
||
// 使用本地@State管理UI状态
|
||
@State private var email: String = ""
|
||
@State private var verificationCode: String = ""
|
||
@State private var codeCountdown: Int = 0
|
||
@FocusState private var focusedField: Field?
|
||
|
||
// 倒计时定时器
|
||
@State private var timerCancellable: AnyCancellable?
|
||
|
||
// 计算属性
|
||
private var isLoginButtonEnabled: Bool {
|
||
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
||
}
|
||
|
||
private var getCodeButtonText: String {
|
||
if codeCountdown > 0 {
|
||
return "\(codeCountdown)s"
|
||
} else {
|
||
return "Get"
|
||
}
|
||
}
|
||
|
||
private var isCodeButtonEnabled: Bool {
|
||
return !store.isCodeLoading && codeCountdown == 0 && !email.isEmpty
|
||
}
|
||
|
||
enum Field {
|
||
case email
|
||
case verificationCode
|
||
}
|
||
|
||
var body: some View {
|
||
WithPerceptionTracking {
|
||
LoginContentView(
|
||
store: store,
|
||
onBack: onBack,
|
||
email: $email,
|
||
verificationCode: $verificationCode,
|
||
codeCountdown: $codeCountdown,
|
||
focusedField: $focusedField,
|
||
isLoginButtonEnabled: isLoginButtonEnabled,
|
||
getCodeButtonText: getCodeButtonText,
|
||
isCodeButtonEnabled: isCodeButtonEnabled
|
||
)
|
||
.onChange(of: store.loginStep) { newStep in
|
||
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
|
||
if newStep == .completed {
|
||
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
|
||
showEmailLogin = false
|
||
}
|
||
}
|
||
}
|
||
.onAppear {
|
||
let _ = WithPerceptionTracking {
|
||
store.send(.resetState)
|
||
email = ""
|
||
verificationCode = ""
|
||
codeCountdown = 0
|
||
stopCountdown()
|
||
#if DEBUG
|
||
email = "exzero@126.com"
|
||
store.send(.emailChanged(email))
|
||
#endif
|
||
}
|
||
}
|
||
.onDisappear {
|
||
let _ = WithPerceptionTracking {
|
||
stopCountdown()
|
||
}
|
||
}
|
||
.onChange(of: email) { newEmail in
|
||
let _ = WithPerceptionTracking {
|
||
store.send(.emailChanged(newEmail))
|
||
}
|
||
}
|
||
.onChange(of: verificationCode) { newCode in
|
||
let _ = WithPerceptionTracking {
|
||
store.send(.verificationCodeChanged(newCode))
|
||
}
|
||
}
|
||
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
||
let _ = WithPerceptionTracking {
|
||
if !isCodeLoading && store.errorMessage == nil {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||
focusedField = .verificationCode
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func startCountdown() {
|
||
stopCountdown()
|
||
codeCountdown = 60
|
||
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
||
.autoconnect()
|
||
.sink { _ in
|
||
if codeCountdown > 0 {
|
||
codeCountdown -= 1
|
||
} else {
|
||
stopCountdown()
|
||
}
|
||
}
|
||
}
|
||
private func stopCountdown() {
|
||
timerCancellable?.cancel()
|
||
timerCancellable = nil
|
||
}
|
||
}
|
||
|
||
private struct LoginContentView: View {
|
||
let store: StoreOf<EMailLoginFeature>
|
||
let onBack: () -> Void
|
||
@Binding var email: String
|
||
@Binding var verificationCode: String
|
||
@Binding var codeCountdown: Int
|
||
@FocusState.Binding var focusedField: EMailLoginView.Field?
|
||
let isLoginButtonEnabled: Bool
|
||
let getCodeButtonText: String
|
||
let isCodeButtonEnabled: Bool
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
WithPerceptionTracking {
|
||
ZStack {
|
||
Image("bg")
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
.ignoresSafeArea(.all)
|
||
VStack(spacing: 0) {
|
||
HStack {
|
||
Button(action: {
|
||
onBack()
|
||
}) {
|
||
Image(systemName: "chevron.left")
|
||
.font(.system(size: 24, weight: .medium))
|
||
.foregroundColor(.white)
|
||
.frame(width: 44, height: 44)
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, 8)
|
||
Spacer().frame(height: 60)
|
||
Text("Email Login")
|
||
.font(.system(size: 28, weight: .bold))
|
||
.foregroundColor(.white)
|
||
.padding(.bottom, 60)
|
||
|
||
VStack(spacing: 24) {
|
||
// 邮箱输入框
|
||
emailInputField
|
||
|
||
// 验证码输入框(带获取按钮)
|
||
verificationCodeInputField
|
||
}
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
.frame(height: 80)
|
||
|
||
// 登录按钮
|
||
Button(action: {
|
||
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
|
||
}) {
|
||
if store.isLoading {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.scaleEffect(1.2)
|
||
} else {
|
||
Text("Login")
|
||
.font(.system(size: 16, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.vertical, 16)
|
||
.background(isLoginButtonEnabled ? Color(red: 0.5, green: 0.3, blue: 0.8) : Color.gray)
|
||
.cornerRadius(8)
|
||
.disabled(!isLoginButtonEnabled)
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
}
|
||
|
||
// MARK: - UI Components
|
||
|
||
private var emailInputField: some View {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.fill(Color.white.opacity(0.1))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
||
)
|
||
.frame(height: 56)
|
||
|
||
TextField("", text: $email)
|
||
.placeholder(when: email.isEmpty) {
|
||
Text("Please enter email")
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
.padding(.horizontal, 24)
|
||
.keyboardType(.emailAddress)
|
||
.autocapitalization(.none)
|
||
.focused($focusedField, equals: .email)
|
||
}
|
||
}
|
||
|
||
private var verificationCodeInputField: some View {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.fill(Color.white.opacity(0.1))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
||
)
|
||
.frame(height: 56)
|
||
|
||
HStack {
|
||
TextField("", text: $verificationCode)
|
||
.placeholder(when: verificationCode.isEmpty) {
|
||
Text("Please enter verification code")
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
.keyboardType(.numberPad)
|
||
.focused($focusedField, equals: .verificationCode)
|
||
|
||
// 获取验证码按钮
|
||
Button(action: {
|
||
store.send(.getVerificationCodeTapped)
|
||
}) {
|
||
ZStack {
|
||
if store.isCodeLoading {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.scaleEffect(0.7)
|
||
} else {
|
||
Text(getCodeButtonText)
|
||
.font(.system(size: 14, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
.frame(width: 60, height: 36)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 15)
|
||
.fill(Color.white.opacity(isCodeButtonEnabled ? 0.2 : 0.1))
|
||
)
|
||
}
|
||
.disabled(!isCodeButtonEnabled || store.isCodeLoading)
|
||
}
|
||
.padding(.horizontal, 24)
|
||
}
|
||
}
|
||
}
|
||
|
||
//#Preview {
|
||
// EMailLoginView(
|
||
// store: Store(
|
||
// initialState: EMailLoginFeature.State()
|
||
// ) {
|
||
// EMailLoginFeature()
|
||
// },
|
||
// onBack: {},
|
||
// showEmailLogin: .constant(true)
|
||
// )
|
||
//}
|