2026-03-09 18:48:14 +08:00

393 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LoginView.swift
// punchnet
//
// Created by on 2026/1/15.
//
import SwiftUI
import Observation
enum ContactType {
case phone
case email
case invalid
}
enum AuthScreen {
case login
case resetPassword
case register
}
struct LoginRootView: View {
@Environment(AppContext.self) var appContext: AppContext
@State private var authScreen = AuthScreen.login
var body: some View {
Group {
switch self.authScreen {
case .login:
LoginView(authScreen: $authScreen)
case .resetPassword:
ResetPasswordView()
case .register:
ResetPasswordView()
}
}
.onChange(of: self.authScreen) {
print("app auth screen changed")
}
}
}
//
struct LoginView: View {
@Environment(UserContext.self) var userContext: UserContext
@State private var authMethod: AuthMethod = .account
@Binding var authScreen: AuthScreen
enum AuthMethod {
case token
case account
}
var body: some View {
VStack {
Text("PunchNet")
.font(.system(size: 16, weight: .medium))
HStack(alignment: .center, spacing: 30) {
Button {
self.authMethod = .token
} label: {
HStack {
Image("logo")
.resizable()
.clipped()
.frame(width: 25, height: 25)
Text("密钥登陆")
.foregroundColor(self.authMethod == .token ? .blue : .black)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Button {
self.authMethod = .account
} label: {
HStack {
Image("logo")
.resizable()
.clipped()
.frame(width: 25, height: 25)
Text("账户登陆")
.foregroundColor(self.authMethod == .account ? .blue : .black)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
Group {
switch self.authMethod {
case .token:
LoginTokenView()
case .account:
LoginAccountView(authScreen: $authScreen)
}
}
Spacer()
}
.frame(width: 400, height: 400)
}
}
extension LoginView {
struct LoginTokenView: View {
@Environment(UserContext.self) var userContext: UserContext
@State private var token: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
var body: some View {
VStack {
TextField("认证密钥", text: $token)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
Rectangle()
.overlay {
Text("登陆")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
}
.frame(width: 120, height: 35)
.foregroundColor(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
.onTapGesture {
Task {
await self.doLogin()
}
}
}
.onAppear {
if let cacheToken = self.userContext.loadCacheToken() {
self.token = cacheToken
}
}
}
//
private func doLogin() async {
do {
try await self.userContext.loginWithToken(token: self.token)
print(self.userContext.networkSession?.accessToken)
// KeychainStore
// let store = KeychainStore.shared
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
// try store.save(tokenData, account: self.username)
// }
} catch let err as SDLAPIError {
await MainActor.run {
self.showAlert = true
self.errorMessage = err.message
}
} catch {
await MainActor.run {
self.showAlert = true
self.errorMessage = "内部错误,请稍后重试"
}
}
}
}
struct LoginAccountView: View {
@Environment(UserContext.self) var userContext: UserContext
@Environment(AppContext.self) var appContext: AppContext
@Binding var authScreen: AuthScreen
@State private var username: String = ""
@State private var password: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
struct LoginResult: Decodable {
var accessToken: String
var networkId: Int
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case networkId = "network_id"
}
}
var body: some View {
VStack(alignment: .center) {
VStack(alignment: .leading) {
TextField("手机号/邮箱", text: $username)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
SecureField("密码", text: $password)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
Button {
self.authScreen = .resetPassword
} label: {
Text("忘记密码?")
.underline(true, color: .blue)
.font(.system(size: 14, weight: .regular))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
}
Button {
if self.username.isEmpty {
self.showAlert = true
self.errorMessage = "账号不能为空"
} else if self.password.isEmpty {
self.showAlert = true
self.errorMessage = "密码不能为空"
} else {
Task {
await self.doLogin()
}
}
} label: {
Text("登陆")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
.frame(width: 120, height: 35)
}
.frame(width: 120, height: 35)
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
}
.alert(isPresented: $showAlert) {
Alert(title: Text("错误提示"), message: Text("账号密码为空"))
}
.onAppear {
if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() {
self.username = cacheUsername
self.password = cachePassword
}
}
}
//
private func doLogin() async {
do {
try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password)
// KeychainStore
// let store = KeychainStore.shared
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
// try store.save(tokenData, account: self.username)
// }
} catch let err as SDLAPIError {
await MainActor.run {
self.showAlert = true
self.errorMessage = err.message
}
} catch {
await MainActor.run {
self.showAlert = true
self.errorMessage = "内部错误,请稍后重试"
}
}
}
}
}
//
struct ResetPasswordView: View {
@Environment(UserContext.self) var userContext: UserContext
@State private var username: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
var body: some View {
VStack {
Text("重置密码")
TextField("手机号/邮箱", text: $username)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
Button {
Task { @MainActor in
await self.sendVerifyCode()
}
} label: {
Text("获取验证码")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
.frame(width: 200, height: 35)
}
.frame(width: 200, height: 35)
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
}
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
private func sendVerifyCode() async {
if username.isEmpty {
self.showAlert = true
self.errorMessage = "手机号/邮箱为空"
return
}
switch identifyContact(username) {
case .email, .phone:
do {
let result = try await self.userContext.sendVerifyCode(username: username)
print("send verify code result: \(result)")
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
}
case .invalid:
self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误"
}
}
private func identifyContact(_ input: String) -> ContactType {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
// 1 11
let phoneRegex = /^1[3-9][0-9]{9}$/
//
let emailRegex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/
if trimmed.wholeMatch(of: phoneRegex) != nil {
return .phone
} else if trimmed.wholeMatch(of: emailRegex) != nil {
return .email
} else {
return .invalid
}
}
}
#Preview {
// ResetPasswordView()
// .environment(UserContext())
}