From 9727fd1096a9b7ed1dfb7dbf24b501e11228fa0c Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Fri, 20 Mar 2026 21:49:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B3=A8=E5=86=8C=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- punchnet/Core/SDLAPIClient.swift | 13 +- .../Views/Privacy/PrivacyDetailView.swift | 98 +++++++++++++++ punchnet/Views/Privacy/PunchNetWebView.swift | 63 ++++++++++ punchnet/Views/Register/RegisterModel.swift | 3 +- punchnet/Views/Register/RegisterView.swift | 112 ++++++++++++++++-- .../ResetPassword/ResetPasswordModel.swift | 2 +- .../ResetPassword/ResetPasswordView.swift | 11 +- punchnet/punchnetApp.swift | 62 ++++++---- 8 files changed, 325 insertions(+), 39 deletions(-) create mode 100644 punchnet/Views/Privacy/PrivacyDetailView.swift create mode 100644 punchnet/Views/Privacy/PunchNetWebView.swift diff --git a/punchnet/Core/SDLAPIClient.swift b/punchnet/Core/SDLAPIClient.swift index 42c0ada..d11224c 100644 --- a/punchnet/Core/SDLAPIClient.swift +++ b/punchnet/Core/SDLAPIClient.swift @@ -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.self, from: data) - if apiResponse.code == 0, let data = apiResponse.data { - return 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 { diff --git a/punchnet/Views/Privacy/PrivacyDetailView.swift b/punchnet/Views/Privacy/PrivacyDetailView.swift new file mode 100644 index 0000000..9a42a64 --- /dev/null +++ b/punchnet/Views/Privacy/PrivacyDetailView.swift @@ -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) + } + +} diff --git a/punchnet/Views/Privacy/PunchNetWebView.swift b/punchnet/Views/Privacy/PunchNetWebView.swift new file mode 100644 index 0000000..cb9866f --- /dev/null +++ b/punchnet/Views/Privacy/PunchNetWebView.swift @@ -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 + } + } + } + } +} diff --git a/punchnet/Views/Register/RegisterModel.swift b/punchnet/Views/Register/RegisterModel.swift index 16c459e..e68786a 100644 --- a/punchnet/Views/Register/RegisterModel.swift +++ b/punchnet/Views/Register/RegisterModel.swift @@ -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, diff --git a/punchnet/Views/Register/RegisterView.swift b/punchnet/Views/Register/RegisterView.swift index 67c2d10..40c2f69 100644 --- a/punchnet/Views/Register/RegisterView.swift +++ b/punchnet/Views/Register/RegisterView.swift @@ -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 { diff --git a/punchnet/Views/ResetPassword/ResetPasswordModel.swift b/punchnet/Views/ResetPassword/ResetPasswordModel.swift index 94e3e4c..46d7104 100644 --- a/punchnet/Views/ResetPassword/ResetPasswordModel.swift +++ b/punchnet/Views/ResetPassword/ResetPasswordModel.swift @@ -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, diff --git a/punchnet/Views/ResetPassword/ResetPasswordView.swift b/punchnet/Views/ResetPassword/ResetPasswordView.swift index 7def67f..b9cec14 100644 --- a/punchnet/Views/ResetPassword/ResetPasswordView.swift +++ b/punchnet/Views/ResetPassword/ResetPasswordView.swift @@ -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 diff --git a/punchnet/punchnetApp.swift b/punchnet/punchnetApp.swift index 5972112..da4d748 100644 --- a/punchnet/punchnetApp.swift +++ b/punchnet/punchnetApp.swift @@ -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 { - try await VPNManager.shared.disableVpn() - DispatchQueue.main.async { + do { + try await VPNManager.shared.disableVpn() + } catch let err { + print("退出时关闭 VPN 失败: \(err)") + } + + print("call me here termi") + + await MainActor.run { sender.reply(toApplicationShouldTerminate: true) } }