完善注册流程

This commit is contained in:
anlicheng 2026-03-20 21:49:59 +08:00
parent 0f98a356b7
commit 9727fd1096
8 changed files with 325 additions and 39 deletions

View File

@ -34,12 +34,19 @@ struct SDLAPIClient {
let (data, _) = try await URLSession.shared.data(for: request) let (data, _) = try await URLSession.shared.data(for: request)
if let response = String(bytes: data, encoding: .utf8) { if let response = String(bytes: data, encoding: .utf8) {
NSLog("response is: \(response)") NSLog("url: \(path), response is: \(response)")
} }
let apiResponse = try JSONDecoder().decode(SDLAPIResponse<T>.self, from: data) let apiResponse = try JSONDecoder().decode(SDLAPIResponse<T>.self, from: data)
if apiResponse.code == 0, let data = apiResponse.data { // code = 0
return data if apiResponse.code == 0 {
if let data = apiResponse.data {
return data
} else if let data = apiResponse.message as? T {
return data
} else {
throw SDLAPIError(code: 0, message: "数据格式错误")
}
} else if let message = apiResponse.message { } else if let message = apiResponse.message {
throw SDLAPIError(code: apiResponse.code, message: message) throw SDLAPIError(code: apiResponse.code, message: message)
} else { } else {

View File

@ -0,0 +1,98 @@
//
// PrivacyDetailView.swift
// punchnet
//
// Created by on 2026/3/20.
//
import SwiftUI
// MARK: - 2.
struct PrivacyDetailView: View {
@Environment(\.dismiss) var dismiss
@AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false
@Binding var showPrivacy: Bool
//
@State private var loadingProgress: Double = 0.0
@State private var isPageLoading: Bool = true
let privacyURL = URL(string: "https://www.baidu.com")! //
var body: some View {
VStack(spacing: 0) {
// MARK:
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("隐私政策与服务条款")
.font(.headline)
Text("由 PunchNet 加密传输")
.font(.caption2)
.foregroundColor(.secondary)
}
Spacer()
if isPageLoading {
ProgressView()
.controlSize(.small)
}
Button(action: { dismiss() }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
.font(.title3)
}
.buttonStyle(.plain)
}
.padding()
.background(.ultraThinMaterial)
// MARK: 线
if isPageLoading {
ProgressView(value: loadingProgress, total: 1.0)
.progressViewStyle(.linear)
.tint(.blue)
.frame(height: 2)
.transition(.opacity)
} else {
Divider().frame(height: 2)
}
// MARK: WebView
PunchNetWebView(url: privacyURL, progress: $loadingProgress, isLoading: $isPageLoading)
.background(Color(NSColor.windowBackgroundColor))
// MARK:
VStack(spacing: 12) {
Divider()
HStack {
Text("继续使用即表示您同意我们的全部条款。")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Button("拒绝") {
self.showPrivacy = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NSApplication.shared.terminate(nil)
}
}
.buttonStyle(.bordered)
Button("同意并继续") {
hasAcceptedPrivacy = true
dismiss()
}
.buttonStyle(.borderedProminent)
.tint(.blue)
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
}
.background(.ultraThinMaterial)
}
.frame(minWidth: 600, minHeight: 700)
}
}

View File

@ -0,0 +1,63 @@
//
// PunchNetWebView.swift
// punchnet
//
// Created by on 2026/3/20.
//
import SwiftUI
import WebKit
// MARK: - 1. WebView ()
struct PunchNetWebView: NSViewRepresentable {
let url: URL
@Binding var progress: Double
@Binding var isLoading: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
// (KVO)
context.coordinator.observation = webView.observe(\.estimatedProgress, options: [.new]) { wv, _ in
DispatchQueue.main.async {
self.progress = wv.estimatedProgress
}
}
let request = URLRequest(url: url)
webView.load(request)
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {
}
//
class Coordinator: NSObject, WKNavigationDelegate {
var parent: PunchNetWebView
var observation: NSKeyValueObservation?
init(_ parent: PunchNetWebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
DispatchQueue.main.async { self.parent.isLoading = true }
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.5)) {
self.parent.isLoading = false
}
}
}
}
}

View File

@ -16,6 +16,7 @@ class RegisterModel {
case requestVerifyCode(username: String?) case requestVerifyCode(username: String?)
case submitVerifyCode(username: String, sessionId: Int) case submitVerifyCode(username: String, sessionId: Int)
case setPassword(sessionId: Int) case setPassword(sessionId: Int)
case success
} }
// //
@ -44,7 +45,7 @@ class RegisterModel {
return try await SDLAPIClient.doPost(path: "/register/sendVerfiyCode", params: params, as: RegisterSession.self) return try await SDLAPIClient.doPost(path: "/register/sendVerfiyCode", params: params, as: RegisterSession.self)
} }
func submitVerifyCode(sessionId: Int, verifyCode: Int) async throws -> String { func submitVerifyCode(sessionId: Int, verifyCode: String) async throws -> String {
var params: [String: Any] = [ var params: [String: Any] = [
"session_id": sessionId, "session_id": sessionId,
"code": verifyCode, "code": verifyCode,

View File

@ -25,6 +25,8 @@ struct RegisterRootView: View {
RegisterSubmitVerifyCodeView(username: username, sessionId: sessionId) RegisterSubmitVerifyCodeView(username: username, sessionId: sessionId)
case .setPassword(let sessionId): case .setPassword(let sessionId):
RegisterSetPasswordView(sessionId: sessionId) RegisterSetPasswordView(sessionId: sessionId)
case .success:
RegisterSuccessView()
} }
} }
.transition(.asymmetric( .transition(.asymmetric(
@ -126,9 +128,12 @@ struct RegisterRequestVerifyCodeView: View {
self.registerModel.stage = .submitVerifyCode(username: username, sessionId: registerSession.sessionId) self.registerModel.stage = .submitVerifyCode(username: username, sessionId: registerSession.sessionId)
self.registerModel.transitionEdge = .trailing self.registerModel.transitionEdge = .trailing
} }
} catch { } catch let err as SDLAPIError {
self.showAlert = true self.showAlert = true
self.errorMessage = error.localizedDescription self.errorMessage = err.message
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
} }
default: default:
self.showAlert = true self.showAlert = true
@ -161,7 +166,7 @@ struct RegisterSubmitVerifyCodeView: View {
// //
var validInputCode: Bool { var validInputCode: Bool {
return !self.code.isEmpty && self.code.count == 4 && self.code.allSatisfy {$0.isNumber} return !self.code.isEmpty && self.code.count == 6 && self.code.allSatisfy {$0.isNumber}
} }
var body: some View { var body: some View {
@ -169,7 +174,7 @@ struct RegisterSubmitVerifyCodeView: View {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)") headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
VStack(alignment: .trailing, spacing: 16) { VStack(alignment: .trailing, spacing: 16) {
PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code) PunchTextField(icon: "envelope.badge", placeholder: "输入 6 位验证码", text: $code)
Button { Button {
self.resendVerifyCodeAction() self.resendVerifyCodeAction()
@ -250,15 +255,19 @@ struct RegisterSubmitVerifyCodeView: View {
self.isProcessing = true self.isProcessing = true
Task { @MainActor in Task { @MainActor in
do { do {
_ = try await self.registerModel.submitVerifyCode(sessionId: sessionId, verifyCode: Int(self.code) ?? 0) _ = try await self.registerModel.submitVerifyCode(sessionId: sessionId, verifyCode: self.code)
withAnimation(.spring(duration: 0.6, bounce: 0.2)) { withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.registerModel.stage = .setPassword(sessionId: sessionId) self.registerModel.stage = .setPassword(sessionId: sessionId)
self.registerModel.transitionEdge = .trailing self.registerModel.transitionEdge = .trailing
} }
} catch { } catch let err as SDLAPIError {
self.showAlert = true self.showAlert = true
self.errorMessage = error.localizedDescription self.errorMessage = err.message
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
} }
self.isProcessing = false self.isProcessing = false
} }
} }
@ -337,8 +346,13 @@ struct RegisterSetPasswordView: View {
Task { @MainActor in Task { @MainActor in
do { do {
_ = try await self.registerModel.register(sessionId: sessionId, password: self.password) _ = try await self.registerModel.register(sessionId: sessionId, password: self.password)
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.registerModel.stage = .success
self.registerModel.transitionEdge = .trailing
}
} catch let err as SDLAPIError {
self.showAlert = true self.showAlert = true
self.errorMessage = "注册成功" self.errorMessage = err.message
} catch { } catch {
self.showAlert = true self.showAlert = true
self.errorMessage = "注册失败,重稍后重试" self.errorMessage = "注册失败,重稍后重试"
@ -349,6 +363,88 @@ struct RegisterSetPasswordView: View {
} }
// MARK:
struct RegisterSuccessView: View {
@Environment(\.dismiss) private var dismiss //
// MARK: -
@State private var animateIcon: Bool = false //
@State private var animateText: Bool = false //
var body: some View {
VStack(spacing: 32) {
Spacer()
// MARK: - ()
ZStack {
//
Circle()
.fill(Color.green.opacity(0.1))
.frame(width: 100, height: 100)
//
.scaleEffect(animateIcon ? 1.1 : 0.95)
.opacity(animateIcon ? 0.8 : 1.0)
// Checkmark
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 56))
.foregroundStyle(.green.gradient)
// Checkmark
.scaleEffect(animateIcon ? 1.05 : 1.0)
}
//
.transition(.move(edge: .bottom).combined(with: .opacity))
// MARK: -
VStack(spacing: 32) {
VStack(spacing: 12) {
Text("注册成功")
.font(.title.bold())
Text("您的 PunchNet 账号已就绪。\n现在可以登录并开始构建您的私有网络了。")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
Button(action: {
//
dismiss()
}) {
Text("立即开始使用")
.fontWeight(.bold)
.frame(width: 200)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.tint(.green)
}
//
.opacity(animateText ? 1.0 : 0.0)
.offset(y: animateText ? 0 : 20)
}
.padding(40)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(NSColor.windowBackgroundColor)) //
.onAppear {
self.startAnimations()
}
}
// MARK: -
private func startAnimations() {
// 1.
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
animateIcon = true
}
// 2. 0.4 使
withAnimation(.spring(duration: 0.6, bounce: 0.3).delay(0.4)) {
animateText = true
}
}
}
// MARK: - // MARK: -
extension View { extension View {

View File

@ -44,7 +44,7 @@ class ResetPasswordModel {
return try await SDLAPIClient.doPost(path: "/password/sendVerfiyCode", params: params, as: ResetPasswordSession.self) return try await SDLAPIClient.doPost(path: "/password/sendVerfiyCode", params: params, as: ResetPasswordSession.self)
} }
func submitVerifyCode(sessionId: Int, verifyCode: Int) async throws -> String { func submitVerifyCode(sessionId: Int, verifyCode: String) async throws -> String {
var params: [String: Any] = [ var params: [String: Any] = [
"session_id": sessionId, "session_id": sessionId,
"code": verifyCode, "code": verifyCode,

View File

@ -109,7 +109,7 @@ struct SubmitVerifyCodeView: View {
@State private var errorMessage = "" @State private var errorMessage = ""
var validInputCode: Bool { var validInputCode: Bool {
return !self.code.isEmpty && self.code.count == 4 && self.code.allSatisfy {$0.isNumber} return !self.code.isEmpty && self.code.count == 6 && self.code.allSatisfy {$0.isNumber}
} }
var body: some View { var body: some View {
@ -117,7 +117,7 @@ struct SubmitVerifyCodeView: View {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)") headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
VStack(alignment: .trailing, spacing: 16) { VStack(alignment: .trailing, spacing: 16) {
PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code) PunchTextField(icon: "envelope.badge", placeholder: "输入 6 位验证码", text: $code)
Button(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") { Button(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") {
self.resendAction() self.resendAction()
@ -183,13 +183,14 @@ struct SubmitVerifyCodeView: View {
self.isProcessing = true self.isProcessing = true
Task { @MainActor in Task { @MainActor in
do { do {
let result = try await resetPasswordModel.submitVerifyCode(sessionId: sessionId, verifyCode: Int(code) ?? 0) 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)) { withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.resetPasswordModel.stage = .resetPassword(username: username, sessionId: sessionId) self.resetPasswordModel.stage = .resetPassword(username: username, sessionId: sessionId)
self.resetPasswordModel.transitionEdge = .trailing self.resetPasswordModel.transitionEdge = .trailing
} }
} catch { } catch let err as SDLAPIError {
self.errorMessage = error.localizedDescription self.errorMessage = err.message
self.showAlert = true self.showAlert = true
} }
self.isProcessing = false self.isProcessing = false

View File

@ -33,6 +33,9 @@ struct punchnetApp: App {
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()
@AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false
// UI
@State private var showPrivacy: Bool = false
init() { init() {
self.noticeServer = UDPNoticeCenterServer() self.noticeServer = UDPNoticeCenterServer()
@ -42,10 +45,19 @@ struct punchnetApp: App {
var body: some Scene { var body: some Scene {
WindowGroup(id: "main") { WindowGroup(id: "main") {
RootView() //RootView()
//RegisterRootView()
RegisterSuccessView()
.navigationTitle("") .navigationTitle("")
.environment(self.appContext) .environment(self.appContext)
.environment(self.userContext) .environment(self.userContext)
.onAppear {
self.showPrivacy = !hasAcceptedPrivacy
}
.sheet(isPresented: $showPrivacy) {
PrivacyDetailView(showPrivacy: $showPrivacy)
//.interactiveDismissDisabled() //
}
} }
// .commands { // .commands {
// CommandGroup(replacing: .appInfo) { // CommandGroup(replacing: .appInfo) {
@ -86,24 +98,24 @@ struct punchnetApp: App {
} }
.windowResizability(.contentSize) .windowResizability(.contentSize)
.defaultPosition(.center) .defaultPosition(.center)
//
MenuBarExtra("punchnet", image: "logo_32") { // MenuBarExtra("punchnet", image: "logo_32") {
VStack { // VStack {
Button(action: { // Button(action: {
self.menuClick() // self.menuClick()
}, label: { // }, label: {
Text("启动") // Text("")
}) // })
//
Divider() // Divider()
//
Button(action: { // Button(action: {
NSApplication.shared.terminate(nil) // NSApplication.shared.terminate(nil)
}, label: { Text("退出") }) // }, label: { Text("退") })
//
} // }
} // }
.menuBarExtraStyle(.menu) // .menuBarExtraStyle(.menu)
} }
private func menuClick() { private func menuClick() {
@ -120,9 +132,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
print("call me applicationShouldTerminate")
Task { Task {
try await VPNManager.shared.disableVpn() do {
DispatchQueue.main.async { try await VPNManager.shared.disableVpn()
} catch let err {
print("退出时关闭 VPN 失败: \(err)")
}
print("call me here termi")
await MainActor.run {
sender.reply(toApplicationShouldTerminate: true) sender.reply(toApplicationShouldTerminate: true)
} }
} }