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>
111 lines
3.7 KiB
Swift
111 lines
3.7 KiB
Swift
import SwiftUI
|
|
|
|
/// 短信登录页(sheet 形式)。登录成功后 dismiss,守卫页因 session 变化自动切到已登录内容。
|
|
struct LoginView: View {
|
|
@Environment(SessionStore.self) private var session
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var vm = LoginViewModel()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 24) {
|
|
header
|
|
|
|
VStack(spacing: 14) {
|
|
phoneField
|
|
codeRow
|
|
}
|
|
|
|
loginButton
|
|
|
|
if vm.isMock {
|
|
Text("体验模式:任意 6 位验证码即可登录")
|
|
.font(.caption)
|
|
.foregroundStyle(AppColors.warning)
|
|
}
|
|
if let err = vm.errorMessage {
|
|
Text(err).font(.footnote).foregroundStyle(AppColors.error)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(24)
|
|
.navigationTitle("手机号登录")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("关闭") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var header: some View {
|
|
VStack(spacing: 10) {
|
|
Image(systemName: "tag.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(AppColors.brand)
|
|
Text("登录后查看你的省钱数据")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.top, 12)
|
|
}
|
|
|
|
private var phoneField: some View {
|
|
TextField("请输入手机号", text: $vm.phone)
|
|
.keyboardType(.numberPad)
|
|
.textContentType(.telephoneNumber)
|
|
.padding()
|
|
.background(AppColors.gray100)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.onChange(of: vm.phone) { _, new in
|
|
vm.phone = String(new.filter(\.isNumber).prefix(11))
|
|
}
|
|
}
|
|
|
|
private var codeRow: some View {
|
|
HStack(spacing: 12) {
|
|
TextField("验证码", text: $vm.code)
|
|
.keyboardType(.numberPad)
|
|
.textContentType(.oneTimeCode)
|
|
.padding()
|
|
.background(AppColors.gray100)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.onChange(of: vm.code) { _, new in
|
|
vm.code = String(new.filter(\.isNumber).prefix(6))
|
|
}
|
|
|
|
Button {
|
|
Task { await vm.sendCode() }
|
|
} label: {
|
|
Text(vm.countdown > 0 ? "\(vm.countdown)s" : "发送验证码")
|
|
.font(.subheadline)
|
|
.frame(width: 100)
|
|
.padding(.vertical, 14)
|
|
.background(vm.canSendCode ? AppColors.primary : AppColors.gray200)
|
|
.foregroundStyle(vm.canSendCode ? .black : AppColors.gray500)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.disabled(!vm.canSendCode)
|
|
}
|
|
}
|
|
|
|
private var loginButton: some View {
|
|
Button {
|
|
Task {
|
|
if await vm.login(into: session) { dismiss() }
|
|
}
|
|
} label: {
|
|
Text(vm.isLoggingIn ? "登录中…" : "登录")
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 16)
|
|
.background(vm.canLogin ? AppColors.primary : AppColors.gray200)
|
|
.foregroundStyle(vm.canLogin ? .black : AppColors.gray500)
|
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
|
}
|
|
.disabled(!vm.canLogin)
|
|
}
|
|
}
|