// // 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): SubmitVerifyCodeView(username: username) case .resetPassword(let username): ResetPasswordView(username: username) } } .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 { // _ = try await resetPasswordModel.requestVerifyCode(username: username) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { resetPasswordModel.stage = .submitVerifyCode(username: username) 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 @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 == 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(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 { // _ = try await resetPasswordModel.submitVerifyCode(username: username, verifyCode: code) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.resetPasswordModel.stage = .resetPassword(username: username) self.resetPasswordModel.transitionEdge = .trailing } } catch { self.errorMessage = error.localizedDescription self.showAlert = true } self.isProcessing = false } } } // MARK: - 4. 第三步:重置密码 struct ResetPasswordView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel let username: String @State private var password = "" @State private var confirm = "" @State private var isProcessing = false // 判断输入是否合法 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) } private func handleReset() { self.isProcessing = true Task { @MainActor in do { //_ = try await resetPasswordModel.resetPassword(username: username, password: password) print("密码重置成功") // 此处可添加重置成功后的跳转逻辑 } catch { print("重置失败: \(error)") } self.isProcessing = false } } }