完善注册流程
This commit is contained in:
parent
0f98a356b7
commit
9727fd1096
@ -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 {
|
||||
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 {
|
||||
|
||||
98
punchnet/Views/Privacy/PrivacyDetailView.swift
Normal file
98
punchnet/Views/Privacy/PrivacyDetailView.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
63
punchnet/Views/Privacy/PunchNetWebView.swift
Normal file
63
punchnet/Views/Privacy/PunchNetWebView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user