punchnet-macos/punchnet/Views/Register/RegisterView.swift
2026-03-20 11:52:11 +08:00

372 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, let sessionId):
RegisterSubmitVerifyCodeView(username: username, sessionId: sessionId)
case .setPassword(let sessionId):
RegisterSetPasswordView(sessionId: sessionId)
}
}
.transition(.asymmetric(
insertion: .move(edge: registerModel.transitionEdge).combined(with: .opacity),
removal: .move(edge: registerModel.transitionEdge == .trailing ? .leading : .trailing).combined(with: .opacity)
))
}
.environment(registerModel)
}
}
// 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()
}) {
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:
do {
let registerSession = try await self.registerModel.requestVerifyCode(username: username)
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.registerModel.stage = .submitVerifyCode(username: username, sessionId: registerSession.sessionId)
self.registerModel.transitionEdge = .trailing
}
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
default:
self.showAlert = true
self.errorMessage = "邮箱格式错误"
}
self.isProcessing = false
}
}
}
// MARK: -
struct RegisterSubmitVerifyCodeView: View {
@Environment(RegisterModel.self) var registerModel
let username: String
let sessionId: Int
@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 validInputCode: Bool {
return !self.code.isEmpty && self.code.count == 4 && self.code.allSatisfy {$0.isNumber}
}
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()
}) {
Text("验证并设置密码")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.disabled(!self.validInputCode)
Button("返回上一步") {
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.registerModel.stage = .requestVerifyCode(username: self.username)
self.registerModel.transitionEdge = .leading
}
}
.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
do {
_ = try await self.registerModel.submitVerifyCode(sessionId: sessionId, verifyCode: Int(self.code) ?? 0)
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.registerModel.stage = .setPassword(sessionId: sessionId)
self.registerModel.transitionEdge = .trailing
}
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
self.isProcessing = false
}
}
}
// MARK: -
struct RegisterSetPasswordView: View {
@Environment(RegisterModel.self) var registerModel
let sessionId: Int
@State private var password = ""
@State private var confirm = ""
@State private var isProcessing = false
//
@State private var showAlert: Bool = 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)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
private func handleRegister() {
self.isProcessing = true
Task { @MainActor in
do {
_ = try await self.registerModel.register(sessionId: sessionId, password: self.password)
self.showAlert = true
self.errorMessage = "注册成功"
} catch {
self.showAlert = true
self.errorMessage = "注册失败,重稍后重试"
}
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)
}
}
}