初始提交

This commit is contained in:
2026-06-22 11:28:01 +08:00
commit 0a0d4fbd79
84 changed files with 8899 additions and 0 deletions

View File

@ -0,0 +1,213 @@
//
// LoginViewModel.swift
// suixinkan
//
// Created by Codex on 2026/6/18.
//
import Foundation
import Observation
///
enum LoginField: Hashable {
case username
case password
}
///
enum LoginValidationError: Equatable {
case invalidPhone
case emptyPassword
case privacyUnchecked
var message: String {
switch self {
case .invalidPhone:
"请输入有效的手机号码"
case .emptyPassword:
"请输入密码"
case .privacyUnchecked:
"请先阅读并同意相关协议"
}
}
var focusField: LoginField? {
switch self {
case .invalidPhone:
.username
case .emptyPassword:
.password
case .privacyUnchecked:
nil
}
}
}
///
enum LoginResolution {
case completed(V9AuthResponse)
case needsAccountSelection(AccountSelectionPayload)
}
/// token ID
enum LoginFlowError: LocalizedError {
case missingToken
case noAvailableAccount
case invalidAccount
var errorDescription: String? {
switch self {
case .missingToken:
"登录响应缺少 token"
case .noAvailableAccount:
"当前账号没有可用的景区账号或门店账号,请联系管理员"
case .invalidAccount:
"账号信息异常,请重新选择账号"
}
}
}
@MainActor
@Observable
/// ViewModel
final class LoginViewModel {
var username = "18651857230"
var password = "zhifly666"
var privacyChecked = false
var isLoading = false
var isSelectingAccount = false
var showsPassword = false
var showsAgreementSheet = false
var pendingAccountSelection: AccountSelectionPayload?
var canSubmit: Bool {
isValidPhone && !trimmedPassword.isEmpty
}
var trimmedPassword: String {
password.trimmingCharacters(in: .whitespacesAndNewlines)
}
var normalizedUsername: String {
normalizedPhoneNumber(username)
}
var isValidPhone: Bool {
normalizedUsername.count == 11 && normalizedUsername.first == "1"
}
///
func applyPreferences(_ preferences: LoginPreferences) {
if username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let lastUsername = preferences.lastUsername {
username = lastUsername
}
privacyChecked = preferences.privacyAgreementAccepted
}
///
func validateForLogin() -> LoginValidationError? {
guard isValidPhone else {
return .invalidPhone
}
guard !trimmedPassword.isEmpty else {
return .emptyPassword
}
guard privacyChecked else {
return .privacyUnchecked
}
return nil
}
///
func acceptAgreement() {
privacyChecked = true
showsAgreementSheet = false
}
/// +86 11
func normalizeUsernameCountryCodeIfNeeded() {
let digits = username.filter(\.isNumber)
let normalizedPhone = normalizedPhoneNumber(username)
guard normalizedPhone != digits, normalizedPhone.count == 11 else { return }
username = normalizedPhone
}
///
func login(authAPI: AuthAPI) async throws -> LoginResolution {
guard !isLoading else {
throw CancellationError()
}
isLoading = true
defer { isLoading = false }
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
defer { isSelectingAccount = false }
let response = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: payload.tempToken)
guard !response.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw LoginFlowError.missingToken
}
pendingAccountSelection = nil
return response
}
///
func clearPendingAccountSelection() {
pendingAccountSelection = nil
}
/// set-user
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)
}
let payload = AccountSelectionPayload(tempToken: token, accounts: accounts)
pendingAccountSelection = payload
return .needsAccountSelection(payload)
}
/// 86
private func normalizedPhoneNumber(_ value: String) -> String {
var digits = value.filter(\.isNumber)
if digits.hasPrefix("86"), digits.count == 13 {
digits.removeFirst(2)
}
return digits
}
}