feat: 更新自定义币种管理,支持多个币种及验证功能

This commit is contained in:
ZhangLei
2025-11-03 23:08:48 +08:00
parent 91127faebe
commit fd0170ac37
7 changed files with 450 additions and 116 deletions

View File

@@ -273,7 +273,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.5; MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 1.1.1; MARKETING_VERSION = 1.2.0;
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 +306,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.5; MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 1.1.1; MARKETING_VERSION = 1.2.0;
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;

View File

@@ -59,7 +59,7 @@ class MenuBarManager: NSObject, ObservableObject {
.store(in: &cancellables) .store(in: &cancellables)
// //
appSettings.$customCryptoSymbol appSettings.$customCryptoSymbols
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.priceManager.updateCryptoSymbolSettings() self.priceManager.updateCryptoSymbolSettings()
@@ -245,8 +245,8 @@ class MenuBarManager: NSObject, ObservableObject {
} }
// - // -
var customSymbolMenuItem: NSMenuItem? var customSymbolMenuItems: [NSMenuItem] = []
if let customSymbol = appSettings.customCryptoSymbol { for customSymbol in appSettings.customCryptoSymbols {
let isCurrent = customSymbol.isCurrentSymbol(currentApiSymbol) let isCurrent = customSymbol.isCurrentSymbol(currentApiSymbol)
let placeholderTitle = isCurrent ? "\(customSymbol.displayName) (自定义): 加载中..." : " \(customSymbol.displayName) (自定义): 加载中..." let placeholderTitle = isCurrent ? "\(customSymbol.displayName) (自定义): 加载中..." : " \(customSymbol.displayName) (自定义): 加载中..."
let item = NSMenuItem(title: placeholderTitle, action: #selector(self.selectOrCopySymbol(_:)), keyEquivalent: "") let item = NSMenuItem(title: placeholderTitle, action: #selector(self.selectOrCopySymbol(_:)), keyEquivalent: "")
@@ -258,7 +258,7 @@ class MenuBarManager: NSObject, ObservableObject {
item.isEnabled = true item.isEnabled = true
item.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true] item.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true]
menu.addItem(item) menu.addItem(item)
customSymbolMenuItem = item customSymbolMenuItems.append(item)
} }
// //
@@ -294,22 +294,24 @@ class MenuBarManager: NSObject, ObservableObject {
} }
// //
if let customSymbol = self.appSettings.customCryptoSymbol, for (index, customSymbol) in self.appSettings.customCryptoSymbols.enumerated() {
let menuItem = customSymbolMenuItem { if index < customSymbolMenuItems.count {
let isCurrent = customSymbol.isCurrentSymbol(currentSymbolAfter) let menuItem = customSymbolMenuItems[index]
let isCurrent = customSymbol.isCurrentSymbol(currentSymbolAfter)
if let price = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol) { if let price = await self.priceManager.fetchCustomSymbolPrice(forApiSymbol: customSymbol.apiSymbol) {
let title = isCurrent ? "\(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))" : " \(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))" let title = isCurrent ? "\(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))" : " \(customSymbol.displayName) (自定义): $\(self.formatPriceWithCommas(price))"
menuItem.title = title menuItem.title = title
menuItem.isEnabled = true menuItem.isEnabled = true
menuItem.target = self menuItem.target = self
menuItem.representedObject = ["customSymbol": customSymbol, "price": price, "isCustom": true] menuItem.representedObject = ["customSymbol": customSymbol, "price": price, "isCustom": true]
} else { } else {
let title = isCurrent ? "\(customSymbol.displayName) (自定义): 错误" : " \(customSymbol.displayName) (自定义): 错误" let title = isCurrent ? "\(customSymbol.displayName) (自定义): 错误" : " \(customSymbol.displayName) (自定义): 错误"
menuItem.title = title menuItem.title = title
menuItem.isEnabled = false menuItem.isEnabled = false
menuItem.target = self menuItem.target = self
menuItem.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true] menuItem.representedObject = ["customSymbol": customSymbol, "price": 0.0, "isCustom": true]
}
} }
} }
} }
@@ -500,9 +502,11 @@ class MenuBarManager: NSObject, ObservableObject {
} else { } else {
// //
if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol { if isCustom, let customSymbol = data["customSymbol"] as? CustomCryptoSymbol {
// // -
appSettings.saveCustomCryptoSymbol(customSymbol) if let index = appSettings.customCryptoSymbols.firstIndex(of: customSymbol) {
print("✅ 已切换到自定义币种: \(customSymbol.displayName)") appSettings.selectCustomCryptoSymbol(at: index)
print("✅ 已切换到自定义币种: \(customSymbol.displayName)")
}
// //
self.priceManager.updateCryptoSymbolSettings() self.priceManager.updateCryptoSymbolSettings()

View File

@@ -25,8 +25,10 @@ class AppSettings: ObservableObject {
// MARK: - // MARK: -
/// /// 5
@Published var customCryptoSymbol: CustomCryptoSymbol? @Published var customCryptoSymbols: [CustomCryptoSymbol] = []
/// 使
@Published var selectedCustomSymbolIndex: Int?
/// 使 /// 使
@Published var useCustomSymbol: Bool = false @Published var useCustomSymbol: Bool = false
@@ -52,7 +54,8 @@ class AppSettings: ObservableObject {
// MARK: - // MARK: -
private let customSymbolKey = "CustomCryptoSymbol" private let customSymbolsKey = "CustomCryptoSymbols"
private let selectedCustomSymbolIndexKey = "SelectedCustomSymbolIndex"
private let useCustomSymbolKey = "UseCustomSymbol" private let useCustomSymbolKey = "UseCustomSymbol"
// MARK: - // MARK: -
@@ -141,17 +144,26 @@ class AppSettings: ObservableObject {
// //
launchAtLogin = defaults.bool(forKey: launchAtLoginKey) launchAtLogin = defaults.bool(forKey: launchAtLoginKey)
// - //
if let customSymbolData = defaults.data(forKey: customSymbolKey), if let customSymbolsData = defaults.data(forKey: customSymbolsKey),
let customSymbol = try? JSONDecoder().decode(CustomCryptoSymbol.self, from: customSymbolData) { let customSymbols = try? JSONDecoder().decode([CustomCryptoSymbol].self, from: customSymbolsData) {
customCryptoSymbol = customSymbol customCryptoSymbols = customSymbols
//
let savedIndex = defaults.integer(forKey: selectedCustomSymbolIndexKey)
if savedIndex >= 0 && savedIndex < customSymbols.count {
selectedCustomSymbolIndex = savedIndex
}
// 使 // 使
useCustomSymbol = defaults.bool(forKey: useCustomSymbolKey) useCustomSymbol = defaults.bool(forKey: useCustomSymbolKey)
#if DEBUG #if DEBUG
print("🔧 [AppSettings] ✅ 已加载自定义币种: \(customSymbol.displayName),使用状态: \(useCustomSymbol)") print("🔧 [AppSettings] ✅ 已加载 \(customSymbols.count) 个自定义币种,使用状态: \(useCustomSymbol)")
if let index = selectedCustomSymbolIndex {
print("🔧 [AppSettings] 当前选中自定义币种: \(customSymbols[index].displayName)")
}
#endif #endif
} else { } else {
customCryptoSymbol = nil customCryptoSymbols = []
selectedCustomSymbolIndex = nil
useCustomSymbol = false useCustomSymbol = false
#if DEBUG #if DEBUG
print("🔧 [AppSettings] 未找到自定义币种数据") print("🔧 [AppSettings] 未找到自定义币种数据")
@@ -172,7 +184,7 @@ class AppSettings: ObservableObject {
#if DEBUG #if DEBUG
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 && customCryptoSymbol != nil ? " (自定义: \(customCryptoSymbol!.displayName))" : "" 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)")
#endif #endif
} }
@@ -193,9 +205,11 @@ class AppSettings: ObservableObject {
// //
useCustomSymbol = false useCustomSymbol = false
customCryptoSymbol = nil customCryptoSymbols = []
selectedCustomSymbolIndex = nil
defaults.set(false, forKey: useCustomSymbolKey) defaults.set(false, forKey: useCustomSymbolKey)
defaults.removeObject(forKey: customSymbolKey) defaults.removeObject(forKey: customSymbolsKey)
defaults.removeObject(forKey: selectedCustomSymbolIndexKey)
// //
proxyEnabled = false proxyEnabled = false
@@ -236,11 +250,13 @@ class AppSettings: ObservableObject {
// 使使 // 使使
if useCustomSymbol { if useCustomSymbol {
useCustomSymbol = false useCustomSymbol = false
selectedCustomSymbolIndex = nil
defaults.set(false, forKey: useCustomSymbolKey) defaults.set(false, forKey: useCustomSymbolKey)
defaults.removeObject(forKey: selectedCustomSymbolIndexKey)
#if DEBUG #if DEBUG
if let customSymbol = customCryptoSymbol { if !customCryptoSymbols.isEmpty {
print("🔧 [AppSettings] ✅ 已切换到默认币种: \(symbol.displayName)自定义币种 \(customSymbol.displayName) 保留") print("🔧 [AppSettings] ✅ 已切换到默认币种: \(symbol.displayName)\(customCryptoSymbols.count) 个自定义币种保留")
} }
#endif #endif
} }
@@ -407,43 +423,133 @@ class AppSettings: ObservableObject {
// MARK: - // MARK: -
/// ///
/// - Parameter customSymbol: /// - Parameter customSymbol:
func saveCustomCryptoSymbol(_ customSymbol: CustomCryptoSymbol) { /// - Returns:
customCryptoSymbol = customSymbol @discardableResult
useCustomSymbol = true func addCustomCryptoSymbol(_ customSymbol: CustomCryptoSymbol) -> Bool {
//
do { guard customCryptoSymbols.count < 5 else {
let data = try JSONEncoder().encode(customSymbol)
defaults.set(data, forKey: customSymbolKey)
defaults.set(true, forKey: useCustomSymbolKey)
#if DEBUG #if DEBUG
print("🔧 [AppSettings] ✅ 已保存自定义币种: \(customSymbol.displayName)") print("🔧 [AppSettings] ⚠️ 已达到最大自定义币种数量限制 (5个)")
#endif
} catch {
#if DEBUG
print("🔧 [AppSettings] ❌ 保存自定义币种失败: \(error.localizedDescription)")
#endif #endif
return false
} }
}
/// //
func removeCustomCryptoSymbol() { guard !customCryptoSymbols.contains(customSymbol) else {
customCryptoSymbol = nil #if DEBUG
useCustomSymbol = false print("🔧 [AppSettings] ⚠️ 自定义币种已存在: \(customSymbol.displayName)")
defaults.removeObject(forKey: customSymbolKey) #endif
defaults.set(false, forKey: useCustomSymbolKey) return false
}
customCryptoSymbols.append(customSymbol)
//
if customCryptoSymbols.count == 1 {
selectedCustomSymbolIndex = 0
useCustomSymbol = true
defaults.set(true, forKey: useCustomSymbolKey)
}
// UserDefaults
saveCustomCryptoSymbols()
#if DEBUG #if DEBUG
print("🔧 [AppSettings] ✅ 已移除自定义币种") print("🔧 [AppSettings] ✅ 已添加自定义币种: \(customSymbol.displayName),当前总数: \(customCryptoSymbols.count)")
#endif #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 /// API
/// - Returns: API /// - Returns: API
func getCurrentActiveApiSymbol() -> String { func getCurrentActiveApiSymbol() -> String {
if useCustomSymbol, let customSymbol = customCryptoSymbol { if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
return customSymbol.apiSymbol return customSymbol.apiSymbol
} else { } else {
return selectedSymbol.apiSymbol return selectedSymbol.apiSymbol
@@ -453,7 +559,7 @@ class AppSettings: ObservableObject {
/// ///
/// - Returns: /// - Returns:
func getCurrentActiveDisplayName() -> String { func getCurrentActiveDisplayName() -> String {
if useCustomSymbol, let customSymbol = customCryptoSymbol { if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
return customSymbol.displayName return customSymbol.displayName
} else { } else {
return selectedSymbol.displayName return selectedSymbol.displayName
@@ -463,7 +569,7 @@ class AppSettings: ObservableObject {
/// ///
/// - Returns: /// - Returns:
func getCurrentActiveSystemImageName() -> String { func getCurrentActiveSystemImageName() -> String {
if useCustomSymbol, let customSymbol = customCryptoSymbol { if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
return customSymbol.systemImageName return customSymbol.systemImageName
} else { } else {
return selectedSymbol.systemImageName return selectedSymbol.systemImageName
@@ -473,7 +579,7 @@ class AppSettings: ObservableObject {
/// ///
/// - Returns: /// - Returns:
func getCurrentActivePairDisplayName() -> String { func getCurrentActivePairDisplayName() -> String {
if useCustomSymbol, let customSymbol = customCryptoSymbol { if useCustomSymbol, let customSymbol = getCurrentSelectedCustomSymbol() {
return customSymbol.pairDisplayName return customSymbol.pairDisplayName
} else { } else {
return selectedSymbol.pairDisplayName return selectedSymbol.pairDisplayName
@@ -483,7 +589,7 @@ class AppSettings: ObservableObject {
/// 使 /// 使
/// - Returns: 使 /// - Returns: 使
func isUsingCustomSymbol() -> Bool { func isUsingCustomSymbol() -> Bool {
return useCustomSymbol && customCryptoSymbol != nil return useCustomSymbol && !customCryptoSymbols.isEmpty && selectedCustomSymbolIndex != nil
} }
} }

View File

@@ -17,7 +17,8 @@ class PriceManager: ObservableObject {
@Published var selectedSymbol: CryptoSymbol @Published var selectedSymbol: CryptoSymbol
// //
@Published var customCryptoSymbol: CustomCryptoSymbol? @Published var customCryptoSymbols: [CustomCryptoSymbol] = []
@Published var selectedCustomSymbolIndex: Int?
@Published var useCustomSymbol: Bool = false @Published var useCustomSymbol: Bool = false
private let priceService: PriceService private let priceService: PriceService
@@ -35,7 +36,8 @@ class PriceManager: ObservableObject {
self.priceService = PriceService(appSettings: appSettings) self.priceService = PriceService(appSettings: appSettings)
// //
self.customCryptoSymbol = appSettings.customCryptoSymbol self.customCryptoSymbols = appSettings.customCryptoSymbols
self.selectedCustomSymbolIndex = appSettings.selectedCustomSymbolIndex
self.useCustomSymbol = appSettings.useCustomSymbol self.useCustomSymbol = appSettings.useCustomSymbol
startPriceUpdates() startPriceUpdates()
@@ -322,7 +324,8 @@ class PriceManager: ObservableObject {
/// AppSettings /// AppSettings
func updateCryptoSymbolSettings() { func updateCryptoSymbolSettings() {
customCryptoSymbol = appSettings.customCryptoSymbol customCryptoSymbols = appSettings.customCryptoSymbols
selectedCustomSymbolIndex = appSettings.selectedCustomSymbolIndex
useCustomSymbol = appSettings.useCustomSymbol useCustomSymbol = appSettings.useCustomSymbol
// //
@@ -344,7 +347,8 @@ class PriceManager: ObservableObject {
func getAllAvailableSymbols() -> [String] { func getAllAvailableSymbols() -> [String] {
var symbols = CryptoSymbol.allApiSymbols var symbols = CryptoSymbol.allApiSymbols
if let customSymbol = appSettings.customCryptoSymbol { //
for customSymbol in appSettings.customCryptoSymbols {
symbols.append(customSymbol.apiSymbol) symbols.append(customSymbol.apiSymbol)
} }
@@ -361,9 +365,10 @@ class PriceManager: ObservableObject {
} }
// //
if let customSymbol = appSettings.customCryptoSymbol, for customSymbol in appSettings.customCryptoSymbols {
customSymbol.apiSymbol == apiSymbol { if customSymbol.apiSymbol == apiSymbol {
return customSymbol.displayName return customSymbol.displayName
}
} }
// APIUSDT // APIUSDT

View File

@@ -240,6 +240,54 @@ class PriceService: NSObject, ObservableObject, URLSessionTaskDelegate {
return price 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: - // MARK: -
/** /**

View File

@@ -2,7 +2,7 @@
// CustomCryptoSymbol.swift // CustomCryptoSymbol.swift
// Bitcoin Monitoring // Bitcoin Monitoring
// //
// Created by Claude on 2025/11/03. // Created by Mark on 2025/11/03.
// //
import Foundation import Foundation

View File

@@ -72,6 +72,15 @@ struct PreferencesWindowView: View {
@State private var isCustomSymbolValid: Bool = false @State private var isCustomSymbolValid: Bool = false
@State private var customSymbolErrorMessage: String? @State private var customSymbolErrorMessage: String?
@State private var showingCustomSymbolDeleteConfirmation: Bool = false @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 @State private var selectedTab: SettingsTab = .general
@@ -81,6 +90,7 @@ struct PreferencesWindowView: View {
init(appSettings: AppSettings, onClose: @escaping () -> Void) { init(appSettings: AppSettings, onClose: @escaping () -> Void) {
self.appSettings = appSettings self.appSettings = appSettings
self.priceService = PriceService(appSettings: appSettings)
self.onClose = onClose self.onClose = onClose
// //
@@ -114,6 +124,11 @@ struct PreferencesWindowView: View {
} message: { } message: {
deleteCustomSymbolMessage deleteCustomSymbolMessage
} }
.alert("币种验证失败", isPresented: $showingValidationFailureAlert) {
Button("确定", role: .cancel) { }
} message: {
Text(validationFailureMessage)
}
} }
// //
@@ -396,56 +411,121 @@ struct PreferencesWindowView: View {
private var customCryptoSettingsView: some View { private var customCryptoSettingsView: some View {
SettingsGroupView(title: "自定义币种", icon: "plus.circle") { SettingsGroupView(title: "自定义币种", icon: "plus.circle") {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
if appSettings.isUsingCustomSymbol() { //
currentCustomSymbolView if !appSettings.customCryptoSymbols.isEmpty {
} else { customSymbolsListView
addCustomSymbolView }
//
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 { private func customSymbolRowView(at index: Int) -> some View {
VStack(alignment: .leading, spacing: 8) { let customSymbol = appSettings.customCryptoSymbols[index]
HStack { let isSelected = appSettings.isUsingCustomSymbol() && appSettings.selectedCustomSymbolIndex == index
Image(systemName: appSettings.getCurrentActiveSystemImageName())
.foregroundColor(.orange)
.font(.system(size: 16))
VStack(alignment: .leading, spacing: 2) { return HStack {
Text("当前自定义币种") //
.font(.subheadline) Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.fontWeight(.medium) .font(.system(size: 14))
.foregroundColor(isSelected ? .blue : .secondary)
Text(appSettings.getCurrentActivePairDisplayName()) //
.font(.caption) Image(systemName: customSymbol.systemImageName)
.foregroundColor(.secondary) .foregroundColor(.orange)
} .font(.system(size: 16))
Spacer() //
VStack(alignment: .leading, spacing: 2) {
Text(customSymbol.displayName)
.font(.subheadline)
.fontWeight(isSelected ? .medium : .regular)
.foregroundColor(.primary)
Button("删除") { Text(customSymbol.pairDisplayName)
showingCustomSymbolDeleteConfirmation = true .font(.caption)
} .foregroundColor(.secondary)
.buttonStyle(.bordered)
.controlSize(.small)
.foregroundColor(.red)
} }
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 { private var addCustomSymbolView: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text("添加自定义币种") Text(appSettings.customCryptoSymbols.isEmpty ? "添加自定义币种" : "添加更多自定义币种")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("输入3-5个大写字母的币种符号ADA、DOGE、SHIB") Text("输入3-5个大写字母的币种符号ENA、TRX、TRUMP")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
//
if appSettings.customCryptoSymbols.count >= 5 {
Text("已达到最大限制5个币种")
.font(.caption)
.foregroundColor(.orange)
}
customSymbolInputView customSymbolInputView
} }
} }
@@ -458,7 +538,7 @@ struct PreferencesWindowView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
HStack(spacing: 12) { HStack(spacing: 12) {
TextField("例如: ADA", text: Binding( TextField("例如: TRX", text: Binding(
get: { customSymbolInput }, get: { customSymbolInput },
set: { newValue in set: { newValue in
let filteredValue = newValue.filter { $0.isLetter }.uppercased() let filteredValue = newValue.filter { $0.isLetter }.uppercased()
@@ -471,13 +551,31 @@ struct PreferencesWindowView: View {
)) ))
.textFieldStyle(RoundedBorderTextFieldStyle()) .textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onSubmit {
//
Task {
await addCustomSymbolWithValidation()
}
}
Button("添加") { Button {
addCustomSymbol() Task {
await addCustomSymbolWithValidation()
}
} label: {
if isValidatingCustomSymbol {
HStack(spacing: 4) {
ProgressView()
.controlSize(.small)
Text("验证中...")
}
} else {
Text("添加")
}
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.controlSize(.small) .controlSize(.small)
.disabled(!isCustomSymbolValid || isSaving) .disabled(!isCustomSymbolValid || isSaving || isValidatingCustomSymbol || appSettings.customCryptoSymbols.count >= 5)
} }
if !isCustomSymbolValid && !customSymbolInput.isEmpty { if !isCustomSymbolValid && !customSymbolInput.isEmpty {
@@ -536,7 +634,9 @@ struct PreferencesWindowView: View {
// //
private var deleteCustomSymbolMessage: Text { 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)\" 吗?删除后将无法恢复。") return Text("确定要删除自定义币种 \"\(customSymbol.displayName)\" 吗?删除后将无法恢复。")
} else { } else {
return Text("确定要删除自定义币种吗?删除后将无法恢复。") return Text("确定要删除自定义币种吗?删除后将无法恢复。")
@@ -667,7 +767,62 @@ struct PreferencesWindowView: View {
// MARK: - // 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() { private func addCustomSymbol() {
guard isCustomSymbolValid, !customSymbolInput.isEmpty else { guard isCustomSymbolValid, !customSymbolInput.isEmpty else {
@@ -676,17 +831,27 @@ struct PreferencesWindowView: View {
do { do {
let customSymbol = try CustomCryptoSymbol(symbol: customSymbolInput) let customSymbol = try CustomCryptoSymbol(symbol: customSymbolInput)
appSettings.saveCustomCryptoSymbol(customSymbol)
// // 使
customSymbolInput = "" let success = appSettings.addCustomCryptoSymbol(customSymbol)
isCustomSymbolValid = false
customSymbolErrorMessage = nil
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)") if success {
//
customSymbolInput = ""
isCustomSymbolValid = false
customSymbolErrorMessage = nil
print("✅ [Preferences] 已添加自定义币种: \(customSymbol.displayName)")
} else {
//
customSymbolErrorMessage = "无法添加该币种(可能已达到最大限制或币种重复)"
isCustomSymbolValid = false
}
} catch { } catch {
// onChange // onChange
print("❌ [Preferences] 添加自定义币种失败: \(error.localizedDescription)") print("❌ [Preferences] 添加自定义币种失败: \(error.localizedDescription)")
customSymbolErrorMessage = "添加失败:\(error.localizedDescription)"
isCustomSymbolValid = false
} }
} }
@@ -694,7 +859,13 @@ struct PreferencesWindowView: View {
* *
*/ */
private func deleteCustomSymbol() { private func deleteCustomSymbol() {
appSettings.removeCustomCryptoSymbol() guard let index = pendingDeleteIndex else {
print("❌ [Preferences] 删除失败:无效的索引")
return
}
appSettings.removeCustomCryptoSymbol(at: index)
pendingDeleteIndex = nil
print("✅ [Preferences] 已删除自定义币种") print("✅ [Preferences] 已删除自定义币种")
} }
} }