From 03a26b2f314c180a894232b037e5937b1dedc0ae Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Mon, 23 Mar 2026 14:58:32 +0800 Subject: [PATCH] fix view --- punchnet/Core/SystemConfig.swift | 5 ++ punchnet/Networking/SDLAPIClient+App.swift | 90 +++++++++++++++++++ .../{Core => Networking}/SDLAPIClient.swift | 0 punchnet/Views/RootView.swift | 44 +++++++-- .../Views/Settings/SettingsAboutView.swift | 43 ++++++++- .../Settings/SettingsUserIssueView.swift | 26 +++--- punchnet/Views/Update/AppUpdateManager.swift | 66 ++++++++++++++ punchnet/Views/Update/AppUpdateView.swift | 77 ++++++++++++++++ punchnet/punchnetApp.swift | 3 +- 9 files changed, 335 insertions(+), 19 deletions(-) create mode 100644 punchnet/Networking/SDLAPIClient+App.swift rename punchnet/{Core => Networking}/SDLAPIClient.swift (100%) create mode 100644 punchnet/Views/Update/AppUpdateManager.swift create mode 100644 punchnet/Views/Update/AppUpdateView.swift diff --git a/punchnet/Core/SystemConfig.swift b/punchnet/Core/SystemConfig.swift index e94732f..1b8d02a 100644 --- a/punchnet/Core/SystemConfig.swift +++ b/punchnet/Core/SystemConfig.swift @@ -13,6 +13,11 @@ struct SystemConfig { static let version_name = "1.1" + static let build: Int = 123 + + // 渠道相关 + static let channel = "appstore" + static let serverHost = "punchnet.s5s8.com" // stun探测辅助服务器ip diff --git a/punchnet/Networking/SDLAPIClient+App.swift b/punchnet/Networking/SDLAPIClient+App.swift new file mode 100644 index 0000000..bee992b --- /dev/null +++ b/punchnet/Networking/SDLAPIClient+App.swift @@ -0,0 +1,90 @@ +// +// SDLAPIClient+App.swift +// punchnet +// +// Created by 安礼成 on 2026/3/21. +// +import Foundation + +extension SDLAPIClient { + + struct AppPoliciesInfo: Codable { + let privacyPolicyUrl: String + let termsOfServiceUrl: String + let privacyPolicyVersion: String + let termsVersion: String + + enum CodingKeys: String, CodingKey { + case privacyPolicyUrl = "privacy_policy_url" + case termsOfServiceUrl = "terms_of_service_url" + case privacyPolicyVersion = "privacy_policy_version" + case termsVersion = "terms_version" + } + } + + // 应用升级信息 + struct AppUpgradeInfo: Codable, Identifiable { + var id = UUID().uuidString + + let hasUpdate: Bool + let latestVersion: String + let latestBuild: Int + // 强制升级的url地址 + let forceUpdateUrl: String? + let releaseNotes: String + let minSupportedVersion: String + let publishTime: Int + + // 是否强制升级 + var forceUpdate: Bool { + return forceUpdateUrl != nil + } + + enum CodingKeys: String, CodingKey { + case hasUpdate = "has_update" + case latestVersion = "latest_version" + case latestBuild = "latest_build" + case forceUpdateUrl = "force_update" + case releaseNotes = "release_notes" + case minSupportedVersion = "min_supported_version" + case publishTime = "publish_time" + } + } + + // 提交用户反馈 + static func appIssue(accessToken: String, contact: String, content: String) async throws -> String { + let params: [String: Any] = [ + "access_token": accessToken, + "contact": contact, + "platform": SystemConfig.systemInfo, + "content": content, + "client_id": SystemConfig.getClientId(), + "mac": SystemConfig.macAddressString(mac: SystemConfig.getMacAddress()) + ] + + return try await SDLAPIClient.doPost(path: "/app/issue", params: params, as: String.self) + } + + // 隐私和服务政策 + static func appPolicies() async throws -> AppPoliciesInfo { + let params: [String: Any] = [ + "platform": "macos", + "client_id": SystemConfig.getClientId() + ] + return try await SDLAPIClient.doPost(path: "/app/policies", params: params, as: AppPoliciesInfo.self) + } + + // 检查app升级 + static func appCheckUpdate() async throws -> AppUpgradeInfo { + let params: [String: Any] = [ + "app_id": "Punchnet", + "platform": "macos", + "version": SystemConfig.systemInfo, + "build": SystemConfig.build, + "channel": SystemConfig.channel, + "client_id": SystemConfig.getClientId() + ] + return try await SDLAPIClient.doPost(path: "/app/checkUpdate", params: params, as: AppUpgradeInfo.self) + } + +} diff --git a/punchnet/Core/SDLAPIClient.swift b/punchnet/Networking/SDLAPIClient.swift similarity index 100% rename from punchnet/Core/SDLAPIClient.swift rename to punchnet/Networking/SDLAPIClient.swift diff --git a/punchnet/Views/RootView.swift b/punchnet/Views/RootView.swift index 882448f..129b435 100644 --- a/punchnet/Views/RootView.swift +++ b/punchnet/Views/RootView.swift @@ -9,16 +9,50 @@ import SwiftUI struct RootView: View { @Environment(UserContext.self) var userContext + @State private var updateManager = AppUpdateManager.shared var body: some View { - Group { - if userContext.isLogined { - NetworkView() - } else { - LoginView() + ZStack { + // 主要界面 + Group { + if userContext.isLogined { + NetworkView() + } else { + LoginView() + } + } + + // 自动更新遮罩 + if updateManager.showUpdateOverlay, let info = updateManager.updateInfo { + // 遮罩背景 + Color.black.opacity(0.4) + .ignoresSafeArea() + .onTapGesture { + if !info.forceUpdate { + updateManager.showUpdateOverlay = false + } + } + + // 弹窗卡片 + AppUpdateView(info: info) { + updateManager.showUpdateOverlay = false + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .black.opacity(0.3), radius: 20) + .transition(.asymmetric( + insertion: .scale(scale: 0.9).combined(with: .opacity), + removal: .opacity + )) } } + .animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay) + .task { + // 启动时静默检查 + let checkUpdateResult = await updateManager.checkUpdate(isManual: false) + NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)") + } } + } #Preview { diff --git a/punchnet/Views/Settings/SettingsAboutView.swift b/punchnet/Views/Settings/SettingsAboutView.swift index 29217c2..f576060 100644 --- a/punchnet/Views/Settings/SettingsAboutView.swift +++ b/punchnet/Views/Settings/SettingsAboutView.swift @@ -10,7 +10,13 @@ struct SettingsAboutView: View { @Environment(\.openURL) private var openURL @State private var isShowingFeedbackSheet = false + @State private var appPoliciesInfo: SDLAPIClient.AppPoliciesInfo? + // 检查更新逻辑 + @State private var updateManager = AppUpdateManager.shared + @State private var showNoUpdateAlert = false + @State private var manualUpdateInfo: SDLAPIClient.AppUpgradeInfo? + var body: some View { ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading, spacing: 32) { @@ -52,10 +58,14 @@ struct SettingsAboutView: View { AboutRow(title: "检查更新", icon: "arrow.clockwise.circle") { Button("立即检查") { // 检查更新逻辑 + Task {@MainActor in + await self.checkAppUpgrade() + } } .buttonStyle(.plain) .foregroundColor(.blue) .font(.subheadline.bold()) + .disabled(updateManager.isChecking) } Divider().padding(.leading, 44) @@ -81,7 +91,9 @@ struct SettingsAboutView: View { .foregroundColor(.secondary) } .onTapGesture { - /* 跳转隐私协议 */ + if let privacyPolicyUrl = self.appPoliciesInfo?.privacyPolicyUrl, let privacyUrl = URL(string: privacyPolicyUrl) { + openURL(privacyUrl) + } } Divider().padding(.leading, 44) @@ -92,7 +104,9 @@ struct SettingsAboutView: View { .foregroundColor(.secondary) } .onTapGesture { - /* 跳转服务条款 */ + if let termsOfServiceUrl = self.appPoliciesInfo?.termsOfServiceUrl, let termsUrl = URL(string: termsOfServiceUrl) { + openURL(termsUrl) + } } } .background(Color.primary.opacity(0.03)) @@ -127,7 +141,32 @@ struct SettingsAboutView: View { } .frame(width: 500, height: 600) // 设置弹窗大小 } + // 手动检查的 Sheet 弹出 + .sheet(item: $manualUpdateInfo) { info in + AppUpdateView(info: info) { + self.manualUpdateInfo = nil + } + } + .alert(isPresented: $showNoUpdateAlert) { + Alert(title: Text("检查更新"), message: Text("您当前使用的是最新版本。")) + } + .task { + self.appPoliciesInfo = try? await SDLAPIClient.appPolicies() + + _ = try? await SDLAPIClient.appCheckUpdate() + } } + + private func checkAppUpgrade() async { + let hasUpdate = await updateManager.checkUpdate(isManual: true) + if hasUpdate { + // 手动检查发现更新,赋值给 sheet 绑定的变量 + self.manualUpdateInfo = updateManager.updateInfo + } else { + self.showNoUpdateAlert = true + } + } + } // MARK: - 复用之前的行组件 diff --git a/punchnet/Views/Settings/SettingsUserIssueView.swift b/punchnet/Views/Settings/SettingsUserIssueView.swift index d8b4ad2..167f44f 100644 --- a/punchnet/Views/Settings/SettingsUserIssueView.swift +++ b/punchnet/Views/Settings/SettingsUserIssueView.swift @@ -18,6 +18,10 @@ struct SettingsUserIssueView: View { @State private var isSubmitting: Bool = false @State private var showSuccessToast: Bool = false + // 错误提示 + @State private var showAlert: Bool = false + @State private var errorMessage: String = "" + var body: some View { ZStack { // 主滚动视图 @@ -113,6 +117,9 @@ struct SettingsUserIssueView: View { successPopup } } + .alert(isPresented: $showAlert) { + Alert(title: Text("提示"), message: Text(errorMessage)) + } } // MARK: - 提交逻辑 @@ -121,7 +128,7 @@ struct SettingsUserIssueView: View { isSubmitting = true } - var params: [String: Any] = [ + let params: [String: Any] = [ "access_token": self.userContext.networkSession?.accessToken ?? "", "contact": self.account, "platform": SystemConfig.systemInfo, @@ -132,15 +139,6 @@ struct SettingsUserIssueView: View { do { _ = try await SDLAPIClient.doPost(path: "/app/issue", params: params, as: String.self) - - } catch let err as SDLAPIError { - - } catch let err { - - } - - // 模拟网络延迟 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) { isSubmitting = false showSuccessToast = true @@ -153,7 +151,15 @@ struct SettingsUserIssueView: View { showSuccessToast = false } } + } catch let err as SDLAPIError { + self.showAlert = true + self.errorMessage = err.message + } catch let err { + self.showAlert = true + self.errorMessage = err.localizedDescription } + + self.isSubmitting = false } // MARK: - 成功弹窗视图 diff --git a/punchnet/Views/Update/AppUpdateManager.swift b/punchnet/Views/Update/AppUpdateManager.swift new file mode 100644 index 0000000..a512d16 --- /dev/null +++ b/punchnet/Views/Update/AppUpdateManager.swift @@ -0,0 +1,66 @@ +// +// AppUpdateManager.swift +// punchnet +// +// Created by 安礼成 on 2026/3/23. +// +import SwiftUI +import Observation + +@Observable +class AppUpdateManager { + static let shared = AppUpdateManager() + + var updateInfo: SDLAPIClient.AppUpgradeInfo? + var isChecking = false + var showUpdateOverlay = false // 用于启动时的全局遮罩 + + @MainActor + func checkUpdate(isManual: Bool = false) async -> Bool { + isChecking = true + defer { + isChecking = false + } + + do { + let updateInfo = try await SDLAPIClient.appCheckUpdate() + // 核心逻辑:比对本地版本 + let currentVersion = SystemConfig.version_name + let needsUpdate = VersionComparator.isVersion(currentVersion, olderThan: updateInfo.latestVersion) + if needsUpdate { + self.updateInfo = updateInfo + // 如果是启动自动检查,则显示遮罩 + if !isManual { + self.showUpdateOverlay = true + } + return true + } + } catch { + print("Update check failed: \(error)") + } + return false + } +} + +struct VersionComparator { + /// 比较版本号:如果 current < latest 返回 true (需要更新) + static func isVersion(_ current: String, olderThan latest: String) -> Bool { + let currentComponents = current.split(separator: ".").map { Int($0) ?? 0 } + let latestComponents = latest.split(separator: ".").map { Int($0) ?? 0 } + + let maxLength = max(currentComponents.count, latestComponents.count) + for i in 0.. latestPart { + return false + } + } + return false + } +} diff --git a/punchnet/Views/Update/AppUpdateView.swift b/punchnet/Views/Update/AppUpdateView.swift new file mode 100644 index 0000000..c72a271 --- /dev/null +++ b/punchnet/Views/Update/AppUpdateView.swift @@ -0,0 +1,77 @@ +// +// AppUpdateView.swift +// punchnet +// +// Created by 安礼成 on 2026/3/23. +// +import SwiftUI + +struct AppUpdateView: View { + let info: SDLAPIClient.AppUpgradeInfo + var dismissAction: () -> Void + + var body: some View { + VStack(spacing: 0) { + // 顶部 Header + VStack(spacing: 12) { + Image(systemName: "arrow.up.rocket.fill") + .font(.system(size: 40)) + .foregroundStyle(.blue.gradient) + + Text("新版本已就绪") + .font(.title3.bold()) + + Text("版本号: \(info.latestVersion)") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.top, 30) + .padding(.bottom, 20) + .frame(maxWidth: .infinity) + .background(Color.blue.opacity(0.05)) + + // 更新日志 + VStack(alignment: .leading, spacing: 12) { + Text("更新说明") + .font(.subheadline.bold()) + + ScrollView { + Text(info.releaseNotes) + .font(.subheadline) + .foregroundColor(.secondary) + .lineSpacing(4) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 120) + + // 操作按钮 + HStack(spacing: 12) { + if !info.forceUpdate { + Button("稍后") { + dismissAction() + } + .buttonStyle(.plain) + .frame(width: 80) + } + + Button { + if let forceUpdateUrl = info.forceUpdateUrl, let url = URL(string: forceUpdateUrl) { + NSWorkspace.shared.open(url) + } + } label: { + Text(info.forceUpdate ? "立即更新" : "下载并安装") + .fontWeight(.bold) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } + .padding(.top, 10) + } + .padding(25) + } + .frame(width: 360) + .background(VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)) + } + +} diff --git a/punchnet/punchnetApp.swift b/punchnet/punchnetApp.swift index 3701b93..2435231 100644 --- a/punchnet/punchnetApp.swift +++ b/punchnet/punchnetApp.swift @@ -45,8 +45,7 @@ struct punchnetApp: App { var body: some Scene { WindowGroup(id: "main") { - //RootView() - SettingsUserIssueView() + RootView() .navigationTitle("") .environment(self.appContext) .environment(self.userContext)