重置密码

This commit is contained in:
anlicheng 2026-03-09 22:35:23 +08:00
parent 89bff2f97b
commit b12695d5bd
3 changed files with 493 additions and 245 deletions

View File

@ -24,9 +24,9 @@ struct LoginRootView: View {
case .login:
LoginView(authScreen: $authScreen)
case .resetPassword:
ResetPasswordView()
ResetPasswordRootView()
case .register:
ResetPasswordView()
EmptyView()
}
}
.onChange(of: self.authScreen) {
@ -101,18 +101,103 @@ struct LoginView: View {
}
extension LoginView {
struct LoginTokenView: View {
@Environment(UserContext.self) var userContext: UserContext
@State private var token: String = ""
struct LoginTokenView: View {
@Environment(UserContext.self) var userContext: UserContext
@State private var token: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
@State private var showAlert = false
@State private var errorMessage = ""
var body: some View {
VStack {
TextField("认证密钥", text: $token)
var body: some View {
VStack {
TextField("认证密钥", text: $token)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
Rectangle()
.overlay {
Text("登陆")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
}
.frame(width: 120, height: 35)
.foregroundColor(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
.onTapGesture {
Task {
await self.doLogin()
}
}
}
.onAppear {
if let cacheToken = self.userContext.loadCacheToken() {
self.token = cacheToken
}
}
}
//
private func doLogin() async {
do {
try await self.userContext.loginWithToken(token: self.token)
print(self.userContext.networkSession?.accessToken)
// KeychainStore
// let store = KeychainStore.shared
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
// try store.save(tokenData, account: self.username)
// }
} catch let err as SDLAPIError {
await MainActor.run {
self.showAlert = true
self.errorMessage = err.message
}
} catch {
await MainActor.run {
self.showAlert = true
self.errorMessage = "内部错误,请稍后重试"
}
}
}
}
struct LoginAccountView: View {
@Environment(UserContext.self) var userContext: UserContext
@Environment(AppContext.self) var appContext: AppContext
@Binding var authScreen: AuthScreen
@State private var username: String = ""
@State private var password: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
struct LoginResult: Decodable {
var accessToken: String
var networkId: Int
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case networkId = "network_id"
}
}
var body: some View {
VStack(alignment: .center) {
VStack(alignment: .leading) {
TextField("手机号/邮箱", text: $username)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
@ -125,250 +210,90 @@ extension LoginView {
.padding(.top, 25)
, alignment: .topLeading)
Rectangle()
.overlay {
Text("登陆")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
}
.frame(width: 120, height: 35)
.foregroundColor(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
.onTapGesture {
Task {
await self.doLogin()
}
}
}
.onAppear {
if let cacheToken = self.userContext.loadCacheToken() {
self.token = cacheToken
}
}
}
//
private func doLogin() async {
do {
try await self.userContext.loginWithToken(token: self.token)
print(self.userContext.networkSession?.accessToken)
// KeychainStore
// let store = KeychainStore.shared
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
// try store.save(tokenData, account: self.username)
// }
} catch let err as SDLAPIError {
await MainActor.run {
self.showAlert = true
self.errorMessage = err.message
}
} catch {
await MainActor.run {
self.showAlert = true
self.errorMessage = "内部错误,请稍后重试"
}
}
}
}
struct LoginAccountView: View {
@Environment(UserContext.self) var userContext: UserContext
@Environment(AppContext.self) var appContext: AppContext
@Binding var authScreen: AuthScreen
@State private var username: String = ""
@State private var password: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
struct LoginResult: Decodable {
var accessToken: String
var networkId: Int
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case networkId = "network_id"
}
}
var body: some View {
VStack(alignment: .center) {
VStack(alignment: .leading) {
TextField("手机号/邮箱", text: $username)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
SecureField("密码", text: $password)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
Button {
self.authScreen = .resetPassword
} label: {
Text("忘记密码?")
.underline(true, color: .blue)
.font(.system(size: 14, weight: .regular))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
}
Button {
if self.username.isEmpty {
self.showAlert = true
self.errorMessage = "账号不能为空"
} else if self.password.isEmpty {
self.showAlert = true
self.errorMessage = "密码不能为空"
} else {
Task {
await self.doLogin()
}
}
} label: {
Text("登陆")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
.frame(width: 120, height: 35)
}
.frame(width: 120, height: 35)
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
}
.alert(isPresented: $showAlert) {
Alert(title: Text("错误提示"), message: Text("账号密码为空"))
}
.onAppear {
if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() {
self.username = cacheUsername
self.password = cachePassword
}
}
}
//
private func doLogin() async {
do {
try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password)
// KeychainStore
// let store = KeychainStore.shared
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
// try store.save(tokenData, account: self.username)
// }
} catch let err as SDLAPIError {
await MainActor.run {
self.showAlert = true
self.errorMessage = err.message
}
} catch {
await MainActor.run {
self.showAlert = true
self.errorMessage = "内部错误,请稍后重试"
}
}
}
}
}
//
struct ResetPasswordView: View {
@Environment(UserContext.self) var userContext: UserContext
@State private var username: String = ""
@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)
SecureField("密码", text: $password)
.multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle())
.frame(width: 200, height: 25)
.background(Color.clear)
.foregroundColor(Color.black)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
.foregroundColor(.blue)
.padding(.top, 25)
, alignment: .topLeading)
Button {
self.authScreen = .resetPassword
} label: {
Text("忘记密码?")
.underline(true, color: .blue)
.font(.system(size: 14, weight: .regular))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
}
Button {
Task { @MainActor in
await self.sendVerifyCode()
if self.username.isEmpty {
self.showAlert = true
self.errorMessage = "账号不能为空"
} else if self.password.isEmpty {
self.showAlert = true
self.errorMessage = "密码不能为空"
} else {
Task {
await self.doLogin()
}
}
} label: {
Text("获取验证码")
.font(.system(size: 14))
Text("登陆")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.black)
.frame(width: 160, height: 36)
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
.frame(width: 120, height: 35)
}
.frame(width: 160, height: 36)
punchnet/Core/SDLUtil.swift.cornerRadius(6)
Spacer()
.frame(width: 120, height: 35)
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0)
}
.padding(.top, 40)
.frame(width: 400, height: 400)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
Alert(title: Text("错误提示"), message: Text("账号密码为空"))
}
}
private func sendVerifyCode() async {
if username.isEmpty {
self.showAlert = true
self.errorMessage = "手机号/邮箱为空"
return
}
switch SDLUtil.identifyContact(username) {
case .email, .phone:
do {
let result = try await self.userContext.sendVerifyCode(username: username)
print("send verify code result: \(result)")
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
.onAppear {
if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() {
self.username = cacheUsername
self.password = cachePassword
}
case .invalid:
self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误"
}
}
//
private func doLogin() async {
do {
try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password)
// KeychainStore
// let store = KeychainStore.shared
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
// try store.save(tokenData, account: self.username)
// }
} catch let err as SDLAPIError {
await MainActor.run {
self.showAlert = true
self.errorMessage = err.message
}
} catch {
await MainActor.run {
self.showAlert = true
self.errorMessage = "内部错误,请稍后重试"
}
}
}
}
#Preview {
// ResetPasswordView()
// .environment(UserContext())
// ResetPasswordView()
// .environment(UserContext())
}

View File

@ -0,0 +1,304 @@
//
// ResetPasswordView.swift
// punchnet
//
// Created by on 2026/3/9.
//
import SwiftUI
enum ResetPasswordStage {
case getVerifyCode
case submitVerifyCode(username: String)
case resetPassword(username: String)
}
struct ResetPasswordRootView: View {
@State private var stage: ResetPasswordStage = .getVerifyCode
var body: some View {
switch stage {
case .getVerifyCode:
GetVerifyCodeView(stage: $stage)
case .submitVerifyCode(let username):
SubmitVerifyCodeView(username: username, stage: $stage)
case .resetPassword(let username):
ResetPasswordView(stage: $stage, username: username)
}
}
}
//
struct GetVerifyCodeView: View {
@Environment(UserContext.self) var userContext: UserContext
@Binding var stage: ResetPasswordStage
@State private var username: String = ""
@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
)
}
Button {
Task { @MainActor in
//await self.sendVerifyCode()
withAnimation {
self.stage = .submitVerifyCode(username: self.username)
}
}
} 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)
Spacer()
}
.padding(.top, 40)
.frame(width: 400, height: 400)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
private func sendVerifyCode() async {
if username.isEmpty {
self.showAlert = true
self.errorMessage = "手机号/邮箱为空"
return
}
switch SDLUtil.identifyContact(username) {
case .email, .phone:
do {
let result = try await self.userContext.sendVerifyCode(username: username)
print("send verify code result: \(result)")
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
case .invalid:
self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误"
}
}
}
//
struct SubmitVerifyCodeView: View {
@Environment(UserContext.self) var userContext: UserContext
@State var username: String
@Binding var stage: ResetPasswordStage
@State private var verifiyCode: String = ""
@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)
.disabled(true)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
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 {
Task { @MainActor in
await self.submitVerifyCode()
withAnimation {
self.stage = .resetPassword(username: username)
}
}
} 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)
Spacer()
}
.padding(.top, 40)
.frame(width: 400, height: 400)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
private func submitVerifyCode() async {
if verifiyCode.isEmpty {
self.showAlert = true
self.errorMessage = "请输入验证码"
return
}
if verifiyCode.count != 4 {
self.showAlert = true
self.errorMessage = "验证码错误"
return
}
do {
let result = try await self.userContext.submitVerifyCode(username: username, verifyCode: verifiyCode)
print("submit verify code result: \(result)")
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
}
}
//
struct ResetPasswordView: View {
@Environment(UserContext.self) var userContext: UserContext
@Binding var stage: ResetPasswordStage
var username: String
@State private var password: String = ""
@State private var confirmPassword: String = ""
@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) {
SecureField("新密码", text: $password)
.textFieldStyle(.plain)
.frame(width: 260, height: 28)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
SecureField("再次输入密码", text: $confirmPassword)
.textFieldStyle(.plain)
.frame(width: 260, height: 28)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
}
Button {
Task { @MainActor in
await self.resetPassword()
self.stage = .resetPassword(username: username)
}
} 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)
Spacer()
}
.padding(.top, 40)
.frame(width: 400, height: 400)
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
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.userContext.resetPassword(username: username, password: self.password)
print("send verify code result: \(result)")
} catch {
self.showAlert = true
self.errorMessage = error.localizedDescription
}
}
}

View File

@ -92,7 +92,6 @@ class UserContext {
}
}
@MainActor
func sendVerifyCode(username: String) async throws -> String {
var params: [String: Any] = [
"username": username
@ -102,6 +101,26 @@ class UserContext {
return try await SDLAPIClient.doPost(path: "/auth/sendVerifyCode", params: params, as: String.self)
}
func submitVerifyCode(username: String, verifyCode: String) async throws -> String {
var params: [String: Any] = [
"username": username,
"verify_code": verifyCode,
]
params.merge(baseParams) {$1}
return try await SDLAPIClient.doPost(path: "/auth/submitVerifyCode", params: params, as: String.self)
}
func resetPassword(username: String, password: String) async throws -> String {
var params: [String: Any] = [
"username": username,
"password": password,
]
params.merge(baseParams) {$1}
return try await SDLAPIClient.doPost(path: "/auth/resetPassword", params: params, as: String.self)
}
func loadCacheToken() -> String? {
if let data = try? KeychainStore.shared.load(account: "token") {
return String(data: data, encoding: .utf8)