Compare commits

...

2 Commits

Author SHA1 Message Date
fd87c244b9 fix register flow 2026-03-19 20:13:50 +08:00
4c9bb58a88 完善注册逻辑 2026-03-19 20:08:03 +08:00
4 changed files with 299 additions and 266 deletions

View File

@ -34,6 +34,16 @@ struct SDLUtil {
} }
return digest.map { String(format: "%02x", $0) }.joined() return digest.map { String(format: "%02x", $0) }.joined()
}
static func isValidIdentifyContact(_ input: String) -> Bool {
switch identifyContact(input) {
case .email, .phone:
true
default:
false
}
} }
static func identifyContact(_ input: String) -> ContactType { static func identifyContact(_ input: String) -> ContactType {
@ -41,7 +51,7 @@ struct SDLUtil {
// 1 11 // 1 11
let phoneRegex = /^1[3-9][0-9]{9}$/ let phoneRegex = /^1[3-9][0-9]{9}$/
// //
let emailRegex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/ let emailRegex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}$/
if trimmed.wholeMatch(of: phoneRegex) != nil { if trimmed.wholeMatch(of: phoneRegex) != nil {
return .phone return .phone
} else if trimmed.wholeMatch(of: emailRegex) != nil { } else if trimmed.wholeMatch(of: emailRegex) != nil {

View File

@ -47,10 +47,12 @@ class RegisterModel {
var params: [String: Any] = [ var params: [String: Any] = [
"username": username, "username": username,
"password": password, "password": password,
"version": SystemConfig.version_name,
"system": SystemConfig.systemInfo
] ]
params.merge(baseParams) {$1} params.merge(baseParams) {$1}
return try await SDLAPIClient.doPost(path: "/auth/resetPassword", params: params, as: String.self) return try await SDLAPIClient.doPost(path: "/auth/register", params: params, as: String.self)
} }
} }

View File

@ -5,82 +5,120 @@
// Created by on 2026/3/9. // Created by on 2026/3/9.
// //
import SwiftUI import SwiftUI
import Observation
// MARK: -
struct RegisterRootView: View { struct RegisterRootView: View {
@State private var registerModel: RegisterModel = RegisterModel() @State private var registerModel = RegisterModel()
var body: some View { var body: some View {
ZStack {
//
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
.ignoresSafeArea()
Group { Group {
switch registerModel.stage { switch registerModel.stage {
case .requestVerifyCode: case .requestVerifyCode:
RegisterRequestVerifyCodeView() RegisterRequestVerifyCodeView()
case .submitVerifyCode(let username): case .submitVerifyCode(let username):
RegisterSubmitVerifyCodeView(username: username) RegisterSubmitVerifyCodeView(username: username)
case .setPassword(username: let username): case .setPassword(let username):
RegisterSetPasswordView(username: username) RegisterSetPasswordView(username: username)
} }
} }
.transition(.asymmetric(insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)))
}
.environment(registerModel) .environment(registerModel)
.frame(width: 400, height: 450)
} }
} }
// // MARK: -
struct RegisterRequestVerifyCodeView: View { struct PunchTextField: View {
@Environment(RegisterModel.self) var registerModel: RegisterModel let icon: String
let placeholder: String
@State private var username: String = "" @Binding var text: String
@State private var showAlert = false var isSecure: Bool = false
@State private var errorMessage = "" var isDisabled: Bool = false
var body: some View { var body: some View {
VStack(spacing: 30) { HStack(spacing: 12) {
Image(systemName: icon)
.foregroundColor(.secondary)
.frame(width: 20)
// if isSecure {
Text("注册 创建个人网络") SecureField(placeholder, text: $text)
.font(.system(size: 18, weight: .medium))
VStack(alignment: .leading, spacing: 16) {
TextField("手机号/邮箱", text: $username)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.frame(width: 260, height: 28) } else {
TextField(placeholder, text: $text)
.textFieldStyle(.plain)
.disabled(isDisabled)
}
}
.padding(10)
.background(Color.primary.opacity(isDisabled ? 0.02 : 0.05))
.cornerRadius(8)
.overlay( .overlay(
Rectangle() RoundedRectangle(cornerRadius: 8)
.frame(height: 1) .stroke(Color.primary.opacity(0.1), lineWidth: 1)
.foregroundColor(.blue),
alignment: .bottom
) )
} }
}
Button { // MARK: -
Task { @MainActor in struct RegisterRequestVerifyCodeView: View {
//await self.sendVerifyCode() @Environment(RegisterModel.self) var registerModel
withAnimation { @State private var username: String = ""
self.registerModel.stage = .submitVerifyCode(username: self.username) @State private var isProcessing = false
//
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""
var body: some View {
VStack(spacing: 24) {
headerSection(title: "创建个人网络", subtitle: "输入手机号或邮箱开始注册")
VStack(spacing: 16) {
PunchTextField(icon: "person.crop.circle", placeholder: "手机号 / 邮箱", text: $username)
} }
} .frame(width: 280)
} label: {
Button(action: {
self.requestVerifyCode()
}) {
if isProcessing {
ProgressView()
.controlSize(.small)
} else {
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) }
.cornerRadius(6) .buttonStyle(.borderedProminent)
.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(self.errorMessage))
} }
} }
private func sendVerifyCode() async { private func requestVerifyCode() {
self.isProcessing = true
Task { @MainActor in
if username.isEmpty { if username.isEmpty {
self.showAlert = true self.showAlert = true
self.errorMessage = "手机号/邮箱为空" self.errorMessage = "手机号/邮箱为空"
self.isProcessing = false
return return
} }
@ -89,198 +127,176 @@ struct RegisterRequestVerifyCodeView: View {
do { do {
let result = try await self.registerModel.requestVerifyCode(username: username) let result = try await self.registerModel.requestVerifyCode(username: username)
print("send verify code result: \(result)") print("send verify code result: \(result)")
withAnimation(.spring()) {
self.registerModel.stage = .submitVerifyCode(username: username)
}
} catch { } catch {
self.showAlert = true self.showAlert = true
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
} }
case .invalid: case .invalid:
self.showAlert = true self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误" self.errorMessage = "手机号/邮箱格式错误"
} }
self.isProcessing = false
} }
} }
// }
// MARK: -
struct RegisterSubmitVerifyCodeView: View { struct RegisterSubmitVerifyCodeView: View {
@Environment(RegisterModel.self) var registerModel: RegisterModel @Environment(RegisterModel.self) var registerModel
let username: String
@State private var code: String = ""
@State private var isProcessing = false
@State var username: String //
@State private var verifiyCode: String = "" @State private var showAlert: Bool = false
@State private var errorMessage: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
var body: some View { var body: some View {
VStack(spacing: 30) { VStack(spacing: 24) {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
// VStack(spacing: 16) {
Text("注册 创建个人网络") PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code)
.font(.system(size: 18, weight: .medium))
VStack(alignment: .leading, spacing: 16) { Button("没有收到?重新获取") {
TextField("手机号/邮箱", text: $username) // Resend Logic
.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: { .buttonStyle(.link)
Text("再次获取") .font(.caption)
.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)
VStack(spacing: 12) {
Button(action: {
self.submitVerifyCode()
}) {
if isProcessing {
ProgressView()
.controlSize(.small)
} else {
Text("验证并设置密码")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
} }
} }
.buttonStyle(.borderedProminent)
.controlSize(.large)
Button { Button("返回上一步") {
Task { @MainActor in
await self.submitVerifyCode()
withAnimation { withAnimation {
self.registerModel.stage = .setPassword(username: self.username) registerModel.stage = .requestVerifyCode
} }
} }
} 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)
.alert(isPresented: $showAlert) { .alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage)) Alert(title: Text("提示"), message: Text(errorMessage))
} }
} }
private func submitVerifyCode() async { private func submitVerifyCode() {
if verifiyCode.isEmpty { self.isProcessing = true
Task { @MainActor in
if self.code.isEmpty {
self.showAlert = true self.showAlert = true
self.errorMessage = "请输入验证码" self.errorMessage = "请输入验证码"
self.isProcessing = false
return return
} }
if verifiyCode.count != 4 { if self.code.count != 4 {
self.showAlert = true self.showAlert = true
self.errorMessage = "验证码错误" self.errorMessage = "验证码错误"
self.isProcessing = false
return return
} }
do { do {
let result = try await self.registerModel.submitVerifyCode(username: username, verifyCode: verifiyCode) let result = try await self.registerModel.submitVerifyCode(username: username, verifyCode: self.code)
print("submit verify code result: \(result)") print("submit verify code result: \(result)")
withAnimation(.spring()) {
registerModel.stage = .setPassword(username: username)
}
} catch { } catch {
self.showAlert = true self.showAlert = true
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
} }
self.isProcessing = false
}
}
} }
} // MARK: -
//
struct RegisterSetPasswordView: View { struct RegisterSetPasswordView: View {
@Environment(RegisterModel.self) var registerModel: RegisterModel @Environment(RegisterModel.self) var registerModel
var username: String let username: String
@State private var password = ""
@State private var confirm = ""
@State private var isProcessing = false
@State private var password: String = "" //
@State private var confirmPassword: String = "" @State private var showAlert: Bool = false
@State private var errorMessage: String = ""
@State private var showAlert = false
@State private var errorMessage = ""
var body: some View { var body: some View {
VStack(spacing: 30) { VStack(spacing: 24) {
headerSection(title: "设置安全密码", subtitle: "最后一步,请确保密码足够强大")
// VStack(spacing: 12) {
Text("注册 创建个人网络") PunchTextField(icon: "lock.shield", placeholder: "新密码", text: $password, isSecure: true)
.font(.system(size: 18, weight: .medium)) 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)
.textFieldStyle(.plain)
.frame(width: 260, height: 28)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(.blue),
alignment: .bottom
)
} }
.frame(width: 280)
Button { Button(action: {
Task { @MainActor in self.handleRegister()
await self.resetPassword() }) {
if isProcessing {
ProgressView()
.controlSize(.small)
} else {
Text("完成注册")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
} }
} 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) .buttonStyle(.borderedProminent)
.cornerRadius(6) .controlSize(.large)
.frame(width: 280)
.disabled(password.isEmpty || password != confirm)
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 resetPassword() async { private func handleRegister() {
self.isProcessing = true
Task { @MainActor in
if password.isEmpty { if password.isEmpty {
self.showAlert = true self.showAlert = true
self.errorMessage = "请输入新密码" self.errorMessage = "请输入新密码"
self.isProcessing = false
return return
} }
if confirmPassword.isEmpty || confirmPassword != password { if confirm.isEmpty || confirm != password {
self.showAlert = true self.showAlert = true
self.errorMessage = "两次输入的密码不一致" self.errorMessage = "两次输入的密码不一致"
self.isProcessing = false
return return
} }
@ -291,6 +307,29 @@ struct RegisterSetPasswordView: View {
self.showAlert = true self.showAlert = true
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
} }
self.isProcessing = false
}
}
}
// MARK: -
extension View {
func headerSection(title: String, subtitle: String) -> some View {
VStack(spacing: 8) {
Image(systemName: "shield.lefthalf.filled")
.font(.system(size: 42))
.foregroundStyle(.blue.gradient)
Text(title)
.font(.title2.bold())
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
} }
} }

View File

@ -47,27 +47,9 @@ struct punchnetApp: App {
var body: some Scene { var body: some Scene {
WindowGroup(id: "mainWindow") { WindowGroup(id: "mainWindow") {
RootView() // RootView()
RegisterRootView()
.frame(width: 800, height: 500) .frame(width: 800, height: 500)
.onAppear {
// //
// guard let screenFrame = NSScreen.main?.frame else {
// return
// }
//
// //
// NSApplication.shared.windows.forEach { window in
// //
// let windowWidth = window.frame.width
// let windowHeight = window.frame.height
// let centerX = (screenFrame.width - windowWidth) / 2
// let centerY = (screenFrame.height - windowHeight) / 2
//
// //
// window.setFrameOrigin(NSPoint(x: centerX, y: centerY))
// }
}
//.toolbar(.hidden)
.navigationTitle("") .navigationTitle("")
.environment(self.appContext) .environment(self.appContext)
.environment(self.userContext) .environment(self.userContext)