mirror of
https://github.com/jiayouzl/Bitcoin-Monitoring.git
synced 2025-11-25 03:15:24 +08:00
feat: 更新自定义币种管理,支持多个币种及验证功能
This commit is contained in:
@@ -273,7 +273,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -306,7 +306,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mark.bitcoin-monitoring";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -59,7 +59,7 @@ class MenuBarManager: NSObject, ObservableObject {
|
||||
.store(in: &cancellables)
|
||||
|
||||
// 监听自定义币种配置变化
|
||||
appSettings.$customCryptoSymbol
|
||||
appSettings.$customCryptoSymbols
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.priceManager.updateCryptoSymbolSettings()
|
||||
@@ -245,8 +245,8 @@ class MenuBarManager: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
// 添加自定义币种菜单项(如果存在)- 显示在最后
|
||||
var customSymbolMenuItem: NSMenuItem?
|
||||
if let customSymbol = appSettings.customCryptoSymbol {
|
||||
var customSymbolMenuItems: [NSMenuItem] = []
|
||||
for customSymbol in appSettings.customCryptoSymbols {
|
||||
let isCurrent = customSymbol.isCurrentSymbol(currentApiSymbol)
|
||||
let placeholderTitle = isCurrent ? "✓ \(customSymbol.displayName) (自定义): 加载中..." : " \(customSymbol.displayName) (自定义): 加载中..."
|
||||
let item = NSMenuItem(title: placeholderTitle, action: #selector(self.selectOrCopySymbol(_:)), keyEquivalent: "")
|
||||
@@ -258,7 +258,7 @@ class MenuBarManager: NSObject, ObservableObject {
|
||||
item.isEnabled = true
|
||||
item.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true]
|
||||
menu.addItem(item)
|
||||
customSymbolMenuItem = item
|
||||
customSymbolMenuItems.append(item)
|
||||
}
|
||||
|
||||
// 异步并发获取所有币种价格并更新对应的菜单项
|
||||
@@ -294,22 +294,24 @@ class MenuBarManager: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
// 更新自定义币种菜单项
|
||||
if let customSymbol = self.appSettings.customCryptoSymbol,
|
||||
let menuItem = customSymbolMenuItem {
|
||||
let isCurrent = customSymbol.isCurrentSymbol(currentSymbolAfter)
|
||||
for (index, customSymbol) in self.appSettings.customCryptoSymbols.enumerated() {
|
||||
if index < customSymbolMenuItems.count {
|
||||
let menuItem = customSymbolMenuItems[index]
|
||||
let isCurrent = customSymbol.isCurrentSymbol(currentSymbolAfter)
|
||||
|
||||
if let price = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol) {
|
||||
let title = isCurrent ? "✓ \(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))" : " \(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))"
|
||||
menuItem.title = title
|
||||
menuItem.isEnabled = true
|
||||
menuItem.target = self
|
||||
menuItem.representedObject = ["customSymbol": customSymbol, "price": price, "isCustom": true]
|
||||
} else {
|
||||
let title = isCurrent ? "✓ \(customSymbol.displayName) (自定义): 错误" : " \(customSymbol.displayName) (自定义): 错误"
|
||||
menuItem.title = title
|
||||
menuItem.isEnabled = false
|
||||
menuItem.target = self
|
||||
menuItem.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true]
|
||||
if let price = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol) {
|
||||
let title = isCurrent ? "✓ \(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))" : " \(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))"
|
||||
menuItem.title = title
|
||||
menuItem.isEnabled = true
|
||||
menuItem.target = self
|
||||
menuItem.representedObject = ["customSymbol": customSymbol, "price": price, "isCustom": true]
|
||||
} else {
|
||||
let title = isCurrent ? "✓ \(customSymbol.displayName) (自定义): 错误" : " \(customSymbol.displayName) (自定义): 错误"
|
||||
menuItem.title = title
|
||||
menuItem.isEnabled = false
|
||||
menuItem.target = self
|
||||
menuItem.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,9 +502,11 @@ class MenuBarManager: NSObject, ObservableObject {
|
||||
} else {
|
||||
// 选择该币种
|
||||
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
|
||||
// 选择自定义币种
|
||||
appSettings.saveCustomCryptoSymbol(customSymbol)
|
||||
print("✅ 已切换到自定义币种: \(customSymbol.displayName)")
|
||||
// 选择自定义币种 - 找到对应的索引并选择
|
||||
if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) {
|
||||
appSettings.selectCustomCryptoSymbol(at: index)
|
||||
print("✅ 已切换到自定义币种: \(customSymbol.displayName)")
|
||||
}
|
||||
|
||||
// 立即更新价格管理器
|
||||
self.priceManager.updateCryptoSymbolSettings()
|
||||
|
||||
@@ -25,8 +25,10 @@ class AppSettings: ObservableObject {
|
||||
|
||||
// MARK: - 自定义币种相关属性
|
||||
|
||||
/// 当前选中的自定义币种(如果有)
|
||||
@Published var customCryptoSymbol: CustomCryptoSymbol?
|
||||
/// 自定义币种列表(最多5个)
|
||||
@Published var customCryptoSymbols: [CustomCryptoSymbol] = []
|
||||
/// 当前选中的自定义币种索引(如果使用自定义币种)
|
||||
@Published var selectedCustomSymbolIndex: Int?
|
||||
/// 是否使用自定义币种
|
||||
@Published var useCustomSymbol: Bool = false
|
||||
|
||||
@@ -52,7 +54,8 @@ class AppSettings: ObservableObject {
|
||||
|
||||
// MARK: - 自定义币种配置键值
|
||||
|
||||
private let customSymbolKey = "CustomCryptoSymbol"
|
||||
private let customSymbolsKey = "CustomCryptoSymbols"
|
||||
private let selectedCustomSymbolIndexKey = "SelectedCustomSymbolIndex"
|
||||
private let useCustomSymbolKey = "UseCustomSymbol"
|
||||
|
||||
// MARK: - 代理配置键值
|
||||
@@ -141,17 +144,26 @@ class AppSettings: ObservableObject {
|
||||
// 加载开机自启动设置
|
||||
launchAtLogin = defaults.bool(forKey: launchAtLoginKey)
|
||||
|
||||
// 加载自定义币种设置 - 总是先尝试读取数据
|
||||
if let customSymbolData = defaults.data(forKey: customSymbolKey),
|
||||
let customSymbol = try? JSONDecoder().decode(CustomCryptoSymbol.self, from: customSymbolData) {
|
||||
customCryptoSymbol = customSymbol
|
||||
// 加载自定义币种设置
|
||||
if let customSymbolsData = defaults.data(forKey: customSymbolsKey),
|
||||
let customSymbols = try? JSONDecoder().decode([CustomCryptoSymbol].self, from: customSymbolsData) {
|
||||
customCryptoSymbols = customSymbols
|
||||
// 加载选中的自定义币种索引
|
||||
let savedIndex = defaults.integer(forKey: selectedCustomSymbolIndexKey)
|
||||
if savedIndex >= 0 && savedIndex < customSymbols.count {
|
||||
selectedCustomSymbolIndex = savedIndex
|
||||
}
|
||||
// 根据保存的状态决定是否使用自定义币种
|
||||
useCustomSymbol = defaults.bool(forKey: useCustomSymbolKey)
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ✅ 已加载自定义币种: \(customSymbol.displayName),使用状态: \(useCustomSymbol)")
|
||||
print("🔧 [AppSettings] ✅ 已加载 \(customSymbols.count) 个自定义币种,使用状态: \(useCustomSymbol)")
|
||||
if let index = selectedCustomSymbolIndex {
|
||||
print("🔧 [AppSettings] 当前选中自定义币种: \(customSymbols[index].displayName)")
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
customCryptoSymbol = nil
|
||||
customCryptoSymbols = []
|
||||
selectedCustomSymbolIndex = nil
|
||||
useCustomSymbol = false
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ℹ️ 未找到自定义币种数据")
|
||||
@@ -172,7 +184,7 @@ class AppSettings: ObservableObject {
|
||||
#if DEBUG
|
||||
let proxyInfo = proxyEnabled ? "\(proxyHost):\(proxyPort)" : "未启用"
|
||||
let authInfo = proxyEnabled && !proxyUsername.isEmpty ? " (认证: \(proxyUsername))" : ""
|
||||
let customInfo = useCustomSymbol && customCryptoSymbol != nil ? " (自定义: \(customCryptoSymbol!.displayName))" : ""
|
||||
let customInfo = useCustomSymbol && !customCryptoSymbols.isEmpty ? " (自定义: \(customCryptoSymbols.count)个)" : ""
|
||||
print("🔧 [AppSettings] 配置加载完成 - 刷新间隔: \(refreshInterval.displayText), 币种: \(getCurrentActiveDisplayName())\(customInfo), 开机自启动: \(launchAtLogin), 代理: \(proxyInfo)\(authInfo)")
|
||||
#endif
|
||||
}
|
||||
@@ -193,9 +205,11 @@ class AppSettings: ObservableObject {
|
||||
|
||||
// 重置自定义币种设置
|
||||
useCustomSymbol = false
|
||||
customCryptoSymbol = nil
|
||||
customCryptoSymbols = []
|
||||
selectedCustomSymbolIndex = nil
|
||||
defaults.set(false, forKey: useCustomSymbolKey)
|
||||
defaults.removeObject(forKey: customSymbolKey)
|
||||
defaults.removeObject(forKey: customSymbolsKey)
|
||||
defaults.removeObject(forKey: selectedCustomSymbolIndexKey)
|
||||
|
||||
// 重置代理设置
|
||||
proxyEnabled = false
|
||||
@@ -236,11 +250,13 @@ class AppSettings: ObservableObject {
|
||||
// 如果当前正在使用自定义币种,只是切换使用状态,不删除数据
|
||||
if useCustomSymbol {
|
||||
useCustomSymbol = false
|
||||
selectedCustomSymbolIndex = nil
|
||||
defaults.set(false, forKey: useCustomSymbolKey)
|
||||
defaults.removeObject(forKey: selectedCustomSymbolIndexKey)
|
||||
|
||||
#if DEBUG
|
||||
if let customSymbol = customCryptoSymbol {
|
||||
print("🔧 [AppSettings] ✅ 已切换到默认币种: \(symbol.displayName),自定义币种 \(customSymbol.displayName) 保留")
|
||||
if !customCryptoSymbols.isEmpty {
|
||||
print("🔧 [AppSettings] ✅ 已切换到默认币种: \(symbol.displayName),\(customCryptoSymbols.count) 个自定义币种保留")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -407,43 +423,133 @@ class AppSettings: ObservableObject {
|
||||
|
||||
// MARK: - 自定义币种相关方法
|
||||
|
||||
/// 保存自定义币种设置
|
||||
/// - Parameter customSymbol: 要保存的自定义币种
|
||||
func saveCustomCryptoSymbol(_ customSymbol: CustomCryptoSymbol) {
|
||||
customCryptoSymbol = customSymbol
|
||||
useCustomSymbol = true
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(customSymbol)
|
||||
defaults.set(data, forKey: customSymbolKey)
|
||||
defaults.set(true, forKey: useCustomSymbolKey)
|
||||
|
||||
/// 添加自定义币种
|
||||
/// - Parameter customSymbol: 要添加的自定义币种
|
||||
/// - Returns: 是否添加成功
|
||||
@discardableResult
|
||||
func addCustomCryptoSymbol(_ customSymbol: CustomCryptoSymbol) -> Bool {
|
||||
// 检查是否已达到最大数量限制
|
||||
guard customCryptoSymbols.count < 5 else {
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ✅ 已保存自定义币种: \(customSymbol.displayName)")
|
||||
#endif
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ❌ 保存自定义币种失败: \(error.localizedDescription)")
|
||||
print("🔧 [AppSettings] ⚠️ 已达到最大自定义币种数量限制 (5个)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除自定义币种
|
||||
func removeCustomCryptoSymbol() {
|
||||
customCryptoSymbol = nil
|
||||
useCustomSymbol = false
|
||||
defaults.removeObject(forKey: customSymbolKey)
|
||||
defaults.set(false, forKey: useCustomSymbolKey)
|
||||
// 检查是否已存在相同的币种
|
||||
guard !customCryptoSymbols.contains(customSymbol) else {
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ⚠️ 自定义币种已存在: \(customSymbol.displayName)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
customCryptoSymbols.append(customSymbol)
|
||||
|
||||
// 如果这是第一个自定义币种,自动选中并启用自定义币种模式
|
||||
if customCryptoSymbols.count == 1 {
|
||||
selectedCustomSymbolIndex = 0
|
||||
useCustomSymbol = true
|
||||
defaults.set(true, forKey: useCustomSymbolKey)
|
||||
}
|
||||
|
||||
// 保存到 UserDefaults
|
||||
saveCustomCryptoSymbols()
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ✅ 已移除自定义币种")
|
||||
print("🔧 [AppSettings] ✅ 已添加自定义币种: \(customSymbol.displayName),当前总数: \(customCryptoSymbols.count)")
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
|
||||
/// 移除指定索引的自定义币种
|
||||
/// - Parameter index: 要移除的币种索引
|
||||
func removeCustomCryptoSymbol(at index: Int) {
|
||||
guard index >= 0 && index < customCryptoSymbols.count else {
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ⚠️ 无效的自定义币种索引: \(index)")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
let removedSymbol = customCryptoSymbols[index]
|
||||
customCryptoSymbols.remove(at: index)
|
||||
|
||||
// 如果移除的是当前选中的币种,需要调整选中状态
|
||||
if selectedCustomSymbolIndex == index {
|
||||
// 如果还有其他自定义币种,选中第一个;否则切换到系统默认币种
|
||||
if !customCryptoSymbols.isEmpty {
|
||||
selectedCustomSymbolIndex = 0
|
||||
} else {
|
||||
// 没有自定义币种了,切换到系统默认币种
|
||||
selectedCustomSymbolIndex = nil
|
||||
useCustomSymbol = false
|
||||
defaults.set(false, forKey: useCustomSymbolKey)
|
||||
}
|
||||
} else if let selectedIndex = selectedCustomSymbolIndex, selectedIndex > index {
|
||||
// 如果选中的币种在移除的币种之后,需要调整索引
|
||||
selectedCustomSymbolIndex = selectedIndex - 1
|
||||
}
|
||||
|
||||
// 保存到 UserDefaults
|
||||
if let selectedIndex = selectedCustomSymbolIndex {
|
||||
defaults.set(selectedIndex, forKey: selectedCustomSymbolIndexKey)
|
||||
} else {
|
||||
defaults.removeObject(forKey: selectedCustomSymbolIndexKey)
|
||||
}
|
||||
saveCustomCryptoSymbols()
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ✅ 已移除自定义币种: \(removedSymbol.displayName),剩余: \(customCryptoSymbols.count)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 选择指定的自定义币种
|
||||
/// - Parameter index: 要选中的币种索引
|
||||
func selectCustomCryptoSymbol(at index: Int) {
|
||||
guard index >= 0 && index < customCryptoSymbols.count else {
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ⚠️ 无效的自定义币种索引: \(index)")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
selectedCustomSymbolIndex = index
|
||||
useCustomSymbol = true
|
||||
defaults.set(index, forKey: selectedCustomSymbolIndexKey)
|
||||
defaults.set(true, forKey: useCustomSymbolKey)
|
||||
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ✅ 已选中自定义币种: \(customCryptoSymbols[index].displayName)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 获取当前选中的自定义币种
|
||||
/// - Returns: 当前选中的自定义币种,如果没有则返回nil
|
||||
func getCurrentSelectedCustomSymbol() -> CustomCryptoSymbol? {
|
||||
guard let index = selectedCustomSymbolIndex,
|
||||
index >= 0 && index < customCryptoSymbols.count else {
|
||||
return nil
|
||||
}
|
||||
return customCryptoSymbols[index]
|
||||
}
|
||||
|
||||
/// 保存自定义币种列表到 UserDefaults
|
||||
private func saveCustomCryptoSymbols() {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(customCryptoSymbols)
|
||||
defaults.set(data, forKey: customSymbolsKey)
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("🔧 [AppSettings] ❌ 保存自定义币种列表失败: \(error.localizedDescription)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前活跃的币种API符号
|
||||
/// - Returns: 当前活跃币种的API符号
|
||||
func getCurrentActiveApiSymbol() -> String {
|
||||
if useCustomSymbol, let customSymbol = customCryptoSymbol {
|
||||
if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
|
||||
return customSymbol.apiSymbol
|
||||
} else {
|
||||
return selectedSymbol.apiSymbol
|
||||
@@ -453,7 +559,7 @@ class AppSettings: ObservableObject {
|
||||
/// 获取当前活跃的币种显示名称
|
||||
/// - Returns: 当前活跃币种的显示名称
|
||||
func getCurrentActiveDisplayName() -> String {
|
||||
if useCustomSymbol, let customSymbol = customCryptoSymbol {
|
||||
if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
|
||||
return customSymbol.displayName
|
||||
} else {
|
||||
return selectedSymbol.displayName
|
||||
@@ -463,7 +569,7 @@ class AppSettings: ObservableObject {
|
||||
/// 获取当前活跃的币种图标
|
||||
/// - Returns: 当前活跃币种的图标名称
|
||||
func getCurrentActiveSystemImageName() -> String {
|
||||
if useCustomSymbol, let customSymbol = customCryptoSymbol {
|
||||
if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
|
||||
return customSymbol.systemImageName
|
||||
} else {
|
||||
return selectedSymbol.systemImageName
|
||||
@@ -473,7 +579,7 @@ class AppSettings: ObservableObject {
|
||||
/// 获取当前活跃的币种交易对显示名称
|
||||
/// - Returns: 当前活跃币种的交易对显示名称
|
||||
func getCurrentActivePairDisplayName() -> String {
|
||||
if useCustomSymbol, let customSymbol = customCryptoSymbol {
|
||||
if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
|
||||
return customSymbol.pairDisplayName
|
||||
} else {
|
||||
return selectedSymbol.pairDisplayName
|
||||
@@ -483,7 +589,7 @@ class AppSettings: ObservableObject {
|
||||
/// 判断是否正在使用自定义币种
|
||||
/// - Returns: 是否正在使用自定义币种
|
||||
func isUsingCustomSymbol() -> Bool {
|
||||
return useCustomSymbol && customCryptoSymbol != nil
|
||||
return useCustomSymbol && !customCryptoSymbols.isEmpty && selectedCustomSymbolIndex != nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class PriceManager: ObservableObject {
|
||||
@Published var selectedSymbol: CryptoSymbol
|
||||
|
||||
// 自定义币种相关属性
|
||||
@Published var customCryptoSymbol: CustomCryptoSymbol?
|
||||
@Published var customCryptoSymbols: [CustomCryptoSymbol] = []
|
||||
@Published var selectedCustomSymbolIndex: Int?
|
||||
@Published var useCustomSymbol: Bool = false
|
||||
|
||||
private let priceService: PriceService
|
||||
@@ -35,7 +36,8 @@ class PriceManager: ObservableObject {
|
||||
self.priceService = PriceService(appSettings: appSettings)
|
||||
|
||||
// 初始化自定义币种状态
|
||||
self.customCryptoSymbol = appSettings.customCryptoSymbol
|
||||
self.customCryptoSymbols = appSettings.customCryptoSymbols
|
||||
self.selectedCustomSymbolIndex = appSettings.selectedCustomSymbolIndex
|
||||
self.useCustomSymbol = appSettings.useCustomSymbol
|
||||
|
||||
startPriceUpdates()
|
||||
@@ -322,7 +324,8 @@ class PriceManager: ObservableObject {
|
||||
|
||||
/// 更新币种设置(当AppSettings中的自定义币种发生变化时调用)
|
||||
func updateCryptoSymbolSettings() {
|
||||
customCryptoSymbol = appSettings.customCryptoSymbol
|
||||
customCryptoSymbols = appSettings.customCryptoSymbols
|
||||
selectedCustomSymbolIndex = appSettings.selectedCustomSymbolIndex
|
||||
useCustomSymbol = appSettings.useCustomSymbol
|
||||
|
||||
// 重置价格状态,强制重新获取
|
||||
@@ -344,7 +347,8 @@ class PriceManager: ObservableObject {
|
||||
func getAllAvailableSymbols() -> [String] {
|
||||
var symbols = CryptoSymbol.allApiSymbols
|
||||
|
||||
if let customSymbol = appSettings.customCryptoSymbol {
|
||||
// 添加所有自定义币种
|
||||
for customSymbol in appSettings.customCryptoSymbols {
|
||||
symbols.append(customSymbol.apiSymbol)
|
||||
}
|
||||
|
||||
@@ -361,9 +365,10 @@ class PriceManager: ObservableObject {
|
||||
}
|
||||
|
||||
// 检查是否是自定义币种
|
||||
if let customSymbol = appSettings.customCryptoSymbol,
|
||||
customSymbol.apiSymbol == apiSymbol {
|
||||
return customSymbol.displayName
|
||||
for customSymbol in appSettings.customCryptoSymbols {
|
||||
if customSymbol.apiSymbol == apiSymbol {
|
||||
return customSymbol.displayName
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都找不到,返回API符号的基础部分(去掉USDT)
|
||||
|
||||
@@ -240,6 +240,54 @@ class PriceService: NSObject, ObservableObject, URLSessionTaskDelegate {
|
||||
return price
|
||||
}
|
||||
|
||||
/// 验证自定义币种是否在币安API中存在
|
||||
/// - Parameter symbol: 币种符号(如 "ADA")
|
||||
/// - Returns: 是否存在该币种
|
||||
func validateCustomSymbol(_ symbol: String) async -> Bool {
|
||||
let apiSymbol = "\(symbol)USDT"
|
||||
let urlString = "\(baseURL)?symbol=\(apiSymbol)"
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
#if DEBUG
|
||||
print("❌ [PriceService] 验证失败:无效的URL - \(urlString)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
print("🔍 [PriceService] 验证币种存在性: \(apiSymbol)")
|
||||
#endif
|
||||
|
||||
do {
|
||||
// 发送网络请求验证币种是否存在
|
||||
let (_, response) = try await session.data(from: url)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
#if DEBUG
|
||||
print("❌ [PriceService] 验证失败:无效响应 - \(symbol)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
let isValid = httpResponse.statusCode == 200
|
||||
|
||||
#if DEBUG
|
||||
if isValid {
|
||||
print("✅ [PriceService] 币种验证成功: \(symbol) 存在")
|
||||
} else {
|
||||
print("❌ [PriceService] 币种验证失败: \(symbol) 不存在 (HTTP \(httpResponse.statusCode))")
|
||||
}
|
||||
#endif
|
||||
|
||||
return isValid
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("❌ [PriceService] 币种验证网络错误: \(symbol) - \(error.localizedDescription)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 代理配置相关方法
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// CustomCryptoSymbol.swift
|
||||
// Bitcoin Monitoring
|
||||
//
|
||||
// Created by Claude on 2025/11/03.
|
||||
// Created by Mark on 2025/11/03.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@@ -72,6 +72,15 @@ struct PreferencesWindowView: View {
|
||||
@State private var isCustomSymbolValid: Bool = false
|
||||
@State private var customSymbolErrorMessage: String?
|
||||
@State private var showingCustomSymbolDeleteConfirmation: Bool = false
|
||||
@State private var pendingDeleteIndex: Int? = nil
|
||||
|
||||
// 验证相关状态
|
||||
@State private var isValidatingCustomSymbol: Bool = false
|
||||
@State private var showingValidationFailureAlert: Bool = false
|
||||
@State private var validationFailureMessage: String = ""
|
||||
|
||||
// PriceService 引用
|
||||
private let priceService: PriceService
|
||||
|
||||
// 导航状态 - 当前选中的标签页
|
||||
@State private var selectedTab: SettingsTab = .general
|
||||
@@ -81,6 +90,7 @@ struct PreferencesWindowView: View {
|
||||
|
||||
init(appSettings: AppSettings, onClose: @escaping () -> Void) {
|
||||
self.appSettings = appSettings
|
||||
self.priceService = PriceService(appSettings: appSettings)
|
||||
self.onClose = onClose
|
||||
|
||||
// 初始化临时状态
|
||||
@@ -114,6 +124,11 @@ struct PreferencesWindowView: View {
|
||||
} message: {
|
||||
deleteCustomSymbolMessage
|
||||
}
|
||||
.alert("币种验证失败", isPresented: $showingValidationFailureAlert) {
|
||||
Button("确定", role: .cancel) { }
|
||||
} message: {
|
||||
Text(validationFailureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// 主要内容视图
|
||||
@@ -396,56 +411,121 @@ struct PreferencesWindowView: View {
|
||||
private var customCryptoSettingsView: some View {
|
||||
SettingsGroupView(title: "自定义币种", icon: "plus.circle") {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if appSettings.isUsingCustomSymbol() {
|
||||
currentCustomSymbolView
|
||||
} else {
|
||||
addCustomSymbolView
|
||||
// 显示已添加的自定义币种列表
|
||||
if !appSettings.customCryptoSymbols.isEmpty {
|
||||
customSymbolsListView
|
||||
}
|
||||
|
||||
// 添加新币种的输入区域
|
||||
addCustomSymbolView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义币种列表视图
|
||||
private var customSymbolsListView: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("已添加的自定义币种 (\(appSettings.customCryptoSymbols.count)/5)")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
ForEach(0..<appSettings.customCryptoSymbols.count, id: \.self) { index in
|
||||
customSymbolRowView(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当前自定义币种视图
|
||||
private var currentCustomSymbolView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: appSettings.getCurrentActiveSystemImageName())
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 16))
|
||||
// 自定义币种行视图
|
||||
private func customSymbolRowView(at index: Int) -> some View {
|
||||
let customSymbol = appSettings.customCryptoSymbols[index]
|
||||
let isSelected = appSettings.isUsingCustomSymbol() && appSettings.selectedCustomSymbolIndex == index
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("当前自定义币种")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
return HStack {
|
||||
// 选中状态指示器
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(isSelected ? .blue : .secondary)
|
||||
|
||||
Text(appSettings.getCurrentActivePairDisplayName())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
// 币种图标
|
||||
Image(systemName: customSymbol.systemImageName)
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 16))
|
||||
|
||||
Spacer()
|
||||
// 币种信息
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(customSymbol.displayName)
|
||||
.font(.subheadline)
|
||||
.fontWeight(isSelected ? .medium : .regular)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Button("删除") {
|
||||
showingCustomSymbolDeleteConfirmation = true
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.foregroundColor(.red)
|
||||
Text(customSymbol.pairDisplayName)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// 删除按钮
|
||||
Button(action: {
|
||||
showingCustomSymbolDeleteConfirmation = true
|
||||
pendingDeleteIndex = index
|
||||
}) {
|
||||
Image(systemName: "trash")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 24, height: 24)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.red.opacity(0.1))
|
||||
)
|
||||
.onHover { isHovered in
|
||||
if isHovered {
|
||||
NSCursor.pointingHand.set()
|
||||
} else {
|
||||
NSCursor.arrow.set()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(isSelected ? Color.blue.opacity(0.1) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(isSelected ? Color.blue : Color(NSColor.separatorColor), lineWidth: 1)
|
||||
)
|
||||
.contentShape(RoundedRectangle(cornerRadius: 6))
|
||||
.onTapGesture {
|
||||
// 点击选中币种
|
||||
appSettings.selectCustomCryptoSymbol(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义币种视图
|
||||
private var addCustomSymbolView: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("添加自定义币种")
|
||||
Text(appSettings.customCryptoSymbols.isEmpty ? "添加自定义币种" : "添加更多自定义币种")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("输入3-5个大写字母的币种符号(如 ADA、DOGE、SHIB)")
|
||||
Text("输入3-5个大写字母的币种符号(如 ENA、TRX、TRUMP)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// 显示数量限制提示
|
||||
if appSettings.customCryptoSymbols.count >= 5 {
|
||||
Text("已达到最大限制(5个币种)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
|
||||
customSymbolInputView
|
||||
}
|
||||
}
|
||||
@@ -458,7 +538,7 @@ struct PreferencesWindowView: View {
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
TextField("例如: ADA", text: Binding(
|
||||
TextField("例如: TRX", text: Binding(
|
||||
get: { customSymbolInput },
|
||||
set: { newValue in
|
||||
let filteredValue = newValue.filter { $0.isLetter }.uppercased()
|
||||
@@ -471,13 +551,31 @@ struct PreferencesWindowView: View {
|
||||
))
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.frame(maxWidth: .infinity)
|
||||
.onSubmit {
|
||||
// 按回车键触发添加自定义币种
|
||||
Task {
|
||||
await addCustomSymbolWithValidation()
|
||||
}
|
||||
}
|
||||
|
||||
Button("添加") {
|
||||
addCustomSymbol()
|
||||
Button {
|
||||
Task {
|
||||
await addCustomSymbolWithValidation()
|
||||
}
|
||||
} label: {
|
||||
if isValidatingCustomSymbol {
|
||||
HStack(spacing: 4) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("验证中...")
|
||||
}
|
||||
} else {
|
||||
Text("添加")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
.disabled(!isCustomSymbolValid || isSaving)
|
||||
.disabled(!isCustomSymbolValid || isSaving || isValidatingCustomSymbol || appSettings.customCryptoSymbols.count >= 5)
|
||||
}
|
||||
|
||||
if !isCustomSymbolValid && !customSymbolInput.isEmpty {
|
||||
@@ -536,7 +634,9 @@ struct PreferencesWindowView: View {
|
||||
|
||||
// 删除自定义币种确认消息
|
||||
private var deleteCustomSymbolMessage: Text {
|
||||
if let customSymbol = appSettings.customCryptoSymbol {
|
||||
if let index = pendingDeleteIndex,
|
||||
index >= 0 && index < appSettings.customCryptoSymbols.count {
|
||||
let customSymbol = appSettings.customCryptoSymbols[index]
|
||||
return Text("确定要删除自定义币种 \"\(customSymbol.displayName)\" 吗?删除后将无法恢复。")
|
||||
} else {
|
||||
return Text("确定要删除自定义币种吗?删除后将无法恢复。")
|
||||
@@ -667,7 +767,62 @@ struct PreferencesWindowView: View {
|
||||
// MARK: - 自定义币种相关方法
|
||||
|
||||
/**
|
||||
* 添加自定义币种
|
||||
* 添加自定义币种(带币安API验证)
|
||||
*/
|
||||
private func addCustomSymbolWithValidation() async {
|
||||
guard isCustomSymbolValid, !customSymbolInput.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let customSymbol = try CustomCryptoSymbol(symbol: customSymbolInput)
|
||||
|
||||
// 开始验证
|
||||
isValidatingCustomSymbol = true
|
||||
|
||||
// 验证币种是否在币安API中存在
|
||||
let isValid = await priceService.validateCustomSymbol(customSymbol.symbol)
|
||||
|
||||
await MainActor.run {
|
||||
isValidatingCustomSymbol = false
|
||||
|
||||
if isValid {
|
||||
// 验证通过,添加币种
|
||||
let success = appSettings.addCustomCryptoSymbol(customSymbol)
|
||||
|
||||
if success {
|
||||
// 清空输入状态
|
||||
customSymbolInput = ""
|
||||
isCustomSymbolValid = false
|
||||
customSymbolErrorMessage = nil
|
||||
|
||||
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)")
|
||||
} else {
|
||||
// 添加失败(可能是因为数量限制或重复)
|
||||
customSymbolErrorMessage = "无法添加该币种(可能已达到最大限制或币种重复)"
|
||||
isCustomSymbolValid = false
|
||||
}
|
||||
} else {
|
||||
// 验证失败,显示错误提示
|
||||
validationFailureMessage = "币种 \"\(customSymbol.symbol)\" 在币安交易所中不存在,请检查币种代码是否正确"
|
||||
showingValidationFailureAlert = true
|
||||
isCustomSymbolValid = false
|
||||
customSymbolErrorMessage = "币种不存在或无法获取价格"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isValidatingCustomSymbol = false
|
||||
// 格式验证失败(这种情况理论上不会发生,因为我们在onChange中已经验证了)
|
||||
print("❌ [Preferences] 添加自定义币种失败: \(error.localizedDescription)")
|
||||
customSymbolErrorMessage = "添加失败:\(error.localizedDescription)"
|
||||
isCustomSymbolValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义币种(原方法,保留作为备用)
|
||||
*/
|
||||
private func addCustomSymbol() {
|
||||
guard isCustomSymbolValid, !customSymbolInput.isEmpty else {
|
||||
@@ -676,17 +831,27 @@ struct PreferencesWindowView: View {
|
||||
|
||||
do {
|
||||
let customSymbol = try CustomCryptoSymbol(symbol: customSymbolInput)
|
||||
appSettings.saveCustomCryptoSymbol(customSymbol)
|
||||
|
||||
// 清空输入状态
|
||||
customSymbolInput = ""
|
||||
isCustomSymbolValid = false
|
||||
customSymbolErrorMessage = nil
|
||||
// 使用新的添加方法
|
||||
let success = appSettings.addCustomCryptoSymbol(customSymbol)
|
||||
|
||||
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)")
|
||||
if success {
|
||||
// 清空输入状态
|
||||
customSymbolInput = ""
|
||||
isCustomSymbolValid = false
|
||||
customSymbolErrorMessage = nil
|
||||
|
||||
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)")
|
||||
} else {
|
||||
// 添加失败(可能是因为数量限制或重复)
|
||||
customSymbolErrorMessage = "无法添加该币种(可能已达到最大限制或币种重复)"
|
||||
isCustomSymbolValid = false
|
||||
}
|
||||
} catch {
|
||||
// 这种情况理论上不会发生,因为我们在onChange中已经验证了
|
||||
print("❌ [Preferences] 添加自定义币种失败: \(error.localizedDescription)")
|
||||
customSymbolErrorMessage = "添加失败:\(error.localizedDescription)"
|
||||
isCustomSymbolValid = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +859,13 @@ struct PreferencesWindowView: View {
|
||||
* 删除自定义币种
|
||||
*/
|
||||
private func deleteCustomSymbol() {
|
||||
appSettings.removeCustomCryptoSymbol()
|
||||
guard let index = pendingDeleteIndex else {
|
||||
print("❌ [Preferences] 删除失败:无效的索引")
|
||||
return
|
||||
}
|
||||
|
||||
appSettings.removeCustomCryptoSymbol(at: index)
|
||||
pendingDeleteIndex = nil
|
||||
print("✅ [Preferences] 已删除自定义币种")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user