From fd0170ac37b64cecba4bf836a373882079c9785a Mon Sep 17 00:00:00 2001 From: ZhangLei Date: Mon, 3 Nov 2025 23:08:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=B8=81=E7=A7=8D=E7=AE=A1=E7=90=86=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E4=B8=AA=E5=B8=81=E7=A7=8D=E5=8F=8A=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bitcoin Monitoring.xcodeproj/project.pbxproj | 4 +- Bitcoin-Monitoring/Core/MenuBarManager.swift | 48 ++-- Bitcoin-Monitoring/Managers/AppSettings.swift | 192 ++++++++++--- .../Managers/PriceManager.swift | 19 +- .../Managers/PriceService.swift | 48 ++++ .../Models/CustomCryptoSymbol.swift | 2 +- .../Views/PreferencesWindowView.swift | 253 +++++++++++++++--- 7 files changed, 450 insertions(+), 116 deletions(-) diff --git a/Bitcoin Monitoring.xcodeproj/project.pbxproj b/Bitcoin Monitoring.xcodeproj/project.pbxproj index 112b608..b524968 100644 --- a/Bitcoin Monitoring.xcodeproj/project.pbxproj +++ b/Bitcoin Monitoring.xcodeproj/project.pbxproj @@ -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; diff --git a/Bitcoin-Monitoring/Core/MenuBarManager.swift b/Bitcoin-Monitoring/Core/MenuBarManager.swift index ba6696e..3148d5a 100644 --- a/Bitcoin-Monitoring/Core/MenuBarManager.swift +++ b/Bitcoin-Monitoring/Core/MenuBarManager.swift @@ -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() diff --git a/Bitcoin-Monitoring/Managers/AppSettings.swift b/Bitcoin-Monitoring/Managers/AppSettings.swift index 7d5c7ab..8df3bf6 100644 --- a/Bitcoin-Monitoring/Managers/AppSettings.swift +++ b/Bitcoin-Monitoring/Managers/AppSettings.swift @@ -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 } } diff --git a/Bitcoin-Monitoring/Managers/PriceManager.swift b/Bitcoin-Monitoring/Managers/PriceManager.swift index f2d6362..0cc36b4 100644 --- a/Bitcoin-Monitoring/Managers/PriceManager.swift +++ b/Bitcoin-Monitoring/Managers/PriceManager.swift @@ -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) diff --git a/Bitcoin-Monitoring/Managers/PriceService.swift b/Bitcoin-Monitoring/Managers/PriceService.swift index 413f633..99123f5 100644 --- a/Bitcoin-Monitoring/Managers/PriceService.swift +++ b/Bitcoin-Monitoring/Managers/PriceService.swift @@ -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: - 代理配置相关方法 /** diff --git a/Bitcoin-Monitoring/Models/CustomCryptoSymbol.swift b/Bitcoin-Monitoring/Models/CustomCryptoSymbol.swift index 2775cd9..498c141 100644 --- a/Bitcoin-Monitoring/Models/CustomCryptoSymbol.swift +++ b/Bitcoin-Monitoring/Models/CustomCryptoSymbol.swift @@ -2,7 +2,7 @@ // CustomCryptoSymbol.swift // Bitcoin Monitoring // -// Created by Claude on 2025/11/03. +// Created by Mark on 2025/11/03. // import Foundation diff --git a/Bitcoin-Monitoring/Views/PreferencesWindowView.swift b/Bitcoin-Monitoring/Views/PreferencesWindowView.swift index 65de9ee..5eb3414 100644 --- a/Bitcoin-Monitoring/Views/PreferencesWindowView.swift +++ b/Bitcoin-Monitoring/Views/PreferencesWindowView.swift @@ -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.. 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] 已删除自定义币种") } }