Files
e-party-iOS/yana/Views/EMailLoginView.swift
edwinQQQ 4a1b814902 feat: 实现数据迁移和用户信息管理优化
- 在AppDelegate中集成数据迁移管理器,支持从UserDefaults迁移到Keychain。
- 重构UserInfoManager,使用Keychain存储用户信息,增加内存缓存以提升性能。
- 添加API加载效果视图,增强用户体验。
- 更新SplashFeature以支持自动登录和认证状态检查。
- 语言设置迁移至Keychain,确保用户设置的安全性。
2025-07-10 17:20:20 +08:00

272 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
struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature>
let onBack: () -> Void
// 使@StateUI
@State private var email: String = ""
@State private var verificationCode: String = ""
@State private var codeCountdown: Int = 0
@State private var timer: Timer?
//
@FocusState private var focusedField: Field?
enum Field {
case email
case verificationCode
}
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
}
//
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
} else if codeCountdown > 0 {
return "\(codeCountdown)S"
} else {
return "email_login.get_code".localized
}
}
//
private var isCodeButtonEnabled: Bool {
return !store.isCodeLoading && codeCountdown == 0
}
var body: some View {
GeometryReader { geometry in
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.title".localized)
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
//
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("placeholder.enter_email".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .email)
}
//
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("placeholder.enter_verification_code".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
.focused($focusedField, equals: .verificationCode)
//
Button(action: {
// API
store.send(.getVerificationCodeTapped)
//
startCountdown()
}) {
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: 18)
.fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1))
)
}
.disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
}
.padding(.horizontal, 32)
Spacer()
.frame(height: 60)
//
Button(action: {
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "email_login.logging_in".localized : "email_login.login_button".localized)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
//
store.send(.resetState)
email = ""
verificationCode = ""
codeCountdown = 0
stopCountdown()
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
}
.onDisappear {
stopCountdown()
}
.onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail))
}
.onChange(of: verificationCode) { newCode in
store.send(.verificationCodeChanged(newCode))
}
.onChange(of: store.isCodeLoading) { isCodeLoading in
// API
if !isCodeLoading && store.errorMessage == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
focusedField = .verificationCode
}
}
}
}
// MARK: -
private func startCountdown() {
stopCountdown()
//
codeCountdown = 60
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
DispatchQueue.main.async {
if codeCountdown > 0 {
codeCountdown -= 1
} else {
stopCountdown()
}
}
}
}
private func stopCountdown() {
timer?.invalidate()
timer = nil
}
}
#Preview {
EMailLoginView(
store: Store(
initialState: EMailLoginFeature.State()
) {
EMailLoginFeature()
},
onBack: {}
)
}