// // 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) case .success: RegisterSuccessView() } } .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 let err as SDLAPIError { self.showAlert = true self.errorMessage = err.message } catch let err { self.showAlert = true self.errorMessage = err.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 == 6 && 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: "输入 6 位验证码", 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: self.code) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.registerModel.stage = .setPassword(sessionId: sessionId) self.registerModel.transitionEdge = .trailing } } catch let err as SDLAPIError { self.showAlert = true self.errorMessage = err.message } catch let err { self.showAlert = true self.errorMessage = err.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) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.registerModel.stage = .success self.registerModel.transitionEdge = .trailing } } catch let err as SDLAPIError { self.showAlert = true self.errorMessage = err.message } catch { self.showAlert = true self.errorMessage = "注册失败,重稍后重试" } self.isProcessing = false } } } // MARK: 第四步 注册成功 struct RegisterSuccessView: View { @Environment(\.dismiss) private var dismiss // 获取关闭窗口的能力 // MARK: - 动画状态 @State private var animateIcon: Bool = false // 用于呼吸灯效果 @State private var animateText: Bool = false // 用于文本延迟浮现 var body: some View { VStack(spacing: 32) { Spacer() // MARK: - 成功动画图标 (带呼吸灯效果) ZStack { // 外层呼吸圈 Circle() .fill(Color.green.opacity(0.1)) .frame(width: 100, height: 100) // 呼吸逻辑:缩放和透明度变化 .scaleEffect(animateIcon ? 1.1 : 0.95) .opacity(animateIcon ? 0.8 : 1.0) // 内层 Checkmark Image(systemName: "checkmark.seal.fill") .font(.system(size: 56)) .foregroundStyle(.green.gradient) // Checkmark 本身做轻微缩放 .scaleEffect(animateIcon ? 1.05 : 1.0) } // 整个图标组的弹出动画 .transition(.move(edge: .bottom).combined(with: .opacity)) // MARK: - 文本和按钮 VStack(spacing: 32) { VStack(spacing: 12) { Text("注册成功") .font(.title.bold()) Text("您的 PunchNet 账号已就绪。\n现在可以登录并开始构建您的私有网络了。") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } Button(action: { // 关闭当前注册窗口 dismiss() }) { Text("立即开始使用") .fontWeight(.bold) .frame(width: 200) } .buttonStyle(.borderedProminent) .controlSize(.large) .tint(.green) } // 文本和按钮比图标晚一点出现,形成层次感 .opacity(animateText ? 1.0 : 0.0) .offset(y: animateText ? 0 : 20) } .padding(40) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(NSColor.windowBackgroundColor)) // 确保背景干净 .onAppear { self.startAnimations() } } // MARK: - 动画逻辑 private func startAnimations() { // 1. 图标呼吸灯:无限循环,缓入缓出 withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { animateIcon = true } // 2. 文本延迟弹出:延迟 0.4 秒,使用弹簧效果 withAnimation(.spring(duration: 0.6, bounce: 0.3).delay(0.4)) { animateText = true } } } // 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) } } }