Files
e-party-iOS/yana/Views/RecoverPasswordView.swift
edwinQQQ b35b6e1ce1 feat: 移除CreateFeedView-Analysis文档并新增用户协议组件以增强用户体验
- 删除CreateFeedView-Analysis.md文档以简化项目结构。
- 新增UserAgreementComponent以处理用户协议的显示和交互。
- 更新多个视图中的onChange逻辑以兼容iOS 17的新API用法,确保代码一致性和可维护性。
- 在Localizable.strings中新增用户协议相关的本地化文本,提升多语言支持。
2025-08-01 14:34:53 +08:00

351 lines
12 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 RecoverPasswordView: View {
let store: StoreOf<RecoverPasswordFeature>
let onBack: () -> Void
// 使@StateUI
@State private var email: String = ""
@State private var verificationCode: String = ""
@State private var newPassword: String = ""
@State private var isNewPasswordVisible: Bool = false
//
@State private var countdown: Int = 0
@State private var timerCancellable: AnyCancellable?
//
private var isEmailValid: Bool {
!email.isEmpty
}
private var isVerificationCodeValid: Bool {
!verificationCode.isEmpty
}
private var isNewPasswordValid: Bool {
!newPassword.isEmpty
}
private var isStoreNotLoading: Bool {
!store.isResetLoading
}
private var isCodeNotLoading: Bool {
!store.isCodeLoading
}
private var isCountdownFinished: Bool {
countdown == 0
}
//
private var isConfirmButtonEnabled: Bool {
isStoreNotLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid
}
//
private var isGetCodeButtonEnabled: Bool {
isCodeNotLoading && isEmailValid && isCountdownFinished
}
//
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
} else if countdown > 0 {
return "\(countdown)s"
} else {
return LocalizedString("recover_password.get_code", comment: "")
}
}
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(LocalizedString("recover_password.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
//
emailInputField
//
verificationCodeInputField
//
newPasswordInputField
}
.padding(.horizontal, 32)
Spacer()
.frame(height: 80)
//
confirmButton
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
resetState()
}
.onDisappear {
stopCountdown()
}
.onChange(of: email) { _, newEmail in
store.send(.emailChanged(newEmail))
}
.onChange(of: verificationCode) { _, newCode in
store.send(.verificationCodeChanged(newCode))
}
.onChange(of: newPassword) { _, newPassword in
store.send(.newPasswordChanged(newPassword))
}
.onChange(of: store.isResetSuccess) { _, isResetSuccess in
if isResetSuccess {
onBack()
}
}
}
// 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(LocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
}
}
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(LocalizedString("recover_password.placeholder_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
//
Button(action: {
startCountdown()
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(isGetCodeButtonEnabled ? 0.2 : 0.1))
)
}
.disabled(!isGetCodeButtonEnabled || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
}
private var newPasswordInputField: 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 {
if isNewPasswordVisible {
TextField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(LocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(LocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
}
Button(action: {
isNewPasswordVisible.toggle()
}) {
Image(systemName: isNewPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
}
.padding(.horizontal, 24)
}
}
private var confirmButton: some View {
Button(action: {
store.send(.resetPasswordTapped)
}) {
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.isResetLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isResetLoading ? LocalizedString("recover_password.resetting", comment: "") : LocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(!isConfirmButtonEnabled)
.opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
}
// MARK: - Private Methods
private func resetState() {
store.send(.resetState)
email = ""
verificationCode = ""
newPassword = ""
isNewPasswordVisible = false
countdown = 0
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
}
private func startCountdown() {
stopCountdown()
countdown = 60
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { _ in
if countdown > 0 {
countdown -= 1
} else {
stopCountdown()
}
}
}
private func stopCountdown() {
timerCancellable?.cancel()
timerCancellable = nil
countdown = 0
}
}
//#Preview {
// RecoverPasswordView(
// store: Store(
// initialState: RecoverPasswordFeature.State()
// ) {
// RecoverPasswordFeature()
// },
// onBack: {}
// )
//}