// // LoginViewModel.swift // suixinkan // import Foundation /// 登录页 ViewModel,UI 状态与 Android `LoginViewModel` 对齐。 final class LoginViewModel { private(set) var account = "" private(set) var password = "" private(set) var isPrivacyChecked = false private(set) var enableRegister = false private(set) var isLoading = false private(set) var isSelectingAccount = false private(set) var pendingAccountSelection: AccountSelectionPayload? var isLoginEnabled: Bool { isValidPhone && !trimmedPassword.isEmpty } var trimmedPassword: String { password.trimmingCharacters(in: .whitespacesAndNewlines) } var normalizedUsername: String { normalizedPhoneNumber(account) } var isValidPhone: Bool { normalizedUsername.count == 11 && normalizedUsername.first == "1" } var onStateChange: (() -> Void)? /// 从本地存储恢复上次登录手机号与协议勾选状态。 func applyStoredPreferences() { if account.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, let lastUsername = AppStore.shared.session.lastLoginUsername { account = lastUsername } isPrivacyChecked = AppStore.shared.session.privacyAgreementAccepted notifyStateChange() } func updateAccount(_ value: String) { account = value notifyStateChange() } func updatePassword(_ value: String) { password = value notifyStateChange() } func togglePrivacyChecked() { isPrivacyChecked.toggle() notifyStateChange() } func updateEnableRegister(_ enabled: Bool) { enableRegister = enabled notifyStateChange() } /// 当输入包含 +86 前缀时,把手机号规范化为 11 位国内手机号。 func normalizeUsernameCountryCodeIfNeeded() { let digits = account.filter(\.isNumber) let normalizedPhone = normalizedPhoneNumber(account) guard normalizedPhone != digits, normalizedPhone.count == 11 else { return } account = normalizedPhone notifyStateChange() } /// 校验登录表单,并返回第一个需要处理的错误。 func validateForLogin() -> LoginValidationError? { guard isValidPhone else { return .invalidPhone } guard !trimmedPassword.isEmpty else { return .emptyPassword } guard isPrivacyChecked else { return .privacyUnchecked } return nil } func login() { if let error = validateForLogin() { onShowMessage?(error.message) return } onLogin?() } /// 加载 App 配置,控制验证码登录/注册入口显隐。 func loadAppConfig(authAPI: AuthAPI) async { do { let config = try await authAPI.getAppConfig() updateEnableRegister(config.enableRegister) } catch { // 配置加载失败不影响密码登录,静默忽略。 } } /// 发起登录,并根据账号数量决定直接完成或进入账号选择。 func login(authAPI: AuthAPI) async throws -> LoginResolution { guard !isLoading else { throw CancellationError() } isLoading = true notifyStateChange() defer { isLoading = false notifyStateChange() } let response = try await authAPI.login( username: normalizedUsername, password: trimmedPassword ) return try await resolveLoginResponse(response, authAPI: authAPI) } /// 选择一个账号并调用 set-user 换取正式 token。 func selectAccount(_ account: AccountSwitchAccount, authAPI: AuthAPI) async throws -> V9AuthResponse { guard let payload = pendingAccountSelection, payload.hasTempToken else { throw LoginFlowError.missingToken } guard account.businessUserId > 0 else { throw LoginFlowError.invalidAccount } guard !isSelectingAccount else { throw CancellationError() } isSelectingAccount = true notifyStateChange() defer { isSelectingAccount = false notifyStateChange() } let response = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: payload.tempToken) guard !response.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw LoginFlowError.missingToken } pendingAccountSelection = nil notifyStateChange() return response } /// 清空待选账号信息,通常在用户取消账号选择时调用。 func clearPendingAccountSelection() { pendingAccountSelection = nil notifyStateChange() } var onShowMessage: ((String) -> Void)? var onLogin: (() -> Void)? var onLoginByCode: (() -> Void)? var onRegister: (() -> Void)? var onUserAgreement: (() -> Void)? var onPrivacyPolicy: (() -> Void)? private func resolveLoginResponse(_ response: V9AuthResponse, authAPI: AuthAPI) async throws -> LoginResolution { let token = response.token.trimmingCharacters(in: .whitespacesAndNewlines) guard !token.isEmpty else { throw LoginFlowError.missingToken } let accounts = response.accounts.filter { $0.businessUserId > 0 } guard !accounts.isEmpty else { throw LoginFlowError.noAvailableAccount } if accounts.count == 1, let account = accounts.first { let finalResponse = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: token) guard !finalResponse.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw LoginFlowError.missingToken } return .completed(finalResponse, account) } let payload = AccountSelectionPayload(tempToken: token, accounts: accounts) pendingAccountSelection = payload notifyStateChange() return .needsAccountSelection(payload) } private func normalizedPhoneNumber(_ value: String) -> String { var digits = value.filter(\.isNumber) if digits.hasPrefix("86"), digits.count == 13 { digits.removeFirst(2) } return digits } private func notifyStateChange() { onStateChange?() } }