mirror of
https://github.com/jiayouzl/Bitcoin-Monitoring.git
synced 2025-11-25 03:15:24 +08:00
feat: 添加Option+点击功能设置,支持复制价格和打开币安交易页面
This commit is contained in:
@@ -78,7 +78,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1620;
|
LastSwiftUpdateCheck = 1620;
|
||||||
LastUpgradeCheck = 2600;
|
LastUpgradeCheck = 2610;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
4E94106F2EB09F90003658CB = {
|
4E94106F2EB09F90003658CB = {
|
||||||
CreatedOnToolsVersion = 16.2;
|
CreatedOnToolsVersion = 16.2;
|
||||||
@@ -131,6 +131,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -196,6 +197,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -273,7 +275,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||||
MARKETING_VERSION = 1.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -306,7 +308,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||||
MARKETING_VERSION = 1.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@@ -450,91 +450,130 @@ class MenuBarManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 选择币种或复制价格(支持Option键切换功能)
|
// 选择币种或执行Option+点击功能
|
||||||
@objc private func selectOrCopySymbol(_ sender: NSMenuItem) {
|
@objc private func selectOrCopySymbol(_ sender: NSMenuItem) {
|
||||||
guard let data = sender.representedObject as? [String: Any] else {
|
guard let data = sender.representedObject as? [String: Any] else {
|
||||||
print("❌ 无法获取菜单项数据")
|
print("❌ 无法获取菜单项数据")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否按住了 Option 键,如果是则复制价格到剪贴板
|
// 检查是否按住了 Option 键
|
||||||
let currentEvent = NSApp.currentEvent
|
let currentEvent = NSApp.currentEvent
|
||||||
let isOptionPressed = currentEvent?.modifierFlags.contains(.option) ?? false
|
let isOptionPressed = currentEvent?.modifierFlags.contains(.option) ?? false
|
||||||
let isCustom = data["isCustom"] as? Bool ?? false
|
let isCustom = data["isCustom"] as? Bool ?? false
|
||||||
|
|
||||||
if isOptionPressed {
|
// 获取币种信息
|
||||||
// 复制价格到剪贴板
|
let displayName: String
|
||||||
let price = data["price"] as? Double ?? 0.0
|
let symbolForURL: String // 用于生成币安URL的币种符号
|
||||||
let displayName: String
|
|
||||||
|
|
||||||
if isCustom {
|
if isCustom {
|
||||||
guard let customSymbol = data["customSymbol"] as? CustomCryptoSymbol else {
|
guard let customSymbol = data["customSymbol"] as? CustomCryptoSymbol else {
|
||||||
print("❌ 无法获取自定义币种数据")
|
print("❌ 无法获取自定义币种数据")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
displayName = customSymbol.displayName
|
|
||||||
} else {
|
|
||||||
guard let symbol = data["symbol"] as? CryptoSymbol else {
|
|
||||||
print("❌ 无法获取默认币种数据")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
displayName = symbol.displayName
|
|
||||||
}
|
}
|
||||||
|
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 isOptionPressed {
|
||||||
if price == 0.0 {
|
// 根据用户设置的Option+点击功能执行相应操作
|
||||||
Task { @MainActor in
|
let optionAction = appSettings.optionClickAction
|
||||||
print("🔄 价格未加载,正在获取 \(displayName) 价格...")
|
|
||||||
var newPrice: Double?
|
|
||||||
|
|
||||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
switch optionAction {
|
||||||
newPrice = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol)
|
case .copyPrice:
|
||||||
} else if let symbol = data["symbol"] as? CryptoSymbol {
|
// 复制价格到剪贴板
|
||||||
newPrice = await self.priceManager.fetchSinglePrice(for: symbol)
|
copyPriceToClipboard(symbol: displayName, data: data, isCustom: isCustom)
|
||||||
}
|
|
||||||
|
|
||||||
if let priceToCopy = newPrice {
|
case .openSpotTrading:
|
||||||
let priceString = self.formatPriceWithCommas(priceToCopy)
|
// 打开币安现货交易页面
|
||||||
let pasteboard = NSPasteboard.general
|
let spotSuccess = BinanceURLGenerator.openSpotTradingPage(for: symbolForURL)
|
||||||
pasteboard.clearContents()
|
if spotSuccess {
|
||||||
pasteboard.setString("$\(priceString)", forType: .string)
|
print("✅ 已打开 \(displayName) 币安现货交易页面")
|
||||||
|
} else {
|
||||||
print("✅ 已复制 \(displayName) 价格到剪贴板: $\(priceString)")
|
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 {
|
} else {
|
||||||
// 选择该币种
|
// 正常点击:选择该币种
|
||||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
selectSymbol(data: data, isCustom: isCustom, displayName: displayName)
|
||||||
// 选择自定义币种 - 找到对应的索引并选择
|
}
|
||||||
if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) {
|
}
|
||||||
appSettings.selectCustomCryptoSymbol(at: index)
|
|
||||||
print("✅ 已切换到自定义币种: \(customSymbol.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
|
if let priceToCopy = newPrice {
|
||||||
self.priceManager.updateCryptoSymbolSettings()
|
let priceString = self.formatPriceWithCommas(priceToCopy)
|
||||||
// 使用0.0价格强制更新显示状态,确保图标和文字都正确更新
|
let pasteboard = NSPasteboard.general
|
||||||
self.updateMenuBarTitle(price: 0.0)
|
pasteboard.clearContents()
|
||||||
} else if let symbol = data["symbol"] as? CryptoSymbol {
|
pasteboard.setString("$\(priceString)", forType: .string)
|
||||||
// 选择默认币种
|
|
||||||
appSettings.saveSelectedSymbol(symbol)
|
|
||||||
print("✅ 已切换到默认币种: \(symbol.displayName)")
|
|
||||||
|
|
||||||
// 立即更新价格管理器和UI
|
print("✅ 已复制 \(symbol) 价格到剪贴板: $\(priceString)")
|
||||||
self.priceManager.updateCryptoSymbolSettings()
|
} else {
|
||||||
// 使用0.0价格强制更新显示状态,确保图标和文字都正确更新
|
print("❌ 无法获取 \(symbol) 价格")
|
||||||
self.updateMenuBarTitle(price: 0.0)
|
}
|
||||||
}
|
}
|
||||||
|
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,26 @@ import Foundation
|
|||||||
import Combine
|
import Combine
|
||||||
import ServiceManagement
|
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
|
@MainActor
|
||||||
@@ -45,6 +65,11 @@ class AppSettings: ObservableObject {
|
|||||||
/// 代理认证密码
|
/// 代理认证密码
|
||||||
@Published var proxyPassword: String = ""
|
@Published var proxyPassword: String = ""
|
||||||
|
|
||||||
|
// MARK: - Option+点击功能设置
|
||||||
|
|
||||||
|
/// Option+左键点击的操作类型
|
||||||
|
@Published var optionClickAction: OptionClickAction = .copyPrice
|
||||||
|
|
||||||
// MARK: - Private Properties
|
// MARK: - Private Properties
|
||||||
|
|
||||||
private let defaults = UserDefaults.standard
|
private let defaults = UserDefaults.standard
|
||||||
@@ -66,6 +91,10 @@ class AppSettings: ObservableObject {
|
|||||||
private let proxyUsernameKey = "ProxyUsername"
|
private let proxyUsernameKey = "ProxyUsername"
|
||||||
private let proxyPasswordKey = "ProxyPassword"
|
private let proxyPasswordKey = "ProxyPassword"
|
||||||
|
|
||||||
|
// MARK: - Option+点击功能配置键值
|
||||||
|
|
||||||
|
private let optionClickActionKey = "OptionClickAction"
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -178,6 +207,20 @@ class AppSettings: ObservableObject {
|
|||||||
proxyUsername = defaults.string(forKey: proxyUsernameKey) ?? ""
|
proxyUsername = defaults.string(forKey: proxyUsernameKey) ?? ""
|
||||||
proxyPassword = defaults.string(forKey: proxyPasswordKey) ?? ""
|
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()
|
checkAndSyncLaunchAtLoginStatus()
|
||||||
|
|
||||||
@@ -185,7 +228,7 @@ class AppSettings: ObservableObject {
|
|||||||
let proxyInfo = proxyEnabled ? "\(proxyHost):\(proxyPort)" : "未启用"
|
let proxyInfo = proxyEnabled ? "\(proxyHost):\(proxyPort)" : "未启用"
|
||||||
let authInfo = proxyEnabled && !proxyUsername.isEmpty ? " (认证: \(proxyUsername))" : ""
|
let authInfo = proxyEnabled && !proxyUsername.isEmpty ? " (认证: \(proxyUsername))" : ""
|
||||||
let customInfo = useCustomSymbol && !customCryptoSymbols.isEmpty ? " (自定义: \(customCryptoSymbols.count)个)" : ""
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +266,12 @@ class AppSettings: ObservableObject {
|
|||||||
defaults.set("", forKey: proxyUsernameKey)
|
defaults.set("", forKey: proxyUsernameKey)
|
||||||
defaults.set("", forKey: proxyPasswordKey)
|
defaults.set("", forKey: proxyPasswordKey)
|
||||||
|
|
||||||
|
// 重置Option+点击功能设置
|
||||||
|
optionClickAction = .copyPrice
|
||||||
|
defaults.set(optionClickAction.rawValue, forKey: optionClickActionKey)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 自定义币种: 已清除, 代理: 已重置")
|
print("🔧 [AppSettings] 重置完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(selectedSymbol.displayName), 自定义币种: 已清除, 代理: 已重置, Option+点击: \(optionClickAction.displayName)")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 重置开机自启动设置
|
// 重置开机自启动设置
|
||||||
@@ -344,6 +391,19 @@ class AppSettings: ObservableObject {
|
|||||||
return false
|
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: - 开机自启动相关方法
|
// MARK: - 开机自启动相关方法
|
||||||
|
|
||||||
/// 切换开机自启动状态
|
/// 切换开机自启动状态
|
||||||
|
|||||||
72
Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift
Normal file
72
Bitcoin-Monitoring/Utils/BinanceURLGenerator.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,10 @@ struct AboutWindowView: View {
|
|||||||
|
|
||||||
FeatureRow(icon: "exclamationmark.triangle.fill", title: "智能重试机制", description: "网络错误自动恢复")
|
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)
|
.padding(24)
|
||||||
.frame(width: 420, height: 500)
|
.frame(width: 420, height: 590) // 设置固定高度以适应内容
|
||||||
.alert("检测更新", isPresented: $showingUpdateAlert) {
|
.alert("检测更新", isPresented: $showingUpdateAlert) {
|
||||||
Button("确定", role: .cancel) {
|
Button("确定", role: .cancel) {
|
||||||
// 如果消息中包含"发现新版本",则打开发布页面并关闭窗口
|
// 如果消息中包含"发现新版本",则打开发布页面并关闭窗口
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ struct PreferencesWindowView: View {
|
|||||||
@State private var tempProxyUsername: String
|
@State private var tempProxyUsername: String
|
||||||
@State private var tempProxyPassword: String
|
@State private var tempProxyPassword: String
|
||||||
@State private var tempLaunchAtLogin: Bool
|
@State private var tempLaunchAtLogin: Bool
|
||||||
|
@State private var tempOptionClickAction: OptionClickAction
|
||||||
|
|
||||||
// 验证状态
|
// 验证状态
|
||||||
@State private var showingValidationError = false
|
@State private var showingValidationError = false
|
||||||
@@ -101,6 +102,7 @@ struct PreferencesWindowView: View {
|
|||||||
self._tempProxyUsername = State(initialValue: appSettings.proxyUsername)
|
self._tempProxyUsername = State(initialValue: appSettings.proxyUsername)
|
||||||
self._tempProxyPassword = State(initialValue: appSettings.proxyPassword)
|
self._tempProxyPassword = State(initialValue: appSettings.proxyPassword)
|
||||||
self._tempLaunchAtLogin = State(initialValue: appSettings.launchAtLogin)
|
self._tempLaunchAtLogin = State(initialValue: appSettings.launchAtLogin)
|
||||||
|
self._tempOptionClickAction = State(initialValue: appSettings.optionClickAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -226,11 +228,12 @@ struct PreferencesWindowView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用设置视图(刷新间隔 + 启动设置)
|
// 通用设置视图(刷新间隔 + 启动设置 + Option+点击功能)
|
||||||
private var generalSettingsView: some View {
|
private var generalSettingsView: some View {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
refreshSettingsView
|
refreshSettingsView
|
||||||
launchSettingsView
|
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 {
|
private var proxySettingsView: some View {
|
||||||
SettingsGroupView(title: "代理设置", icon: "network") {
|
SettingsGroupView(title: "代理设置", icon: "network") {
|
||||||
@@ -681,6 +715,12 @@ struct PreferencesWindowView: View {
|
|||||||
print("✅ [Preferences] 已设置开机自启动: \(tempLaunchAtLogin)")
|
print("✅ [Preferences] 已设置开机自启动: \(tempLaunchAtLogin)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存Option+点击功能设置
|
||||||
|
if tempOptionClickAction != appSettings.optionClickAction {
|
||||||
|
appSettings.saveOptionClickAction(tempOptionClickAction)
|
||||||
|
print("✅ [Preferences] 已保存Option+点击功能: \(tempOptionClickAction.displayName)")
|
||||||
|
}
|
||||||
|
|
||||||
// 保存代理设置
|
// 保存代理设置
|
||||||
let port = Int(tempProxyPort) ?? 3128
|
let port = Int(tempProxyPort) ?? 3128
|
||||||
appSettings.saveProxySettings(
|
appSettings.saveProxySettings(
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -36,6 +36,7 @@
|
|||||||
- **价格复制功能**: 支持一键复制当前价格到剪贴板
|
- **价格复制功能**: 支持一键复制当前价格到剪贴板
|
||||||
- **配置持久化**: 用户设置自动保存,重启后保持配置
|
- **配置持久化**: 用户设置自动保存,重启后保持配置
|
||||||
- **开机自启动**: 可选是否开机自动启动APP
|
- **开机自启动**: 可选是否开机自动启动APP
|
||||||
|
- **代理支持**: 支持 HTTP/HTTPS 代理配置,支持代理认证
|
||||||
|
|
||||||
### 🎨 用户体验
|
### 🎨 用户体验
|
||||||
- **中文界面**: 完整的中文用户界面
|
- **中文界面**: 完整的中文用户界面
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
- **部署目标**: macOS 13.1
|
- **部署目标**: macOS 13.1
|
||||||
|
|
||||||
### 网络要求
|
### 网络要求
|
||||||
- 需要稳定的互联网连接
|
- 需要稳定的互联网连接,国内用户建议使用科学上网工具 或 设置代理服务器。
|
||||||
- 访问币安 API (`https://api.binance.com`) 的网络权限
|
- 访问币安 API (`https://api.binance.com`) 的网络权限
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
@@ -124,24 +125,6 @@ xcodebuild -project "Bitcoin Monitoring.xcodeproj" -scheme "Bitcoin Monitoring"
|
|||||||
- **自定义币种组件**: 专门的币种管理界面,支持添加、删除和切换自定义币种
|
- **自定义币种组件**: 专门的币种管理界面,支持添加、删除和切换自定义币种
|
||||||
- **图标缓存系统**: `CryptoIconGenerator` 实现图标生成和缓存机制,避免重复生成
|
- **图标缓存系统**: `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 集成
|
||||||
|
|
||||||
### 币安 API 端点
|
### 币安 API 端点
|
||||||
@@ -161,7 +144,7 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL}
|
|||||||
- **网络异常处理**: 用户友好的错误提示和状态显示
|
- **网络异常处理**: 用户友好的错误提示和状态显示
|
||||||
- **代理配置支持**: 完整的 HTTP/HTTPS 代理配置和连接测试
|
- **代理配置支持**: 完整的 HTTP/HTTPS 代理配置和连接测试
|
||||||
|
|
||||||
## ⚙️ 配置管理
|
## ⚙️ 配置管理(持久化配置)
|
||||||
|
|
||||||
### UserDefaults 键值
|
### UserDefaults 键值
|
||||||
- `BTCRefreshInterval`: 刷新间隔设置
|
- `BTCRefreshInterval`: 刷新间隔设置
|
||||||
@@ -171,9 +154,7 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL}
|
|||||||
- `SelectedCustomSymbolIndex`: 当前选中的自定义币种索引
|
- `SelectedCustomSymbolIndex`: 当前选中的自定义币种索引
|
||||||
- `UseCustomSymbol`: 是否使用自定义币种
|
- `UseCustomSymbol`: 是否使用自定义币种
|
||||||
- `ProxyEnabled/ProxyHost/ProxyPort/ProxyUsername/ProxyPassword`: 代理设置(包括认证)
|
- `ProxyEnabled/ProxyHost/ProxyPort/ProxyUsername/ProxyPassword`: 代理设置(包括认证)
|
||||||
|
- `OptionClickAction`: 选项点击操作设置
|
||||||
### 配置持久化
|
|
||||||
所有用户设置通过 `AppSettings` 类的 `@Published` 属性自动保存到 UserDefaults,应用重启后保持配置。使用 Combine 框架实现配置变化的实时响应。
|
|
||||||
|
|
||||||
## 🔧 故障排除
|
## 🔧 故障排除
|
||||||
|
|
||||||
@@ -184,10 +165,12 @@ GET https://api.binance.com/api/v3/ticker/price?symbol={SYMBOL}
|
|||||||
**问题**: 双击应用图标无反应
|
**问题**: 双击应用图标无反应
|
||||||
**解决方案**:
|
**解决方案**:
|
||||||
```bash
|
```bash
|
||||||
# 指定该命令以启动APP
|
# 执行该命令以启动APP
|
||||||
sudo xattr -d com.apple.quarantine "/Applications/Bitcoin Monitoring.app"
|
sudo xattr -d com.apple.quarantine "/Applications/Bitcoin Monitoring.app"
|
||||||
|
|
||||||
# 或者在系统偏好设置中允许应用运行
|
# 或者
|
||||||
|
|
||||||
|
# 系统偏好设置中允许应用运行
|
||||||
系统设置 → 隐私与安全性 → 安全性 → 已阻止“Bitcoin Monitoring.app”以保护Mac → 仍要打开
|
系统设置 → 隐私与安全性 → 安全性 → 已阻止“Bitcoin Monitoring.app”以保护Mac → 仍要打开
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user