fix reset password

This commit is contained in:
anlicheng 2026-03-19 21:37:33 +08:00
parent ed6ae5c757
commit 85756fb6ee
3 changed files with 194 additions and 217 deletions

View File

@ -7,17 +7,19 @@
import Foundation import Foundation
import Observation import Observation
import SwiftUI
@Observable @Observable
class ResetPasswordModel { class ResetPasswordModel {
enum Stage { enum Stage {
case requestVerifyCode case requestVerifyCode(username: String?)
case submitVerifyCode(username: String) case submitVerifyCode(username: String)
case resetPassword(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] = [ private let baseParams: [String: Any] = [
"client_id": SystemConfig.getClientId(), "client_id": SystemConfig.getClientId(),

View File

@ -5,292 +5,273 @@
// Created by on 2026/3/9. // Created by on 2026/3/9.
// //
import SwiftUI import SwiftUI
import Observation
// MARK: - 1.
struct ResetPasswordRootView: View { struct ResetPasswordRootView: View {
@State private var resetPasswordModel = ResetPasswordModel() @State private var resetPasswordModel = ResetPasswordModel()
var body: some View { var body: some View {
Group { ZStack {
switch resetPasswordModel.stage { // (macOS )
case .requestVerifyCode: VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
GetVerifyCodeView() .ignoresSafeArea()
case .submitVerifyCode(let username):
SubmitVerifyCodeView(username: username) Group {
case .resetPassword(let username): switch resetPasswordModel.stage {
ResetPasswordView(username: username) 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) .environment(resetPasswordModel)
.frame(width: 400, height: 450)
} }
} }
// // MARK: - 2.
struct GetVerifyCodeView: View { struct GetVerifyCodeView: View {
@Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel @Environment(ResetPasswordModel.self) var resetPasswordModel
@State var username: String = ""
@State private var username: String = "" @State private var isProcessing = false
@State private var showAlert = false @State private var showAlert = false
@State private var errorMessage = "" @State private var errorMessage = ""
var body: some View { var body: some View {
VStack(spacing: 30) { VStack(spacing: 24) {
headerSection(title: "重置密码", subtitle: "请输入关联的手机号或邮箱来验证身份")
//
Text("重置密码") PunchTextField(icon: "person.crop.circle", placeholder: "手机号 / 邮箱", text: $username)
.font(.system(size: 18, weight: .medium)) .frame(width: 280)
VStack(alignment: .leading, spacing: 16) {
TextField("手机号/邮箱", text: $username)
.textFieldStyle(.plain)
.frame(width: 260, height: 28)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
}
Button { Button {
Task { @MainActor in self.sendVerifyCode()
//await self.sendVerifyCode()
withAnimation {
self.resetPasswordModel.stage = .submitVerifyCode(username: self.username)
}
}
} label: { } label: {
Text("获取验证码") Text("获取验证码")
.font(.system(size: 14)) .fontWeight(.medium)
.foregroundColor(.black) .frame(maxWidth: .infinity)
.frame(width: 160, height: 36)
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
} }
.frame(width: 160, height: 36) .buttonStyle(.borderedProminent)
.cornerRadius(6) .controlSize(.large)
.frame(width: 280)
.disabled(!SDLUtil.isValidIdentifyContact(username) || isProcessing)
Spacer() Spacer()
} }
.padding(.top, 40) .padding(40)
.frame(width: 400, height: 400)
.alert(isPresented: $showAlert) { .alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage)) Alert(title: Text("提示"), message: Text(errorMessage))
} }
} }
private func sendVerifyCode() async { //
if username.isEmpty { private func sendVerifyCode() {
self.showAlert = true self.isProcessing = true
self.errorMessage = "手机号/邮箱为空" Task { @MainActor in
return
}
switch SDLUtil.identifyContact(username) {
case .email, .phone:
do { do {
let result = try await self.resetPasswordModel.requestVerifyCode(username: username) _ = try await resetPasswordModel.requestVerifyCode(username: username)
print("send verify code result: \(result)") withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
resetPasswordModel.stage = .submitVerifyCode(username: username)
resetPasswordModel.transitionEdge = .trailing
}
} catch { } catch {
self.showAlert = true
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
self.showAlert = true
} }
self.isProcessing = false
case .invalid:
self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误"
} }
} }
} }
// // MARK: - 3.
struct SubmitVerifyCodeView: View { struct SubmitVerifyCodeView: View {
@Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel @Environment(ResetPasswordModel.self) var resetPasswordModel
let username: String
@State var username: String @State private var code: String = ""
@State private var verifiyCode: String = "" @State private var isProcessing = false
//
@State private var remainingSeconds = 60
@State private var isResendEnabled = false
//
@State private var showAlert = false @State private var showAlert = false
@State private var errorMessage = "" @State private var errorMessage = ""
var validInputCode: Bool {
return !self.code.isEmpty && self.code.count == 4 && self.code.allSatisfy {$0.isNumber}
}
var body: some View { var body: some View {
VStack(spacing: 30) { VStack(spacing: 24) {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
//
Text("重置密码") VStack(alignment: .trailing, spacing: 16) {
.font(.system(size: 18, weight: .medium)) PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code)
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
)
HStack { Button(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") {
TextField("验证码", text: $verifiyCode) self.resendAction()
.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)
} }
.buttonStyle(.link)
.font(.caption)
.disabled(!isResendEnabled)
} }
.frame(width: 280)
Button {
Task { @MainActor in VStack(spacing: 12) {
await self.submitVerifyCode() Button {
withAnimation { self.submitAction()
self.resetPasswordModel.stage = .resetPassword(username: username) } 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: { .buttonStyle(.plain)
Text("设置密码") .foregroundColor(.secondary)
.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) .frame(width: 280)
.cornerRadius(6)
Spacer() Spacer()
} }
.padding(.top, 40) .padding(40)
.frame(width: 400, height: 400) .task {
await startCountdown()
}
.alert(isPresented: $showAlert) { .alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage)) Alert(title: Text("提示"), message: Text(self.errorMessage))
} }
} }
private func submitVerifyCode() async { private func resendAction() {
if verifiyCode.isEmpty { Task {
self.showAlert = true _ = try? await resetPasswordModel.requestVerifyCode(username: username)
self.errorMessage = "请输入验证码" await startCountdown()
return
} }
}
if verifiyCode.count != 4 {
self.showAlert = true private func startCountdown() async {
self.errorMessage = "验证码错误" self.isResendEnabled = false
return self.remainingSeconds = 60
while remainingSeconds > 0 {
try? await Task.sleep(nanoseconds: 1_000_000_000)
self.remainingSeconds -= 1
} }
self.isResendEnabled = true
do { }
let result = try await self.resetPasswordModel.submitVerifyCode(username: username, verifyCode: verifiyCode)
print("submit verify code result: \(result)") private func submitAction() {
} catch { self.isProcessing = true
self.showAlert = true Task { @MainActor in
self.errorMessage = error.localizedDescription 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 { 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 = "" var passwordError: String? {
if password.isEmpty || confirm.isEmpty {
@State private var showAlert = false return nil
@State private var errorMessage = "" }
if password != confirm {
return "两次输入的密码不一致"
}
if password.count < 8 {
return "密码至少需要 8 位"
}
return nil
}
var body: some View { var body: some View {
VStack(spacing: 30) { VStack(spacing: 24) {
headerSection(title: "设置新密码", subtitle: "请为账号 \(username) 设置一个强密码")
//
Text("重置密码") VStack(spacing: 12) {
.font(.system(size: 18, weight: .medium)) PunchTextField(icon: "lock.shield", placeholder: "新密码 (至少8位)", text: $password, isSecure: true)
PunchTextField(icon: "lock.shield", placeholder: "确认新密码", text: $confirm, isSecure: true)
VStack(alignment: .leading, spacing: 16) {
SecureField("新密码", text: $password)
.textFieldStyle(.plain)
.frame(width: 260, height: 28)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
SecureField("再次输入密码", text: $confirmPassword) if let passwordError = self.passwordError {
.textFieldStyle(.plain) Text(passwordError)
.frame(width: 260, height: 28) .font(.caption)
.overlay( .foregroundColor(.red)
Rectangle() .frame(width: 280, alignment: .leading)
.frame(height: 1) }
.foregroundColor(.blue),
alignment: .bottom
)
} }
.frame(width: 280)
Button { Button {
Task { @MainActor in self.handleReset()
await self.resetPassword()
}
} label: { } label: {
Text("保存") Text("重置密码并登录")
.font(.system(size: 14)) .fontWeight(.medium)
.foregroundColor(.black) .frame(maxWidth: .infinity)
.frame(width: 160, height: 36)
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
} }
.frame(width: 160, height: 36) .buttonStyle(.borderedProminent)
.cornerRadius(6) .controlSize(.large)
.frame(width: 280)
.disabled(!isInputValid || isProcessing)
Spacer() Spacer()
} }
.padding(.top, 40) .padding(40)
.frame(width: 400, height: 400)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
} }
private func resetPassword() async { private func handleReset() {
if password.isEmpty { self.isProcessing = true
self.showAlert = true Task { @MainActor in
self.errorMessage = "请输入新密码" do {
return _ = try await resetPasswordModel.resetPassword(username: username, password: password)
} print("密码重置成功")
//
if confirmPassword.isEmpty || confirmPassword != password { } catch {
self.showAlert = true print("重置失败: \(error)")
self.errorMessage = "两次输入的密码不一致" }
return self.isProcessing = false
}
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
} }
} }

View File

@ -30,10 +30,6 @@ struct punchnetApp: App {
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @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 private var noticeServer: UDPNoticeCenterServer
@State private var appContext: AppContext @State private var appContext: AppContext
@State private var userContext = UserContext() @State private var userContext = UserContext()
@ -41,15 +37,13 @@ struct punchnetApp: App {
init() { init() {
self.noticeServer = UDPNoticeCenterServer() self.noticeServer = UDPNoticeCenterServer()
self.noticeServer.start() self.noticeServer.start()
self.appContext = AppContext(noticePort: self.noticeServer.port) self.appContext = AppContext(noticePort: self.noticeServer.port)
} }
var body: some Scene { var body: some Scene {
WindowGroup(id: "mainWindow") { WindowGroup(id: "mainWindow") {
// RootView() // RootView()
RegisterRootView() ResetPasswordRootView()
.frame(width: 800, height: 500)
.navigationTitle("") .navigationTitle("")
.environment(self.appContext) .environment(self.appContext)
.environment(self.userContext) .environment(self.userContext)