98b7600797
对标安卓正式版、对接 app-server(8770)的 SwiftUI 客户端。iOS 无障碍受限、无比价核心,做自成一体:美团 CPS 导购 + 福利(签到/任务/金币)+ 查看(比价记录/省钱战绩)。 功能完整 M0-M5:脚手架、短信登录(JWT+Keychain+401 自动刷新)、首页券 feed 导购、比价记录+省钱战绩、福利、我的(资料/反馈/设置/注销)。 单 target、0 三方依赖、XcodeGen 生成工程;Debug/Release 双 plist 分流 ATS。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.3 KiB
Swift
68 lines
2.3 KiB
Swift
import SwiftUI
|
|
|
|
/// 短信登录状态机(对齐安卓 SmsLoginViewModel)。一键登录首版不做。
|
|
@MainActor
|
|
@Observable
|
|
final class LoginViewModel {
|
|
var phone: String = ""
|
|
var code: String = ""
|
|
var countdown: Int = 0
|
|
var isSending = false
|
|
var isLoggingIn = false
|
|
var isMock = false
|
|
var errorMessage: String?
|
|
|
|
private let auth = AuthService()
|
|
private var timerTask: Task<Void, Never>?
|
|
|
|
var isValidPhone: Bool { phone.count == 11 && phone.hasPrefix("1") }
|
|
var canSendCode: Bool { countdown == 0 && !isSending && isValidPhone }
|
|
var canLogin: Bool { isValidPhone && code.count >= 4 && !isLoggingIn }
|
|
|
|
func sendCode() async {
|
|
guard canSendCode else { return }
|
|
errorMessage = nil
|
|
isSending = true
|
|
defer { isSending = false }
|
|
do {
|
|
let resp = try await auth.smsSend(phone: phone)
|
|
isMock = resp.mock
|
|
startCountdown(resp.cooldownSec > 0 ? resp.cooldownSec : 60)
|
|
} catch {
|
|
errorMessage = (error as? LocalizedError)?.errorDescription ?? "验证码发送失败,请重试"
|
|
}
|
|
}
|
|
|
|
/// 登录成功则已写入 session,返回 true(供 View 关闭页面)。
|
|
func login(into session: SessionStore) async -> Bool {
|
|
guard canLogin else { return false }
|
|
errorMessage = nil
|
|
isLoggingIn = true
|
|
defer { isLoggingIn = false }
|
|
do {
|
|
let resp = try await auth.smsLogin(phone: phone, code: code)
|
|
await session.loginSucceeded(resp)
|
|
return true
|
|
} catch APIError.http(let status, _) where status == 400 {
|
|
errorMessage = "验证码错误或已失效,请重新获取"
|
|
return false
|
|
} catch {
|
|
errorMessage = (error as? LocalizedError)?.errorDescription ?? "登录失败,请重试"
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func startCountdown(_ seconds: Int) {
|
|
countdown = seconds
|
|
timerTask?.cancel()
|
|
timerTask = Task { @MainActor [weak self] in
|
|
while true {
|
|
do { try await Task.sleep(for: .seconds(1)) }
|
|
catch { return } // 被 cancel(重发时)→ 立即退出,不再误减新一轮计时
|
|
guard let self, self.countdown > 0 else { return }
|
|
self.countdown -= 1
|
|
}
|
|
}
|
|
}
|
|
}
|