// // ResetPasswordView.swift // punchnet // // Created by 安礼成 on 2026/3/9. // import SwiftUI import Observation // MARK: - 1. 根视图 struct ResetPasswordRootView: View { @State private var resetPasswordModel = ResetPasswordModel() @Environment(AppContext.self) private var appContext: AppContext var body: some View { ZStack(alignment: .center) { switch resetPasswordModel.stage { case .requestVerifyCode: GetVerifyCodeView() case .submitVerifyCode: SubmitVerifyCodeView() case .resetPassword: ResetPasswordView() 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) .overlay(alignment: .topLeading) { // 仅在非成功页面显示返回按钮 if resetPasswordModel.stage != .success { 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: - 2. 第一步:获取验证码 struct GetVerifyCodeView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel @State private var isProcessing = false @State private var showAlert = false @State private var errorMessage = "" var body: some View { @Bindable var model = resetPasswordModel VStack(spacing: 24) { headerSection(title: "重置密码", subtitle: "请输入关联的邮箱来验证身份") PunchTextField(icon: "person.crop.circle", placeholder: "邮箱", text: $model.username) .frame(width: 280) Button { Task { @MainActor in await self.sendVerifyCode(username: model.username) } } label: { Text("获取验证码") .fontWeight(.medium) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .frame(width: 280) .disabled(!SDLUtil.isValidIdentifyContact(model.username) || isProcessing) Spacer() } .padding(40) .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(errorMessage)) } } // 发送验证码 private func sendVerifyCode(username: String) async { self.isProcessing = true defer { self.isProcessing = false } do { let resetSession = try await resetPasswordModel.requestVerifyCode(username: username) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.resetPasswordModel.stage = .submitVerifyCode self.resetPasswordModel.sessionId = resetSession.sessionId self.resetPasswordModel.transitionEdge = .trailing } } catch { self.errorMessage = error.localizedDescription self.showAlert = true } } } // MARK: - 3. 第二步:验证验证码 struct SubmitVerifyCodeView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel @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: "验证码已发送至 \(self.resetPasswordModel.username)") VStack(alignment: .trailing, spacing: 16) { PunchTextField(icon: "envelope.badge", placeholder: "输入 6 位验证码", text: $code) Button(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") { Task { @MainActor in await self.resendAction(username: self.resetPasswordModel.username) } } .buttonStyle(.link) .font(.caption) .disabled(!isResendEnabled) } .frame(width: 280) VStack(spacing: 12) { Button { Task { @MainActor in await self.submitAction(sessionId: self.resetPasswordModel.sessionId) } } 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 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(username: String) async { _ = 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(sessionId: Int) async { self.isProcessing = true defer { self.isProcessing = false } 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 self.resetPasswordModel.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: - 4. 第三步:重置密码 struct ResetPasswordView: View { @Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel @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: "请为账号 \(self.resetPasswordModel.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 { Task { @MainActor in await self.handleReset(sessionId: self.resetPasswordModel.sessionId) } } 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(sessionId: Int) async { self.isProcessing = true defer { self.isProcessing = false } 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 = "重置失败, 请稍后重试" } } } struct ResetPasswordSuccessView: View { @Environment(AppContext.self) var appContext: AppContext @Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel // 动画状态 @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: { withAnimation(.spring(duration: 0.6, bounce: 0.2)) { self.appContext.appScene = .login(username: self.resetPasswordModel.username) } }) { 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 } } } }