// // ResetPasswordView.swift // punchnet // // Created by 安礼成 on 2026/3/9. // import SwiftUI import Observation // MARK: - 注册根视图 struct RegisterRootView: View { @State private var registerModel = RegisterModel() @Environment(AppContext.self) private var appContext: AppContext var body: some View { ZStack(alignment: .center) { Color.clear ZStack(alignment: .center) { switch registerModel.stage { case .requestVerifyCode: RegisterRequestVerifyCodeView() case .submitVerifyCode: RegisterSubmitVerifyCodeView() case .setPassword: RegisterSetPasswordView() 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) // --- 核心改动:使用 overlay --- .overlay(alignment: .topLeading) { // 仅在非成功页面显示返回按钮 switch registerModel.stage { case .success: EmptyView() default: Button(action: { // 执行返回逻辑,例如重置到登录 withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.appContext.appScene = .login(username: nil) } }) { HStack { Image(systemName: "chevron.left") .font(.system(size: 16, weight: .semibold)) .padding(5) Text("首页") .font(.system(size: 16, weight: .regular)) } .contentShape(Rectangle()) // 扩大点击热区 } .buttonStyle(.plain) .padding([.top, .leading], 16) // 控制距离窗口边缘的边距 .transition(.opacity) // 按钮出现的动画 } } } } // 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: RegisterModel @State private var isProcessing = false // 错误提示 @State private var showAlert: Bool = false @State private var errorMessage: String = "" var body: some View { @Bindable var model = registerModel VStack(spacing: 24) { headerSection(title: "创建个人网络", subtitle: "输入邮箱开始注册") VStack(spacing: 16) { PunchTextField(icon: "person.crop.circle", placeholder: "邮箱", text: $model.username) } .frame(width: 280) Button(action: { Task { @MainActor in await self.requestVerifyCode(username: model.username) } }) { Text("获取验证码") .fontWeight(.medium) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .frame(width: 280) .disabled(!SDLUtil.isValidIdentifyContact(model.username) || isProcessing) } .padding(40) .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(self.errorMessage)) } } private func requestVerifyCode(username: String) async { self.isProcessing = true defer { self.isProcessing = false } if username.isEmpty { self.showAlert = true self.errorMessage = "邮箱为空" 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 self.registerModel.username = username self.registerModel.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 = "邮箱格式错误" } } } // MARK: - 第二步:验证 struct RegisterSubmitVerifyCodeView: View { @Environment(RegisterModel.self) var registerModel: RegisterModel @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: "验证码已发送至 \(registerModel.username)") VStack(alignment: .trailing, spacing: 16) { PunchTextField(icon: "envelope.badge", placeholder: "输入 6 位验证码", text: $code) Button { Task { @MainActor in await self.resendVerifyCodeAction(username: registerModel.username) } } label: { if isEnabled { Text("没有收到?重新获取") } else { Text("重新获取 (\(remainingSeconds)s)") } } .buttonStyle(.link) .font(.caption) .disabled(!isEnabled) // 倒计时期间禁用按钮 } .frame(width: 280) VStack(spacing: 12) { Button(action: { Task { @MainActor in await self.submitVerifyCode(sessionId: registerModel.sessionId) } }) { 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 self.registerModel.transitionEdge = .leading } } .buttonStyle(.plain) .foregroundColor(.secondary) } .frame(width: 280) } .padding(40) .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(errorMessage)) } .task { await self.startCountdown() } } // 重新发送验证码 private func resendVerifyCodeAction(username: String) async { 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(sessionId: Int) async { self.isProcessing = true defer { self.isProcessing = false } do { _ = try await self.registerModel.submitVerifyCode(sessionId: sessionId, verifyCode: self.code) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.registerModel.stage = .setPassword 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 } } } // MARK: - 第三步:设置密码 struct RegisterSetPasswordView: View { @Environment(RegisterModel.self) var registerModel: RegisterModel @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: { Task { @MainActor in await self.handleRegister(sessionId: registerModel.sessionId) } }) { if isProcessing { ProgressView() .controlSize(.small) } else { Text("完成注册") .fontWeight(.medium) .frame(maxWidth: .infinity) } } .buttonStyle(.borderedProminent) .controlSize(.large) .frame(width: 280) .disabled(passwordError != nil) } .padding(40) .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(self.errorMessage)) } } private func handleRegister(sessionId: Int) async { self.isProcessing = true defer { self.isProcessing = false } 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 = "注册失败,重稍后重试" } } } // MARK: 第四步 注册成功 struct RegisterSuccessView: View { @Environment(AppContext.self) private var appContext: AppContext @Environment(RegisterModel.self) private var registerModel: RegisterModel // 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: { // 关闭当前注册窗口 withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.appContext.appScene = .login(username: registerModel.username) } }) { 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) } } }