// // ResetPasswordView.swift // punchnet // // Created by 安礼成 on 2026/3/9. // import SwiftUI import Observation // MARK: - 1. 根视图 struct ResetPasswordRootView: View { @State private var resetPasswordModel = ResetPasswordModel() var body: some View { ZStack { // 背景毛玻璃 (macOS 风格) VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow) .ignoresSafeArea() Group { switch resetPasswordModel.stage { case .requestVerifyCode(let username): GetVerifyCodeView(username: username ?? "") case .submitVerifyCode(let username, let sessionId): SubmitVerifyCodeView(username: username, sessionId: sessionId) case .resetPassword(let username, let sessionId): ResetPasswordView(username: username, sessionId: sessionId) case .success: ResetPasswordSuccessView() } } .transition(.asymmetric( insertion: .move(edge: resetPasswordModel.transitionEdge).combined(with: .opacity), removal: .move(edge: resetPasswordModel.transitionEdge == .trailing ? .leading : .trailing).combined(with: .opacity) )) } .environment(resetPasswordModel) } } // MARK: - 2. 第一步:获取验证码 struct GetVerifyCodeView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel @State var username: String = "" @State private var isProcessing = false @State private var showAlert = false @State private var errorMessage = "" var body: some View { VStack(spacing: 24) { headerSection(title: "重置密码", subtitle: "请输入关联的邮箱来验证身份") PunchTextField(icon: "person.crop.circle", placeholder: "邮箱", text: $username) .frame(width: 280) Button { self.sendVerifyCode() } label: { 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(errorMessage)) } } // 发送验证码 private func sendVerifyCode() { self.isProcessing = true Task { @MainActor in do { let resetSession = try await resetPasswordModel.requestVerifyCode(username: username) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { resetPasswordModel.stage = .submitVerifyCode(username: username, sessionId: resetSession.sessionId) resetPasswordModel.transitionEdge = .trailing } } catch { self.errorMessage = error.localizedDescription self.showAlert = true } self.isProcessing = false } } } // MARK: - 3. 第二步:验证验证码 struct SubmitVerifyCodeView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel let username: String let sessionId: Int @State private var code: String = "" @State private var isProcessing = false // 重发逻辑 @State private var remainingSeconds = 60 @State private var isResendEnabled = false // 错误逻辑处理 @State private var showAlert = false @State private var errorMessage = "" 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(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") { self.resendAction() } .buttonStyle(.link) .font(.caption) .disabled(!isResendEnabled) } .frame(width: 280) VStack(spacing: 12) { Button { self.submitAction() } label: { Text("验证并继续") .fontWeight(.medium) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .disabled(!validInputCode) Button("返回上一步") { withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.resetPasswordModel.stage = .requestVerifyCode(username: self.username) self.resetPasswordModel.transitionEdge = .leading } } .buttonStyle(.plain) .foregroundColor(.secondary) } .frame(width: 280) Spacer() } .padding(40) .task { await startCountdown() } .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(self.errorMessage)) } } private func resendAction() { Task { _ = try? await resetPasswordModel.requestVerifyCode(username: username) await startCountdown() } } private func startCountdown() async { self.isResendEnabled = false self.remainingSeconds = 60 while remainingSeconds > 0 { try? await Task.sleep(nanoseconds: 1_000_000_000) self.remainingSeconds -= 1 } self.isResendEnabled = true } private func submitAction() { self.isProcessing = true Task { @MainActor in do { let result = try await resetPasswordModel.submitVerifyCode(sessionId: sessionId, verifyCode: code) NSLog("reset password submit verify code result: \(result)") withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.resetPasswordModel.stage = .resetPassword(username: username, sessionId: sessionId) self.resetPasswordModel.transitionEdge = .trailing } } catch let err as SDLAPIError { self.errorMessage = err.message self.showAlert = true } self.isProcessing = false } } } // MARK: - 4. 第三步:重置密码 struct ResetPasswordView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel let username: String let sessionId: Int @State private var password = "" @State private var confirm = "" @State private var isProcessing = false // 错误逻辑处理 @State private var showAlert = false @State private var errorMessage = "" // 判断输入是否合法 var isInputValid: Bool { !password.isEmpty && password == confirm && password.count >= 8 } // 提示错误信息 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: "请为账号 \(username) 设置一个强密码") VStack(spacing: 12) { PunchTextField(icon: "lock.shield", placeholder: "新密码 (至少8位)", text: $password, isSecure: true) PunchTextField(icon: "lock.shield", placeholder: "确认新密码", text: $confirm, isSecure: true) if let passwordError = self.passwordError { Text(passwordError) .font(.caption) .foregroundColor(.red) .frame(width: 280, alignment: .leading) } } .frame(width: 280) Button { self.handleReset() } label: { Text("重置密码并登录") .fontWeight(.medium) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .frame(width: 280) .disabled(!isInputValid || isProcessing) Spacer() } .padding(40) .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(self.errorMessage)) } } private func handleReset() { self.isProcessing = true Task { @MainActor in do { let result = try await resetPasswordModel.resetPassword(sessionId: sessionId, newPassword: password) print("密码重置成功: \(result)") withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.resetPasswordModel.stage = .success self.resetPasswordModel.transitionEdge = .trailing } } catch { self.showAlert = true self.errorMessage = "重置失败, 请稍后重试" } self.isProcessing = false } } } struct ResetPasswordSuccessView: View { @Environment(\.dismiss) private var dismiss // 动画状态 @State private var animateIcon = false @State private var animateText = false var body: some View { VStack(spacing: 32) { Spacer() // 成功图标 (使用蓝紫色调区别于注册的绿色) ZStack { Circle() .fill(Color.blue.opacity(0.1)) .frame(width: 100, height: 100) .scaleEffect(animateIcon ? 1.1 : 0.95) .opacity(animateIcon ? 0.8 : 1.0) Image(systemName: "lock.circle.fill") .font(.system(size: 56)) .foregroundStyle(.blue.gradient) .scaleEffect(animateIcon ? 1.05 : 1.0) } .transition(.move(edge: .bottom).combined(with: .opacity)) VStack(spacing: 32) { VStack(spacing: 12) { Text("密码重置成功") .font(.title2.bold()) Text("您的新密码已生效。\n为了安全,建议您立即尝试使用新密码登录。") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } Button(action: { dismiss() // 关闭重置窗口 }) { Text("返回登录") .fontWeight(.bold) .frame(width: 200) } .buttonStyle(.borderedProminent) .controlSize(.large) .tint(.blue) } .opacity(animateText ? 1.0 : 0.0) .offset(y: animateText ? 0 : 20) Spacer() } .padding(40) .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { animateIcon = true } withAnimation(.spring(duration: 0.6, bounce: 0.3).delay(0.4)) { animateText = true } } } }