punchnet-macos/punchnet/Views/Register/RegisterView.swift
2026-03-19 20:43:35 +08:00

379 lines
12 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.

//
// ResetPasswordView.swift
// punchnet
//
// Created by on 2026/3/9.
//
import SwiftUI
import Observation
// MARK: -
struct RegisterRootView: View {
@State private var registerModel = RegisterModel()
var body: some View {
ZStack {
//
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
.ignoresSafeArea()
Group {
switch registerModel.stage {
case .requestVerifyCode(let username):
RegisterRequestVerifyCodeView(username: username ?? "")
case .submitVerifyCode(let username):
RegisterSubmitVerifyCodeView(username: username)
case .setPassword(let username):
RegisterSetPasswordView(username: username)
}
}
.transition(.asymmetric(insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)))
}
.environment(registerModel)
.frame(width: 400, height: 450)
}
}
// MARK: -
struct PunchTextField: View {
let icon: String
let placeholder: String
@Binding var text: String
var isSecure: Bool = false
var isDisabled: Bool = false
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.foregroundColor(.secondary)
.frame(width: 20)
if isSecure {
SecureField(placeholder, text: $text)
.textFieldStyle(.plain)
} else {
TextField(placeholder, text: $text)
.textFieldStyle(.plain)
.disabled(isDisabled)
}
}
.padding(10)
.background(Color.primary.opacity(isDisabled ? 0.02 : 0.05))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
)
}
}
// MARK: -
struct RegisterRequestVerifyCodeView: View {
@Environment(RegisterModel.self) var registerModel
@State var username: String = ""
@State private var isProcessing = false
//
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""
var body: some View {
VStack(spacing: 24) {
headerSection(title: "创建个人网络", subtitle: "输入手机号或邮箱开始注册")
VStack(spacing: 16) {
PunchTextField(icon: "person.crop.circle", placeholder: "手机号 / 邮箱", text: $username)
}
.frame(width: 280)
Button(action: {
self.requestVerifyCode()
}) {
if isProcessing {
ProgressView()
.controlSize(.small)
} else {
Text("获取验证码")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(width: 280)
.disabled(!SDLUtil.isValidIdentifyContact(username) || isProcessing)
Spacer()
}
.padding(40)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
private func requestVerifyCode() {
self.isProcessing = true
Task { @MainActor in
if username.isEmpty {
self.showAlert = true
self.errorMessage = "手机号/邮箱为空"
self.isProcessing = false
return
}
switch SDLUtil.identifyContact(username) {
case .email, .phone:
do {
let result = try await self.registerModel.requestVerifyCode(username: username)
print("send verify code result: \(result)")
withAnimation(.spring()) {
self.registerModel.stage = .submitVerifyCode(username: username)
}
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
case .invalid:
self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误"
}
self.isProcessing = false
}
}
}
// MARK: -
struct RegisterSubmitVerifyCodeView: View {
@Environment(RegisterModel.self) var registerModel
let username: String
@State private var code: String = ""
@State private var isProcessing = false
//
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""
// 使
@State private var isEnabled: Bool = false
@State private var remainingSeconds = 60
@State private var timer: Timer? = nil
var body: some View {
VStack(spacing: 24) {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
VStack(alignment: .trailing, spacing: 16) {
PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code)
Button {
self.resendVerifyCodeAction()
} label: {
if isEnabled {
Text("没有收到?重新获取")
} else {
Text("重新获取 (\(remainingSeconds)s)")
}
}
.buttonStyle(.link)
.font(.caption)
.disabled(!isEnabled) //
}
.frame(width: 280)
VStack(spacing: 12) {
Button(action: {
self.submitVerifyCode()
}) {
if isProcessing {
ProgressView()
.controlSize(.small)
} else {
Text("验证并设置密码")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
Button("返回上一步") {
withAnimation {
registerModel.stage = .requestVerifyCode(username: self.username)
}
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
}
.frame(width: 280)
Spacer()
}
.padding(40)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(errorMessage))
}
.task {
await self.startCountdown()
}
}
//
private func resendVerifyCodeAction() {
Task {
do {
let result = try await self.registerModel.requestVerifyCode(username: username)
print("send verify code result: \(result)")
} catch let err {
print("resend verify get error: \(err)")
}
//
await self.startCountdown()
}
}
//
private func startCountdown() async {
self.isEnabled = false
self.remainingSeconds = 60
for sec in (1...self.remainingSeconds).reversed() {
self.remainingSeconds = sec
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
self.isEnabled = true
}
//
private func submitVerifyCode() {
self.isProcessing = true
Task { @MainActor in
if self.code.isEmpty {
self.showAlert = true
self.errorMessage = "请输入验证码"
self.isProcessing = false
return
}
if self.code.count != 4 {
self.showAlert = true
self.errorMessage = "验证码错误"
self.isProcessing = false
return
}
do {
let result = try await self.registerModel.submitVerifyCode(username: username, verifyCode: self.code)
print("submit verify code result: \(result)")
withAnimation(.spring()) {
registerModel.stage = .setPassword(username: username)
}
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
self.isProcessing = false
}
}
}
// MARK: -
struct RegisterSetPasswordView: View {
@Environment(RegisterModel.self) var registerModel
let username: String
@State private var password = ""
@State private var confirm = ""
@State private var isProcessing = false
//
@State private var errorMessage: String?
//
var passwordError: String? {
if password.isEmpty || confirm.isEmpty {
return nil
}
if password != confirm {
return "两次输入的密码不一致"
}
if password.count < 8 {
return "密码至少需要 8 位"
}
return nil
}
var body: some View {
VStack(spacing: 24) {
headerSection(title: "设置安全密码", subtitle: "最后一步,请确保密码足够强大")
VStack(spacing: 12) {
PunchTextField(icon: "lock.shield", placeholder: "新密码", text: $password, isSecure: true)
PunchTextField(icon: "lock.shield", placeholder: "确认密码", text: $confirm, isSecure: true)
if let error = passwordError {
Text(error)
.font(.caption)
.foregroundColor(.red)
.frame(width: 280, alignment: .leading)
}
}
.frame(width: 280)
Button(action: {
self.handleRegister()
}) {
if isProcessing {
ProgressView()
.controlSize(.small)
} else {
Text("完成注册")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(width: 280)
.disabled(passwordError != nil)
Spacer()
}
.padding(40)
}
private func handleRegister() {
self.isProcessing = true
Task { @MainActor in
do {
let result = try await self.registerModel.register(username: username, password: self.password)
print("send verify code result: \(result)")
} catch {
self.errorMessage = error.localizedDescription
}
self.isProcessing = false
}
}
}
// MARK: -
extension View {
func headerSection(title: String, subtitle: String) -> some View {
VStack(spacing: 8) {
Image(systemName: "shield.lefthalf.filled")
.font(.system(size: 42))
.foregroundStyle(.blue.gradient)
Text(title)
.font(.title2.bold())
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
}