
- 在swift-assistant-style.mdc中添加项目背景、代码结构、命名规范、Swift最佳实践、UI开发、性能、安全性、测试与质量、核心功能、开发流程、App Store指南等详细规则。 - 在yanaApp.swift中将SplashView替换为Splash,简化应用结构。
333 lines
9.7 KiB
Swift
333 lines
9.7 KiB
Swift
import SwiftUI
|
||
import Combine
|
||
|
||
// MARK: - EMailLogin ViewModel
|
||
|
||
@MainActor
|
||
class EMailLoginViewModel: ObservableObject {
|
||
// MARK: - Published Properties
|
||
@Published var email: String = ""
|
||
@Published var verificationCode: String = ""
|
||
@Published var codeCountdown: Int = 0
|
||
@Published var isLoading: Bool = false
|
||
@Published var isCodeLoading: Bool = false
|
||
@Published var errorMessage: String?
|
||
@Published var loginStep: LoginStep = .input
|
||
|
||
// MARK: - Callbacks
|
||
var onBack: (() -> Void)?
|
||
var onLoginSuccess: (() -> Void)?
|
||
|
||
// MARK: - Private Properties
|
||
private var cancellables = Set<AnyCancellable>()
|
||
private var timerCancellable: AnyCancellable?
|
||
|
||
// MARK: - Enums
|
||
enum LoginStep: Equatable {
|
||
case input
|
||
case completed
|
||
}
|
||
|
||
// MARK: - Computed Properties
|
||
var isLoginButtonEnabled: Bool {
|
||
return !isLoading && !email.isEmpty && !verificationCode.isEmpty
|
||
}
|
||
|
||
var getCodeButtonText: String {
|
||
if codeCountdown > 0 {
|
||
return "\(codeCountdown)s"
|
||
} else {
|
||
return "Get"
|
||
}
|
||
}
|
||
|
||
var isCodeButtonEnabled: Bool {
|
||
return !isCodeLoading && codeCountdown == 0 && !email.isEmpty
|
||
}
|
||
|
||
// MARK: - Public Methods
|
||
func onBackTapped() {
|
||
onBack?()
|
||
}
|
||
|
||
func onEmailChanged(_ newEmail: String) {
|
||
email = newEmail
|
||
}
|
||
|
||
func onVerificationCodeChanged(_ newCode: String) {
|
||
verificationCode = newCode
|
||
}
|
||
|
||
func onGetVerificationCodeTapped() {
|
||
guard isCodeButtonEnabled 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 onLoginTapped() {
|
||
guard isLoginButtonEnabled else { return }
|
||
|
||
isLoading = true
|
||
errorMessage = nil
|
||
|
||
Task {
|
||
do {
|
||
let result = try await performLogin()
|
||
await MainActor.run {
|
||
self.handleLoginResult(result)
|
||
}
|
||
} catch {
|
||
await MainActor.run {
|
||
self.handleLoginError(error)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func resetState() {
|
||
email = ""
|
||
verificationCode = ""
|
||
codeCountdown = 0
|
||
isLoading = false
|
||
isCodeLoading = false
|
||
errorMessage = nil
|
||
loginStep = .input
|
||
stopCountdown()
|
||
}
|
||
|
||
// 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 performLogin() async throws -> Bool {
|
||
return false
|
||
// let request = EmailLoginRequest(
|
||
// email: email,
|
||
// verificationCode: verificationCode
|
||
// )
|
||
//
|
||
// let apiService = LiveAPIService()
|
||
// let response: EmailLoginResponse = try await apiService.request(request)
|
||
//
|
||
// if response.code == 200, let data = response.data {
|
||
// // 保存用户信息
|
||
// await UserInfoManager.saveUserInfo(data)
|
||
//
|
||
// // 创建并保存账户模型
|
||
// let accountModel = AccountModel(
|
||
// uid: data.uid,
|
||
// accessToken: data.accessToken,
|
||
// tokenType: data.tokenType,
|
||
// refreshToken: data.refreshToken,
|
||
// expiresIn: data.expiresIn
|
||
// )
|
||
// await UserInfoManager.saveAccountModel(accountModel)
|
||
//
|
||
// // 获取用户详细信息
|
||
// if let userInfo = await UserInfoManager.fetchUserInfoFromServer(
|
||
// uid: String(data.uid),
|
||
// apiService: apiService
|
||
// ) {
|
||
// await UserInfoManager.saveUserInfo(userInfo)
|
||
// }
|
||
//
|
||
// return true
|
||
// } else {
|
||
// throw APIError.serverError(response.message ?? "Login failed")
|
||
// }
|
||
}
|
||
|
||
private func handleCodeRequestResult(_ success: Bool) {
|
||
isCodeLoading = false
|
||
if success {
|
||
startCountdown()
|
||
}
|
||
}
|
||
|
||
private func handleCodeRequestError(_ error: Error) {
|
||
isCodeLoading = false
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
|
||
private func handleLoginResult(_ success: Bool) {
|
||
isLoading = false
|
||
if success {
|
||
loginStep = .completed
|
||
onLoginSuccess?()
|
||
}
|
||
}
|
||
|
||
private func handleLoginError(_ error: Error) {
|
||
isLoading = false
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
|
||
private func startCountdown() {
|
||
stopCountdown()
|
||
codeCountdown = 60
|
||
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
||
.autoconnect()
|
||
.sink { _ in
|
||
if self.codeCountdown > 0 {
|
||
self.codeCountdown -= 1
|
||
} else {
|
||
self.stopCountdown()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func stopCountdown() {
|
||
timerCancellable?.cancel()
|
||
timerCancellable = nil
|
||
}
|
||
}
|
||
|
||
// MARK: - EMailLogin View
|
||
|
||
struct EMailLoginPage: View {
|
||
@StateObject private var viewModel = EMailLoginViewModel()
|
||
let onBack: () -> Void
|
||
let onLoginSuccess: () -> Void
|
||
|
||
@FocusState private var focusedField: Field?
|
||
|
||
enum Field {
|
||
case email
|
||
case verificationCode
|
||
}
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
ZStack {
|
||
LoginBackgroundView()
|
||
|
||
VStack(spacing: 0) {
|
||
LoginHeaderView(onBack: {
|
||
viewModel.onBackTapped()
|
||
})
|
||
|
||
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)
|
||
|
||
// 登录按钮
|
||
LoginButtonView(
|
||
isLoading: viewModel.isLoading,
|
||
isEnabled: viewModel.isLoginButtonEnabled,
|
||
onTap: {
|
||
viewModel.onLoginTapped()
|
||
}
|
||
)
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
.onAppear {
|
||
viewModel.onBack = onBack
|
||
viewModel.onLoginSuccess = onLoginSuccess
|
||
viewModel.resetState()
|
||
|
||
#if DEBUG
|
||
viewModel.email = "exzero@126.com"
|
||
#endif
|
||
}
|
||
.onDisappear {
|
||
// viewModel.stopCountdown()
|
||
}
|
||
.onChange(of: viewModel.email) { _, newEmail in
|
||
viewModel.onEmailChanged(newEmail)
|
||
}
|
||
.onChange(of: viewModel.verificationCode) { _, newCode in
|
||
viewModel.onVerificationCodeChanged(newCode)
|
||
}
|
||
.onChange(of: viewModel.isCodeLoading) { _, isCodeLoading in
|
||
if !isCodeLoading && viewModel.errorMessage == nil {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||
focusedField = .verificationCode
|
||
}
|
||
}
|
||
}
|
||
.onChange(of: viewModel.loginStep) { _, newStep in
|
||
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
|
||
if newStep == .completed {
|
||
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - UI Components
|
||
|
||
private var emailInputField: some View {
|
||
CustomInputField(
|
||
type: .text,
|
||
placeholder: "Please enter email",
|
||
text: $viewModel.email
|
||
)
|
||
.focused($focusedField, equals: .email)
|
||
}
|
||
|
||
private var verificationCodeInputField: some View {
|
||
CustomInputField(
|
||
type: .verificationCode,
|
||
placeholder: "Please enter verification code",
|
||
text: $viewModel.verificationCode,
|
||
onGetCode: {
|
||
viewModel.onGetVerificationCodeTapped()
|
||
},
|
||
isCodeButtonEnabled: viewModel.isCodeButtonEnabled,
|
||
isCodeLoading: viewModel.isCodeLoading,
|
||
getCodeButtonText: viewModel.getCodeButtonText
|
||
)
|
||
.focused($focusedField, equals: .verificationCode)
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
EMailLoginPage(
|
||
onBack: {},
|
||
onLoginSuccess: {}
|
||
)
|
||
}
|