Files
e-party-iOS/yana/Views/EMailLoginView.swift
edwinQQQ 3d00e459e3 feat: 更新文档和视图以支持iOS 17及优化用户体验
- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。
- 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
- 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。
- 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。
- 修复多个视图中的逻辑错误,确保功能正常运行。
2025-07-29 17:57:42 +08:00

278 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import ComposableArchitecture
import Combine
struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature>
let onBack: () -> Void
@Binding var showEmailLogin: Bool //
// 使@StateUI
@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 LocalizedString("email_login.get_code", comment: "")
}
}
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(LocalizedString("email_login.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
VStack(spacing: 20) {
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("email icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("email_login.email", comment: ""))
.font(.system(size: 16))
.foregroundColor(.white)
}
TextField("", text: $email)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.focused($focusedField, equals: .email)
.keyboardType(.emailAddress)
.autocapitalization(.none)
}
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("id icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("email_login.verification_code", comment: ""))
.font(.system(size: 16))
.foregroundColor(.white)
}
HStack {
TextField("", text: $verificationCode)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.focused($focusedField, equals: .verificationCode)
.keyboardType(.numberPad)
Button(action: {
store.send(.getVerificationCodeTapped)
}) {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(isCodeButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
}
.disabled(!isCodeButtonEnabled)
}
}
//
Button(action: {
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
}) {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
} else {
Text(LocalizedString("email_login.login", comment: ""))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
.disabled(!isLoginButtonEnabled)
.padding(.top, 20)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
// API Loading
APILoadingEffectView()
}
}
}
}
.navigationBarHidden(true)
}
}
//#Preview {
// EMailLoginView(
// store: Store(
// initialState: EMailLoginFeature.State()
// ) {
// EMailLoginFeature()
// },
// onBack: {},
// showEmailLogin: .constant(true)
// )
//}