From de96284ac2dc040ed3843dc15af28234fe0ae68a Mon Sep 17 00:00:00 2001 From: ZhangLei Date: Tue, 11 Nov 2025 16:47:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Option+=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E5=8A=9F=E8=83=BD=E8=AE=BE=E7=BD=AE=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=8D=E5=88=B6=E4=BB=B7=E6=A0=BC=E5=92=8C=E6=89=93?= =?UTF-8?q?=E5=BC=80=E5=B8=81=E5=AE=89=E4=BA=A4=E6=98=93=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bitcoin Monitoring.xcodeproj/project.pbxproj | 8 +- Bitcoin-Monitoring/Core/MenuBarManager.swift | 163 +++++++++++------- Bitcoin-Monitoring/Managers/AppSettings.swift | 64 ++++++- .../Utils/BinanceURLGenerator.swift | 72 ++++++++ .../Views/AboutWindowView.swift | 6 +- .../Views/PreferencesWindowView.swift | 42 ++++- README.md | 33 +--- 7 files changed, 294 insertions(+), 94 deletions(-) create mode 100644 Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift diff --git a/Bitcoin Monitoring.xcodeproj/project.pbxproj b/Bitcoin Monitoring.xcodeproj/project.pbxproj index b524968..d6fc03d 100644 --- a/Bitcoin Monitoring.xcodeproj/project.pbxproj +++ b/Bitcoin Monitoring.xcodeproj/project.pbxproj @@ -78,7 +78,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 2600; + LastUpgradeCheck = 2610; TargetAttributes = { 4E94106F2EB09F90003658CB = { CreatedOnToolsVersion = 16.2; @@ -131,6 +131,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -196,6 +197,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -273,7 +275,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -306,7 +308,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.1; PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Bitcoin-Monitoring/Core/MenuBarManager.swift b/Bitcoin-Monitoring/Core/MenuBarManager.swift index c4e91da..a623e4b 100644 --- a/Bitcoin-Monitoring/Core/MenuBarManager.swift +++ b/Bitcoin-Monitoring/Core/MenuBarManager.swift @@ -450,91 +450,130 @@ class MenuBarManager: NSObject, ObservableObject { } - // 选择币种或复制价格(支持Option键切换功能) + // 选择币种或执行Option+点击功能 @objc private func selectOrCopySymbol(_ sender: NSMenuItem) { guard let data = sender.representedObject as? [String: Any] else { print("❌ 无法获取菜单项数据") return } - // 检查是否按住了 Option 键,如果是则复制价格到剪贴板 + // 检查是否按住了 Option 键 let currentEvent = NSApp.currentEvent let isOptionPressed = currentEvent?.modifierFlags.contains(.option) ?? false let isCustom = data["isCustom"] as? Bool ?? false - if isOptionPressed { - // 复制价格到剪贴板 - let price = data["price"] as? Double ?? 0.0 - let displayName: String + // 获取币种信息 + let displayName: String + let symbolForURL: String // 用于生成币安URL的币种符号 - if isCustom { - guard let customSymbol = data["customSymbol"] as? CustomCryptoSymbol else { - print("❌ 无法获取自定义币种数据") - return - } - displayName = customSymbol.displayName - } else { - guard let symbol = data["symbol"] as? CryptoSymbol else { - print("❌ 无法获取默认币种数据") - return - } - displayName = symbol.displayName + if isCustom { + guard let customSymbol = data["customSymbol"] as? CustomCryptoSymbol else { + print("❌ 无法获取自定义币种数据") + return } + displayName = customSymbol.displayName + symbolForURL = customSymbol.symbol // 自定义币种的符号(如BTC, ETH) + } else { + guard let symbol = data["symbol"] as? CryptoSymbol else { + print("❌ 无法获取默认币种数据") + return + } + displayName = symbol.displayName + symbolForURL = symbol.displayName // 使用displayName获取币种基础符号(如BTC, ETH) + } - // 如果价格还没加载完成,先获取价格再复制 - if price == 0.0 { - Task { @MainActor in - print("🔄 价格未加载,正在获取 \(displayName) 价格...") - var newPrice: Double? + if isOptionPressed { + // 根据用户设置的Option+点击功能执行相应操作 + let optionAction = appSettings.optionClickAction - if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol { - newPrice = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol) - } else if let symbol = data["symbol"] as? CryptoSymbol { - newPrice = await self.priceManager.fetchSinglePrice(for: symbol) - } + switch optionAction { + case .copyPrice: + // 复制价格到剪贴板 + copyPriceToClipboard(symbol: displayName, data: data, isCustom: isCustom) - if let priceToCopy = newPrice { - let priceString = self.formatPriceWithCommas(priceToCopy) - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString("$\(priceString)", forType: .string) - - print("✅ 已复制 \(displayName) 价格到剪贴板: $\(priceString)") - } else { - print("❌ 无法获取 \(displayName) 价格") - } + case .openSpotTrading: + // 打开币安现货交易页面 + let spotSuccess = BinanceURLGenerator.openSpotTradingPage(for: symbolForURL) + if spotSuccess { + print("✅ 已打开 \(displayName) 币安现货交易页面") + } else { + print("❌ 打开 \(displayName) 币安现货交易页面失败") } - } else { - let priceString = formatPriceWithCommas(price) - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString("$\(priceString)", forType: .string) - print("✅ 已复制 \(displayName) 价格到剪贴板: $\(priceString)") + case .openFuturesTrading: + // 打开币安合约交易页面 + let futuresSuccess = BinanceURLGenerator.openFuturesTradingPage(for: symbolForURL) + if futuresSuccess { + print("✅ 已打开 \(displayName) 币安合约交易页面") + } else { + print("❌ 打开 \(displayName) 币安合约交易页面失败") + } } } else { - // 选择该币种 - if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol { - // 选择自定义币种 - 找到对应的索引并选择 - if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) { - appSettings.selectCustomCryptoSymbol(at: index) - print("✅ 已切换到自定义币种: \(customSymbol.displayName)") + // 正常点击:选择该币种 + selectSymbol(data: data, isCustom: isCustom, displayName: displayName) + } + } + + // 复制价格到剪贴板的辅助方法 + private func copyPriceToClipboard(symbol: String, data: [String: Any], isCustom: Bool) { + let price = data["price"] as? Double ?? 0.0 + + // 如果价格还没加载完成,先获取价格再复制 + if price == 0.0 { + Task { @MainActor in + print("🔄 价格未加载,正在获取 \(symbol) 价格...") + var newPrice: Double? + + if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol { + newPrice = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol) + } else if let symbol = data["symbol"] as? CryptoSymbol { + newPrice = await self.priceManager.fetchSinglePrice(for: symbol) } - // 立即更新价格管理器和UI - self.priceManager.updateCryptoSymbolSettings() - // 使用0.0价格强制更新显示状态,确保图标和文字都正确更新 - self.updateMenuBarTitle(price: 0.0) - } else if let symbol = data["symbol"] as? CryptoSymbol { - // 选择默认币种 - appSettings.saveSelectedSymbol(symbol) - print("✅ 已切换到默认币种: \(symbol.displayName)") + if let priceToCopy = newPrice { + let priceString = self.formatPriceWithCommas(priceToCopy) + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString("$\(priceString)", forType: .string) - // 立即更新价格管理器和UI - self.priceManager.updateCryptoSymbolSettings() - // 使用0.0价格强制更新显示状态,确保图标和文字都正确更新 - self.updateMenuBarTitle(price: 0.0) + print("✅ 已复制 \(symbol) 价格到剪贴板: $\(priceString)") + } else { + print("❌ 无法获取 \(symbol) 价格") + } } + } else { + let priceString = formatPriceWithCommas(price) + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString("$\(priceString)", forType: .string) + + print("✅ 已复制 \(symbol) 价格到剪贴板: $\(priceString)") + } + } + + // 选择币种的辅助方法 + private func selectSymbol(data: [String: Any], isCustom: Bool, displayName: String) { + if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol { + // 选择自定义币种 - 找到对应的索引并选择 + if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) { + appSettings.selectCustomCryptoSymbol(at: index) + print("✅ 已切换到自定义币种: \(displayName)") + } + + // 立即更新价格管理器和UI + self.priceManager.updateCryptoSymbolSettings() + // 使用0.0价格强制更新显示状态,确保图标和文字都正确更新 + self.updateMenuBarTitle(price: 0.0) + } else if let symbol = data["symbol"] as? CryptoSymbol { + // 选择默认币种 + appSettings.saveSelectedSymbol(symbol) + print("✅ 已切换到默认币种: \(displayName)") + + // 立即更新价格管理器和UI + self.priceManager.updateCryptoSymbolSettings() + // 使用0.0价格强制更新显示状态,确保图标和文字都正确更新 + self.updateMenuBarTitle(price: 0.0) } } diff --git a/Bitcoin-Monitoring/Managers/AppSettings.swift b/Bitcoin-Monitoring/Managers/AppSettings.swift index 8b4e778..a287b33 100644 --- a/Bitcoin-Monitoring/Managers/AppSettings.swift +++ b/Bitcoin-Monitoring/Managers/AppSettings.swift @@ -9,6 +9,26 @@ import Foundation import Combine import ServiceManagement +/// Option+点击操作类型枚举 +/// 定义用户按住Option键点击币种时可以执行的操作 +enum OptionClickAction: String, CaseIterable, Codable { + case copyPrice = "copyPrice" + case openSpotTrading = "openSpotTrading" + case openFuturesTrading = "openFuturesTrading" + + /// 获取操作的显示名称 + var displayName: String { + switch self { + case .copyPrice: + return "复制价格" + case .openSpotTrading: + return "Binance现货交易" + case .openFuturesTrading: + return "Binance合约交易" + } + } +} + /// 应用配置管理类 /// 负责管理用户的刷新间隔设置和其他应用配置 @MainActor @@ -45,6 +65,11 @@ class AppSettings: ObservableObject { /// 代理认证密码 @Published var proxyPassword: String = "" + // MARK: - Option+点击功能设置 + + /// Option+左键点击的操作类型 + @Published var optionClickAction: OptionClickAction = .copyPrice + // MARK: - Private Properties private let defaults = UserDefaults.standard @@ -66,6 +91,10 @@ class AppSettings: ObservableObject { private let proxyUsernameKey = "ProxyUsername" private let proxyPasswordKey = "ProxyPassword" + // MARK: - Option+点击功能配置键值 + + private let optionClickActionKey = "OptionClickAction" + // MARK: - Initialization init() { @@ -178,6 +207,20 @@ class AppSettings: ObservableObject { proxyUsername = defaults.string(forKey: proxyUsernameKey) ?? "" proxyPassword = defaults.string(forKey: proxyPasswordKey) ?? "" + // 加载Option+点击功能设置 + if let optionClickActionRaw = defaults.string(forKey: optionClickActionKey), + let savedAction = OptionClickAction(rawValue: optionClickActionRaw) { + optionClickAction = savedAction + #if DEBUG + print("🔧 [AppSettings] ✅ 已加载Option+点击功能: \(savedAction.displayName)") + #endif + } else { + optionClickAction = .copyPrice + #if DEBUG + print("🔧 [AppSettings] ❌ 未找到有效Option+点击功能配置,使用默认值: \(optionClickAction.displayName)") + #endif + } + // 检查实际的自启动状态并同步 checkAndSyncLaunchAtLoginStatus() @@ -185,7 +228,7 @@ class AppSettings: ObservableObject { let proxyInfo = proxyEnabled ? "\(proxyHost):\(proxyPort)" : "未启用" let authInfo = proxyEnabled && !proxyUsername.isEmpty ? " (认证: \(proxyUsername))" : "" let customInfo = useCustomSymbol && !customCryptoSymbols.isEmpty ? " (自定义: \(customCryptoSymbols.count)个)" : "" - print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(getCurrentActiveDisplayName())\(customInfo), 开机自启动: \(launchAtLogin), 代理: \(proxyInfo)\(authInfo)") + print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(getCurrentActiveDisplayName())\(customInfo), 开机自启动: \(launchAtLogin), 代理: \(proxyInfo)\(authInfo), Option+点击: \(optionClickAction.displayName)") #endif } @@ -223,8 +266,12 @@ class AppSettings: ObservableObject { defaults.set("", forKey: proxyUsernameKey) defaults.set("", forKey: proxyPasswordKey) + // 重置Option+点击功能设置 + optionClickAction = .copyPrice + defaults.set(optionClickAction.rawValue, forKey: optionClickActionKey) + #if DEBUG - print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 自定义币种: 已清除, 代理: 已重置") + print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 自定义币种: 已清除, 代理: 已重置, Option+点击: \(optionClickAction.displayName)") #endif // 重置开机自启动设置 @@ -344,6 +391,19 @@ class AppSettings: ObservableObject { return false } + // MARK: - Option+点击功能相关方法 + + /// 保存Option+点击功能设置 + /// - Parameter action: 要保存的操作类型 + func saveOptionClickAction(_ action: OptionClickAction) { + optionClickAction = action + defaults.set(action.rawValue, forKey: optionClickActionKey) + + #if DEBUG + print("🔧 [AppSettings] 保存Option+点击功能设置: \(action.displayName)") + #endif + } + // MARK: - 开机自启动相关方法 /// 切换开机自启动状态 diff --git a/Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift b/Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift new file mode 100644 index 0000000..39dc21f --- /dev/null +++ b/Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift @@ -0,0 +1,72 @@ +// +// BinanceURLGenerator.swift +// Bitcoin Monitoring +// +// Created by Mark on 2025/11/11. +// + +import Foundation +import AppKit + +/** + * 币安URL生成器 + * 负责生成币安现货和合约交易页面的URL + */ +struct BinanceURLGenerator { + + /// 生成币安现货交易页面URL + /// - Parameter symbol: 币种符号(如 BTC、ETH) + /// - Returns: 现货交易页面URL字符串 + static func generateSpotTradingURL(for symbol: String) -> String { + return "https://www.binance.com/zh-CN/trade/\(symbol)_USDT?type=spot" + } + + /// 生成币安合约交易页面URL + /// - Parameter symbol: 币种符号(如 BTC、ETH) + /// - Returns: 合约交易页面URL字符串 + static func generateFuturesTradingURL(for symbol: String) -> String { + return "https://www.binance.com/zh-CN/futures/\(symbol)USDT" + } + + /// 打开币安现货交易页面 + /// - Parameter symbol: 币种符号 + /// - Returns: 是否成功打开页面 + static func openSpotTradingPage(for symbol: String) -> Bool { + let url = generateSpotTradingURL(for: symbol) + return openURL(url) + } + + /// 打开币安合约交易页面 + /// - Parameter symbol: 币种符号 + /// - Returns: 是否成功打开页面 + static func openFuturesTradingPage(for symbol: String) -> Bool { + let url = generateFuturesTradingURL(for: symbol) + return openURL(url) + } + + /// 使用系统默认浏览器打开URL + /// - Parameter urlString: URL字符串 + /// - Returns: 是否成功打开 + private static func openURL(_ urlString: String) -> Bool { + guard let url = URL(string: urlString) else { + #if DEBUG + print("❌ [BinanceURLGenerator] 无效的URL: \(urlString)") + #endif + return false + } + + // 使用NSWorkspace打开URL(macOS专用) + if #available(macOS 10.15, *) { + NSWorkspace.shared.open(url) + } else { + // 对于较老的macOS版本,使用NSWorkspace的旧API + let workspace = NSWorkspace.shared + workspace.open(url) + } + + #if DEBUG + print("✅ [BinanceURLGenerator] 已打开URL: \(urlString)") + #endif + return true + } +} \ No newline at end of file diff --git a/Bitcoin-Monitoring/Views/AboutWindowView.swift b/Bitcoin-Monitoring/Views/AboutWindowView.swift index 8ec94c5..6cacbde 100644 --- a/Bitcoin-Monitoring/Views/AboutWindowView.swift +++ b/Bitcoin-Monitoring/Views/AboutWindowView.swift @@ -147,6 +147,10 @@ struct AboutWindowView: View { FeatureRow(icon: "exclamationmark.triangle.fill", title: "智能重试机制", description: "网络错误自动恢复") + FeatureRow(icon: "network", title: "支持代理服务器", description: "适用于受限网络环境") + + FeatureRow(icon: "bell.fill", title: "更多功能", description: "等待你的发掘!") + } } @@ -197,7 +201,7 @@ struct AboutWindowView: View { } } .padding(24) - .frame(width: 420, height: 500) + .frame(width: 420, height: 590) // 设置固定高度以适应内容 .alert("检测更新", isPresented: $showingUpdateAlert) { Button("确定", role: .cancel) { // 如果消息中包含"发现新版本",则打开发布页面并关闭窗口 diff --git a/Bitcoin-Monitoring/Views/PreferencesWindowView.swift b/Bitcoin-Monitoring/Views/PreferencesWindowView.swift index 5b7c8cb..5fd272b 100644 --- a/Bitcoin-Monitoring/Views/PreferencesWindowView.swift +++ b/Bitcoin-Monitoring/Views/PreferencesWindowView.swift @@ -53,6 +53,7 @@ struct PreferencesWindowView: View { @State private var tempProxyUsername: String @State private var tempProxyPassword: String @State private var tempLaunchAtLogin: Bool + @State private var tempOptionClickAction: OptionClickAction // 验证状态 @State private var showingValidationError = false @@ -101,6 +102,7 @@ struct PreferencesWindowView: View { self._tempProxyUsername = State(initialValue: appSettings.proxyUsername) self._tempProxyPassword = State(initialValue: appSettings.proxyPassword) self._tempLaunchAtLogin = State(initialValue: appSettings.launchAtLogin) + self._tempOptionClickAction = State(initialValue: appSettings.optionClickAction) } var body: some View { @@ -226,11 +228,12 @@ struct PreferencesWindowView: View { } } - // 通用设置视图(刷新间隔 + 启动设置) + // 通用设置视图(刷新间隔 + 启动设置 + Option+点击功能) private var generalSettingsView: some View { VStack(spacing: 24) { refreshSettingsView launchSettingsView + optionClickSettingsView } } @@ -282,6 +285,37 @@ struct PreferencesWindowView: View { } } + // Option+点击功能设置视图 + private var optionClickSettingsView: some View { + SettingsGroupView(title: "Option+点击功能", icon: "cursorarrow.click.2") { + VStack(alignment: .leading, spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("按住Option+左键功能") + .font(.subheadline) + .foregroundColor(.primary) + + Text("设置按住Option键点击币种时执行的操作") + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + // 使用Picker让用户选择操作类型 + Picker("Option+点击操作", selection: $tempOptionClickAction) { + ForEach(OptionClickAction.allCases, id: \.self) { action in + Text(action.displayName).tag(action) + } + } + .pickerStyle(MenuPickerStyle()) + .frame(width: 180) + .labelsHidden() + } + } + } + } + // 代理设置视图 private var proxySettingsView: some View { SettingsGroupView(title: "代理设置", icon: "network") { @@ -681,6 +715,12 @@ struct PreferencesWindowView: View { print("✅ [Preferences] 已设置开机自启动: \(tempLaunchAtLogin)") } + // 保存Option+点击功能设置 + if tempOptionClickAction != appSettings.optionClickAction { + appSettings.saveOptionClickAction(tempOptionClickAction) + print("✅ [Preferences] 已保存Option+点击功能: \(tempOptionClickAction.displayName)") + } + // 保存代理设置 let port = Int(tempProxyPort) ?? 3128 appSettings.saveProxySettings( diff --git a/README.md b/README.md index c204654..239c37d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ - **价格复制功能**: 支持一键复制当前价格到剪贴板 - **配置持久化**: 用户设置自动保存,重启后保持配置 - **开机自启动**: 可选是否开机自动启动APP +- **代理支持**: 支持 HTTP/HTTPS 代理配置,支持代理认证 ### 🎨 用户体验 - **中文界面**: 完整的中文用户界面 @@ -67,7 +68,7 @@ - **部署目标**: macOS 13.1 ### 网络要求 -- 需要稳定的互联网连接 +- 需要稳定的互联网连接,国内用户建议使用科学上网工具 或 设置代理服务器。 - 访问币安 API (`https://api.binance.com`) 的网络权限 ## 🚀 快速开始 @@ -124,24 +125,6 @@ xcodebuild -project "Bitcoin Monitoring.xcodeproj" -scheme "Bitcoin Monitoring" - **自定义币种组件**: 专门的币种管理界面,支持添加、删除和切换自定义币种 - **图标缓存系统**: `CryptoIconGenerator` 实现图标生成和缓存机制,避免重复生成 -### 并发处理 - -```swift -// 主线程 UI 更新 -@MainActor -class BTCMenuBarApp: ObservableObject - -// 异步网络请求 -func fetchPrice() async throws -> Double - -// Combine 响应式流 -priceManager.$currentPrice - .receive(on: DispatchQueue.main) - .sink { [weak self] price in - self?.updateMenuBarTitle(price: price) - } -``` - ## 🔧 API 集成 ### 币安 API 端点 @@ -161,7 +144,7 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL} - **网络异常处理**: 用户友好的错误提示和状态显示 - **代理配置支持**: 完整的 HTTP/HTTPS 代理配置和连接测试 -## ⚙️ 配置管理 +## ⚙️ 配置管理(持久化配置) ### UserDefaults 键值 - `BTCRefreshInterval`: 刷新间隔设置 @@ -171,9 +154,7 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL} - `SelectedCustomSymbolIndex`: 当前选中的自定义币种索引 - `UseCustomSymbol`: 是否使用自定义币种 - `ProxyEnabled/ProxyHost/ProxyPort/ProxyUsername/ProxyPassword`: 代理设置(包括认证) - -### 配置持久化 -所有用户设置通过 `AppSettings` 类的 `@Published` 属性自动保存到 UserDefaults,应用重启后保持配置。使用 Combine 框架实现配置变化的实时响应。 +- `OptionClickAction`: 选项点击操作设置 ## 🔧 故障排除 @@ -184,10 +165,12 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL} **问题**: 双击应用图标无反应 **解决方案**: ```bash -# 指定该命令以启动APP +# 执行该命令以启动APP sudo xattr -d com.apple.quarantine "/Applications/Bitcoin Monitoring.app" -# 或者在系统偏好设置中允许应用运行 +# 或者 + +# 系统偏好设置中允许应用运行 系统设置 → 隐私与安全性 → 安全性 → 已阻止“Bitcoin Monitoring.app”以保护Mac → 仍要打开 ```