From 85756fb6eedf057d79d6c50c5695f4153dc33d81 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Thu, 19 Mar 2026 21:37:33 +0800 Subject: [PATCH] fix reset password --- .../ResetPassword/ResetPasswordModel.swift | 6 +- .../ResetPassword/ResetPasswordView.swift | 397 +++++++++--------- punchnet/punchnetApp.swift | 8 +- 3 files changed, 194 insertions(+), 217 deletions(-) diff --git a/punchnet/Views/ResetPassword/ResetPasswordModel.swift b/punchnet/Views/ResetPassword/ResetPasswordModel.swift index 0e0a01a..9941af3 100644 --- a/punchnet/Views/ResetPassword/ResetPasswordModel.swift +++ b/punchnet/Views/ResetPassword/ResetPasswordModel.swift @@ -7,17 +7,19 @@ import Foundation import Observation +import SwiftUI @Observable class ResetPasswordModel { enum Stage { - case requestVerifyCode + case requestVerifyCode(username: String?) case submitVerifyCode(username: String) case resetPassword(username: String) } - var stage: Stage = .requestVerifyCode + var stage: Stage = .requestVerifyCode(username: nil) + var transitionEdge: Edge = .trailing // 默认从右进入 private let baseParams: [String: Any] = [ "client_id": SystemConfig.getClientId(), diff --git a/punchnet/Views/ResetPassword/ResetPasswordView.swift b/punchnet/Views/ResetPassword/ResetPasswordView.swift index 6951762..7ce69ac 100644 --- a/punchnet/Views/ResetPassword/ResetPasswordView.swift +++ b/punchnet/Views/ResetPassword/ResetPasswordView.swift @@ -5,292 +5,273 @@ // Created by 安礼成 on 2026/3/9. // import SwiftUI +import Observation +// MARK: - 1. 根视图 struct ResetPasswordRootView: View { @State private var resetPasswordModel = ResetPasswordModel() var body: some View { - Group { - switch resetPasswordModel.stage { - case .requestVerifyCode: - GetVerifyCodeView() - case .submitVerifyCode(let username): - SubmitVerifyCodeView(username: username) - case .resetPassword(let username): - ResetPasswordView(username: username) + 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) + .frame(width: 400, height: 450) } } -// 获取验证码 +// MARK: - 2. 第一步:获取验证码 struct GetVerifyCodeView: View { - @Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel - - @State private var username: String = "" + @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: 30) { - - // 标题 - Text("重置密码") - .font(.system(size: 18, weight: .medium)) - - VStack(alignment: .leading, spacing: 16) { - TextField("手机号/邮箱", text: $username) - .textFieldStyle(.plain) - .frame(width: 260, height: 28) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue), - alignment: .bottom - ) - } - + VStack(spacing: 24) { + headerSection(title: "重置密码", subtitle: "请输入关联的手机号或邮箱来验证身份") + + PunchTextField(icon: "person.crop.circle", placeholder: "手机号 / 邮箱", text: $username) + .frame(width: 280) + Button { - Task { @MainActor in - //await self.sendVerifyCode() - withAnimation { - self.resetPasswordModel.stage = .submitVerifyCode(username: self.username) - } - } + self.sendVerifyCode() } label: { Text("获取验证码") - .font(.system(size: 14)) - .foregroundColor(.black) - .frame(width: 160, height: 36) - .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + .fontWeight(.medium) + .frame(maxWidth: .infinity) } - .frame(width: 160, height: 36) - .cornerRadius(6) + .buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(width: 280) + .disabled(!SDLUtil.isValidIdentifyContact(username) || isProcessing) Spacer() } - .padding(.top, 40) - .frame(width: 400, height: 400) + .padding(40) .alert(isPresented: $showAlert) { - Alert(title: Text("提示"), message: Text(self.errorMessage)) + Alert(title: Text("提示"), message: Text(errorMessage)) } } - private func sendVerifyCode() async { - if username.isEmpty { - self.showAlert = true - self.errorMessage = "手机号/邮箱为空" - return - } - - switch SDLUtil.identifyContact(username) { - case .email, .phone: + // 发送验证码 + private func sendVerifyCode() { + self.isProcessing = true + Task { @MainActor in do { - let result = try await self.resetPasswordModel.requestVerifyCode(username: username) - print("send verify code result: \(result)") + _ = try await resetPasswordModel.requestVerifyCode(username: username) + withAnimation(.spring(duration: 0.6, bounce: 0.2)) { + resetPasswordModel.stage = .submitVerifyCode(username: username) + resetPasswordModel.transitionEdge = .trailing + } } catch { - self.showAlert = true self.errorMessage = error.localizedDescription + self.showAlert = true } - - case .invalid: - self.showAlert = true - self.errorMessage = "手机号/邮箱格式错误" + self.isProcessing = false } } + } -// 输入验证码 +// MARK: - 3. 第二步:验证验证码 struct SubmitVerifyCodeView: View { - @Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel - - @State var username: String - @State private var verifiyCode: String = "" + @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: 30) { - - // 标题 - Text("重置密码") - .font(.system(size: 18, weight: .medium)) - - VStack(alignment: .leading, spacing: 16) { - TextField("手机号/邮箱", text: $username) - .textFieldStyle(.plain) - .frame(width: 260, height: 28) - .disabled(true) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue), - alignment: .bottom - ) + VStack(spacing: 24) { + headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)") + + VStack(alignment: .trailing, spacing: 16) { + PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code) - HStack { - TextField("验证码", text: $verifiyCode) - .textFieldStyle(.plain) - .frame(width: 260, height: 28) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue), - alignment: .bottom - ) - Spacer() - - Button { - Task { @MainActor in - //await self.sendVerifyCode() - } - } label: { - Text("再次获取") - .font(.system(size: 14)) - .foregroundColor(.black) - .frame(width: 160, height: 36) - .background(Color(red: 74/255, green: 207/255, blue: 154/255)) - } - .frame(width: 160, height: 36) - .cornerRadius(6) + Button(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") { + self.resendAction() } + .buttonStyle(.link) + .font(.caption) + .disabled(!isResendEnabled) } - - Button { - Task { @MainActor in - await self.submitVerifyCode() - withAnimation { - self.resetPasswordModel.stage = .resetPassword(username: username) + .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 } } - } label: { - Text("设置密码") - .font(.system(size: 14)) - .foregroundColor(.black) - .frame(width: 160, height: 36) - .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + .buttonStyle(.plain) + .foregroundColor(.secondary) } - .frame(width: 160, height: 36) - .cornerRadius(6) + .frame(width: 280) Spacer() } - .padding(.top, 40) - .frame(width: 400, height: 400) + .padding(40) + .task { + await startCountdown() + } .alert(isPresented: $showAlert) { Alert(title: Text("提示"), message: Text(self.errorMessage)) } } - private func submitVerifyCode() async { - if verifiyCode.isEmpty { - self.showAlert = true - self.errorMessage = "请输入验证码" - return + private func resendAction() { + Task { + _ = try? await resetPasswordModel.requestVerifyCode(username: username) + await startCountdown() } - - if verifiyCode.count != 4 { - self.showAlert = true - self.errorMessage = "验证码错误" - return + } + + 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 } - - do { - let result = try await self.resetPasswordModel.submitVerifyCode(username: username, verifyCode: verifiyCode) - print("submit verify code result: \(result)") - } catch { - self.showAlert = true - self.errorMessage = error.localizedDescription + 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: ResetPasswordModel + @Environment(ResetPasswordModel.self) var resetPasswordModel + let username: String + @State private var password = "" + @State private var confirm = "" + @State private var isProcessing = false - var username: String + // 判断输入是否合法 + var isInputValid: Bool { + !password.isEmpty && password == confirm && password.count >= 8 + } - @State private var password: String = "" - @State private var confirmPassword: String = "" - - @State private var showAlert = false - @State private var errorMessage = "" + // 提示错误信息 + 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: 30) { - - // 标题 - Text("重置密码") - .font(.system(size: 18, weight: .medium)) - - VStack(alignment: .leading, spacing: 16) { - SecureField("新密码", text: $password) - .textFieldStyle(.plain) - .frame(width: 260, height: 28) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue), - alignment: .bottom - ) + 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) - SecureField("再次输入密码", text: $confirmPassword) - .textFieldStyle(.plain) - .frame(width: 260, height: 28) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue), - alignment: .bottom - ) + 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.resetPassword() - } + self.handleReset() } label: { - Text("保存") - .font(.system(size: 14)) - .foregroundColor(.black) - .frame(width: 160, height: 36) - .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + Text("重置密码并登录") + .fontWeight(.medium) + .frame(maxWidth: .infinity) } - .frame(width: 160, height: 36) - .cornerRadius(6) + .buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(width: 280) + .disabled(!isInputValid || isProcessing) Spacer() } - .padding(.top, 40) - .frame(width: 400, height: 400) - .alert(isPresented: $showAlert) { - Alert(title: Text("提示"), message: Text(self.errorMessage)) - } + .padding(40) } - private func resetPassword() async { - if password.isEmpty { - self.showAlert = true - self.errorMessage = "请输入新密码" - return - } - - if confirmPassword.isEmpty || confirmPassword != password { - self.showAlert = true - self.errorMessage = "两次输入的密码不一致" - return - } - - do { - let result = try await self.resetPasswordModel.resetPassword(username: username, password: self.password) - print("send verify code result: \(result)") - } catch { - self.showAlert = true - self.errorMessage = error.localizedDescription + 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 } } diff --git a/punchnet/punchnetApp.swift b/punchnet/punchnetApp.swift index dccd7db..71b0d3a 100644 --- a/punchnet/punchnetApp.swift +++ b/punchnet/punchnetApp.swift @@ -30,10 +30,6 @@ struct punchnetApp: App { @Environment(\.openWindow) private var openWindow @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - @AppStorage("token") var token: String = "" - @AppStorage("network_code") var networkCode: String = "" - @AppStorage("hostname") var hostname: String = "" - private var noticeServer: UDPNoticeCenterServer @State private var appContext: AppContext @State private var userContext = UserContext() @@ -41,15 +37,13 @@ struct punchnetApp: App { init() { self.noticeServer = UDPNoticeCenterServer() self.noticeServer.start() - self.appContext = AppContext(noticePort: self.noticeServer.port) } var body: some Scene { WindowGroup(id: "mainWindow") { // RootView() - RegisterRootView() - .frame(width: 800, height: 500) + ResetPasswordRootView() .navigationTitle("") .environment(self.appContext) .environment(self.userContext)