Files
e-party-iOS/yana/MVVM/RecoverPasswordPage.swift
edwinQQQ 428aa95c5e feat: 更新Swift助手样式规则和应用结构
- 在swift-assistant-style.mdc中添加项目背景、代码结构、命名规范、Swift最佳实践、UI开发、性能、安全性、测试与质量、核心功能、开发流程、App Store指南等详细规则。
- 在yanaApp.swift中将SplashView替换为Splash,简化应用结构。
2025-08-06 14:12:20 +08:00

437 lines
14 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 Combine
// MARK: - RecoverPassword ViewModel
@MainActor
class RecoverPasswordViewModel: ObservableObject {
// MARK: - Published Properties
@Published var email: String = ""
@Published var verificationCode: String = ""
@Published var newPassword: String = ""
@Published var isNewPasswordVisible: Bool = false
@Published var countdown: Int = 0
@Published var isResetLoading: Bool = false
@Published var isCodeLoading: Bool = false
@Published var errorMessage: String?
@Published var isResetSuccess: Bool = false
// MARK: - Callbacks
var onBack: (() -> Void)?
// MARK: - Private Properties
private var timerCancellable: AnyCancellable?
// MARK: - Computed Properties
var isEmailValid: Bool {
!email.isEmpty
}
var isVerificationCodeValid: Bool {
!verificationCode.isEmpty
}
var isNewPasswordValid: Bool {
!newPassword.isEmpty
}
var isConfirmButtonEnabled: Bool {
!isResetLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid
}
var isGetCodeButtonEnabled: Bool {
!isCodeLoading && isEmailValid && countdown == 0
}
var getCodeButtonText: String {
if isCodeLoading {
return ""
} else if countdown > 0 {
return "\(countdown)s"
} else {
return LocalizedString("recover_password.get_code", comment: "")
}
}
// MARK: - Public Methods
func onBackTapped() {
onBack?()
}
func onEmailChanged(_ newEmail: String) {
email = newEmail
}
func onVerificationCodeChanged(_ newCode: String) {
verificationCode = newCode
}
func onNewPasswordChanged(_ newPassword: String) {
self.newPassword = newPassword
}
func onGetVerificationCodeTapped() {
guard isGetCodeButtonEnabled else { return }
isCodeLoading = true
errorMessage = nil
Task {
do {
let result = try await requestVerificationCode()
await MainActor.run {
self.handleCodeRequestResult(result)
}
} catch {
await MainActor.run {
self.handleCodeRequestError(error)
}
}
}
}
func onResetPasswordTapped() {
guard isConfirmButtonEnabled else { return }
isResetLoading = true
errorMessage = nil
Task {
do {
let result = try await resetPassword()
await MainActor.run {
self.handleResetResult(result)
}
} catch {
await MainActor.run {
self.handleResetError(error)
}
}
}
}
func resetState() {
email = ""
verificationCode = ""
newPassword = ""
isNewPasswordVisible = false
countdown = 0
isResetLoading = false
isCodeLoading = false
errorMessage = nil
isResetSuccess = false
stopCountdown()
#if DEBUG
email = "exzero@126.com"
#endif
}
// MARK: - Private Methods
private func requestVerificationCode() async throws -> Bool {
return false
// let request = EmailVerificationCodeRequest(email: email)
// let apiService = LiveAPIService()
// let response: EmailVerificationCodeResponse = try await apiService.request(request)
//
// if response.code == 200 {
// return true
// } else {
// throw APIError.serverError(response.message ?? "Failed to send verification code")
// }
}
private func resetPassword() async throws -> Bool {
return false
// let request = ResetPasswordRequest(
// email: email,
// verificationCode: verificationCode,
// newPassword: newPassword
// )
//
// let apiService = LiveAPIService()
// let response: ResetPasswordResponse = try await apiService.request(request)
//
// if response.code == 200 {
// return true
// } else {
// throw APIError.serverError(response.message ?? "Failed to reset password")
// }
}
private func handleCodeRequestResult(_ success: Bool) {
isCodeLoading = false
if success {
startCountdown()
}
}
private func handleCodeRequestError(_ error: Error) {
isCodeLoading = false
errorMessage = error.localizedDescription
}
private func handleResetResult(_ success: Bool) {
isResetLoading = false
if success {
isResetSuccess = true
onBack?()
}
}
private func handleResetError(_ error: Error) {
isResetLoading = false
errorMessage = error.localizedDescription
}
private func startCountdown() {
stopCountdown()
countdown = 60
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { _ in
if self.countdown > 0 {
self.countdown -= 1
} else {
self.stopCountdown()
}
}
}
private func stopCountdown() {
timerCancellable?.cancel()
timerCancellable = nil
countdown = 0
}
}
// MARK: - RecoverPassword View
struct RecoverPasswordPage: View {
@StateObject private var viewModel = RecoverPasswordViewModel()
let onBack: () -> Void
var body: some View {
GeometryReader { geometry in
ZStack {
//
LoginBackgroundView()
VStack(spacing: 0) {
//
LoginHeaderView(onBack: {
viewModel.onBackTapped()
})
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 = viewModel.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
viewModel.onBack = onBack
viewModel.resetState()
}
.onDisappear {
// viewModel.stopCountdown()
}
.onChange(of: viewModel.email) { _, newEmail in
viewModel.onEmailChanged(newEmail)
}
.onChange(of: viewModel.verificationCode) { _, newCode in
viewModel.onVerificationCodeChanged(newCode)
}
.onChange(of: viewModel.newPassword) { _, newPassword in
viewModel.onNewPasswordChanged(newPassword)
}
}
// 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: $viewModel.email)
.placeholder(when: viewModel.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: $viewModel.verificationCode)
.placeholder(when: viewModel.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: {
viewModel.onGetVerificationCodeTapped()
}) {
ZStack {
if viewModel.isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(viewModel.getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(Color.white.opacity(viewModel.isGetCodeButtonEnabled ? 0.2 : 0.1))
)
}
.disabled(!viewModel.isGetCodeButtonEnabled || viewModel.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 viewModel.isNewPasswordVisible {
TextField("", text: $viewModel.newPassword)
.placeholder(when: viewModel.newPassword.isEmpty) {
Text(LocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $viewModel.newPassword)
.placeholder(when: viewModel.newPassword.isEmpty) {
Text(LocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
}
Button(action: {
viewModel.isNewPasswordVisible.toggle()
}) {
Image(systemName: viewModel.isNewPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
}
.padding(.horizontal, 24)
}
}
private var confirmButton: some View {
Button(action: {
viewModel.onResetPasswordTapped()
}) {
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 viewModel.isResetLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(viewModel.isResetLoading ? LocalizedString("recover_password.resetting", comment: "") : LocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(!viewModel.isConfirmButtonEnabled)
.opacity(viewModel.isConfirmButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
}
}
#Preview {
RecoverPasswordPage(onBack: {})
}