
- 在swift-assistant-style.mdc中添加项目背景、代码结构、命名规范、Swift最佳实践、UI开发、性能、安全性、测试与质量、核心功能、开发流程、App Store指南等详细规则。 - 在yanaApp.swift中将SplashView替换为Splash,简化应用结构。
231 lines
7.2 KiB
Swift
231 lines
7.2 KiB
Swift
import SwiftUI
|
||
import Combine
|
||
|
||
// MARK: - IDLogin ViewModel
|
||
|
||
@MainActor
|
||
class IDLoginViewModel: ObservableObject {
|
||
// MARK: - Published Properties
|
||
@Published var userID: String = ""
|
||
@Published var password: String = ""
|
||
@Published var isPasswordVisible: Bool = false
|
||
@Published var isLoading: Bool = false
|
||
@Published var errorMessage: String?
|
||
@Published var showRecoverPassword: Bool = false
|
||
@Published var loginStep: LoginStep = .input
|
||
|
||
// MARK: - Callbacks
|
||
var onBack: (() -> Void)?
|
||
var onLoginSuccess: (() -> Void)?
|
||
|
||
// MARK: - Private Properties
|
||
private var cancellables = Set<AnyCancellable>()
|
||
|
||
// MARK: - Enums
|
||
enum LoginStep: Equatable {
|
||
case input
|
||
case completed
|
||
}
|
||
|
||
// MARK: - Computed Properties
|
||
var isLoginButtonEnabled: Bool {
|
||
return !isLoading && !userID.isEmpty && !password.isEmpty
|
||
}
|
||
|
||
// MARK: - Public Methods
|
||
func onBackTapped() {
|
||
onBack?()
|
||
}
|
||
|
||
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 onRecoverPasswordTapped() {
|
||
showRecoverPassword = true
|
||
}
|
||
|
||
func onRecoverPasswordBack() {
|
||
showRecoverPassword = false
|
||
}
|
||
|
||
// MARK: - Private Methods
|
||
private func performLogin() async throws -> Bool {
|
||
// 使用LoginHelper创建登录请求(包含DES加密)
|
||
guard let loginRequest = await LoginHelper.createIDLoginRequest(
|
||
userID: userID,
|
||
password: password
|
||
) else {
|
||
throw APIError.custom("DES加密失败")
|
||
}
|
||
|
||
let apiService = LiveAPIService()
|
||
let response: IDLoginResponse = try await apiService.request(loginRequest)
|
||
|
||
if response.code == 200, let data = response.data {
|
||
// 保存用户信息(如果API返回了用户信息)
|
||
if let userInfo = data.userInfo {
|
||
await UserInfoManager.saveUserInfo(userInfo)
|
||
}
|
||
|
||
// 创建并保存账户模型
|
||
guard let accountModel = AccountModel.from(loginData: data) else {
|
||
throw APIError.custom("账户信息无效")
|
||
}
|
||
await UserInfoManager.saveAccountModel(accountModel)
|
||
|
||
// 获取用户详细信息(如果API没有返回用户信息)
|
||
if data.userInfo == nil, let userInfo = await UserInfoManager.fetchUserInfoFromServer(
|
||
uid: String(data.uid ?? 0),
|
||
apiService: apiService
|
||
) {
|
||
await UserInfoManager.saveUserInfo(userInfo)
|
||
}
|
||
|
||
return true
|
||
} else {
|
||
throw APIError.custom(response.message ?? "Login failed")
|
||
}
|
||
}
|
||
|
||
private func handleLoginResult(_ success: Bool) {
|
||
isLoading = false
|
||
if success {
|
||
loginStep = .completed
|
||
onLoginSuccess?()
|
||
}
|
||
}
|
||
|
||
private func handleLoginError(_ error: Error) {
|
||
isLoading = false
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
// MARK: - IDLogin View
|
||
|
||
struct IDLoginPage: View {
|
||
@StateObject private var viewModel = IDLoginViewModel()
|
||
let onBack: () -> Void
|
||
let onLoginSuccess: () -> Void
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
ZStack {
|
||
// 背景
|
||
LoginBackgroundView()
|
||
|
||
VStack(spacing: 0) {
|
||
// 顶部导航栏
|
||
LoginHeaderView(onBack: {
|
||
viewModel.onBackTapped()
|
||
})
|
||
|
||
Spacer()
|
||
.frame(height: 60)
|
||
|
||
// 标题
|
||
Text("ID Login")
|
||
.font(.system(size: 28, weight: .bold))
|
||
.foregroundColor(.white)
|
||
.padding(.bottom, 60)
|
||
|
||
// 输入框区域
|
||
VStack(spacing: 24) {
|
||
// 用户ID输入框(只允许数字)
|
||
CustomInputField(
|
||
type: .number,
|
||
placeholder: "Please enter ID",
|
||
text: $viewModel.userID
|
||
)
|
||
|
||
// 密码输入框(带眼睛按钮)
|
||
CustomInputField(
|
||
type: .password,
|
||
placeholder: "Please enter password",
|
||
text: $viewModel.password,
|
||
isPasswordVisible: $viewModel.isPasswordVisible
|
||
)
|
||
}
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
.frame(height: 80)
|
||
|
||
// 忘记密码按钮
|
||
HStack {
|
||
Spacer()
|
||
Button(action: {
|
||
viewModel.onRecoverPasswordTapped()
|
||
}) {
|
||
Text("Forgot Password?")
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.white.opacity(0.8))
|
||
}
|
||
}
|
||
.padding(.horizontal, 32)
|
||
.padding(.bottom, 20)
|
||
|
||
// 登录按钮
|
||
LoginButtonView(
|
||
isLoading: viewModel.isLoading,
|
||
isEnabled: viewModel.isLoginButtonEnabled,
|
||
onTap: {
|
||
viewModel.onLoginTapped()
|
||
}
|
||
)
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
.navigationBarHidden(true)
|
||
.navigationDestination(isPresented: $viewModel.showRecoverPassword) {
|
||
RecoverPasswordPage(
|
||
onBack: {
|
||
viewModel.onRecoverPasswordBack()
|
||
}
|
||
)
|
||
.navigationBarHidden(true)
|
||
}
|
||
.onAppear {
|
||
viewModel.onBack = onBack
|
||
viewModel.onLoginSuccess = onLoginSuccess
|
||
|
||
#if DEBUG
|
||
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
||
#endif
|
||
}
|
||
.onChange(of: viewModel.loginStep) { _, newStep in
|
||
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
|
||
if newStep == .completed {
|
||
debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
IDLoginPage(
|
||
onBack: {},
|
||
onLoginSuccess: {}
|
||
)
|
||
}
|