完善注册流程

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)
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)
if apiResponse.code == 0, let data = apiResponse.data {
// code = 0
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 {
throw SDLAPIError(code: apiResponse.code, message: message)
} 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 submitVerifyCode(username: String, 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)
}
func submitVerifyCode(sessionId: Int, verifyCode: Int) async throws -> String {
func submitVerifyCode(sessionId: Int, verifyCode: String) async throws -> String {
var params: [String: Any] = [
"session_id": sessionId,
"code": verifyCode,

View File

@ -25,6 +25,8 @@ struct RegisterRootView: View {
RegisterSubmitVerifyCodeView(username: username, sessionId: sessionId)
case .setPassword(let sessionId):
RegisterSetPasswordView(sessionId: sessionId)
case .success:
RegisterSuccessView()
}
}
.transition(.asymmetric(
@ -126,9 +128,12 @@ struct RegisterRequestVerifyCodeView: View {
self.registerModel.stage = .submitVerifyCode(username: username, sessionId: registerSession.sessionId)
self.registerModel.transitionEdge = .trailing
}
} catch {
} catch let err as SDLAPIError {
self.showAlert = true
self.errorMessage = error.localizedDescription
self.errorMessage = err.message
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
}
default:
self.showAlert = true
@ -161,7 +166,7 @@ struct RegisterSubmitVerifyCodeView: View {
//
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 {
@ -169,7 +174,7 @@ struct RegisterSubmitVerifyCodeView: View {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
VStack(alignment: .trailing, spacing: 16) {
PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code)
PunchTextField(icon: "envelope.badge", placeholder: "输入 6 位验证码", text: $code)
Button {
self.resendVerifyCodeAction()
@ -250,15 +255,19 @@ struct RegisterSubmitVerifyCodeView: View {
self.isProcessing = true
Task { @MainActor in
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)) {
self.registerModel.stage = .setPassword(sessionId: sessionId)
self.registerModel.transitionEdge = .trailing
}
} catch {
} catch let err as SDLAPIError {
self.showAlert = true
self.errorMessage = error.localizedDescription
self.errorMessage = err.message
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
}
self.isProcessing = false
}
}
@ -337,8 +346,13 @@ struct RegisterSetPasswordView: View {
Task { @MainActor in
do {
_ = 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.errorMessage = "注册成功"
self.errorMessage = err.message
} catch {
self.showAlert = true
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: -
extension View {

View File

@ -44,7 +44,7 @@ class ResetPasswordModel {
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] = [
"session_id": sessionId,
"code": verifyCode,

View File

@ -109,7 +109,7 @@ struct SubmitVerifyCodeView: View {
@State private var errorMessage = ""
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 {
@ -117,7 +117,7 @@ struct SubmitVerifyCodeView: View {
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
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)") {
self.resendAction()
@ -183,13 +183,14 @@ struct SubmitVerifyCodeView: View {
self.isProcessing = true
Task { @MainActor in
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)) {
self.resetPasswordModel.stage = .resetPassword(username: username, sessionId: sessionId)
self.resetPasswordModel.transitionEdge = .trailing
}
} catch {
self.errorMessage = error.localizedDescription
} catch let err as SDLAPIError {
self.errorMessage = err.message
self.showAlert = true
}
self.isProcessing = false

View File

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