diff --git a/StockMonitor.xcodeproj/project.pbxproj b/StockMonitor.xcodeproj/project.pbxproj index a76ec34..bf0cfd5 100644 --- a/StockMonitor.xcodeproj/project.pbxproj +++ b/StockMonitor.xcodeproj/project.pbxproj @@ -339,7 +339,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.vgearen.Stockbar; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -373,7 +373,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.vgearen.Stockbar; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -393,7 +393,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.vgearen.StockbarTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -412,7 +412,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.vgearen.StockbarTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; diff --git a/StockMonitor/Models/AppSettings.swift b/StockMonitor/Models/AppSettings.swift index 0d8e133..9ab8eac 100644 --- a/StockMonitor/Models/AppSettings.swift +++ b/StockMonitor/Models/AppSettings.swift @@ -65,6 +65,7 @@ struct AppSettings { var displayCurrency: DisplayCurrency = .cny var sortRule: SortRule = .changeDesc var usPriceMode: USPriceMode = .sessionPrice + var groupHoldings: Bool = false static let validRefreshIntervals = [3, 5, 10, 30] @@ -75,7 +76,7 @@ struct AppSettings { // MARK: - Codable(容错:缺失字段使用默认值) extension AppSettings: Codable { enum CodingKeys: String, CodingKey { - case statusBarStockId, refreshInterval, colorScheme, displayCurrency, sortRule, usPriceMode + case statusBarStockId, refreshInterval, colorScheme, displayCurrency, sortRule, usPriceMode, groupHoldings } init(from decoder: Decoder) throws { @@ -86,5 +87,6 @@ extension AppSettings: Codable { displayCurrency = (try? c.decodeIfPresent(DisplayCurrency.self, forKey: .displayCurrency)) ?? .cny sortRule = (try? c.decodeIfPresent(SortRule.self, forKey: .sortRule)) ?? .changeDesc usPriceMode = (try? c.decodeIfPresent(USPriceMode.self, forKey: .usPriceMode)) ?? .sessionPrice + groupHoldings = (try? c.decodeIfPresent(Bool.self, forKey: .groupHoldings)) ?? false } } diff --git a/StockMonitor/Views/DropdownView.swift b/StockMonitor/Views/DropdownView.swift index fa59a4e..bd2a4bd 100644 --- a/StockMonitor/Views/DropdownView.swift +++ b/StockMonitor/Views/DropdownView.swift @@ -7,22 +7,48 @@ struct DropdownView: View { @State private var selectedStock: Stock? = nil @State private var lastSelectedStockId: String? = nil - private var groupedStocks: [(Market, [Stock])] { - [Market.aStock, .hkStock, .usStock].compactMap { market in - var s = appState.stocks.filter { $0.market == market } - if sortByChange { - let rule = appState.config.sortRule - let usMode = appState.config.usPriceMode - s.sort { a, b in - let qa = appState.quotes[a.id] - let qb = appState.quotes[b.id] - let va = sortValue(quote: qa, market: market, mode: usMode) - let vb = sortValue(quote: qb, market: market, mode: usMode) - return rule == .changeDesc ? va > vb : va < vb - } + /// 分组数据:(market, stocks, customTitle?) + private var groupedStocks: [(Market, [Stock], String?)] { + let rule = appState.config.sortRule + let usMode = appState.config.usPriceMode + let doSort = sortByChange + + func sorted(_ stocks: [Stock], market: Market) -> [Stock] { + guard doSort else { return stocks } + return stocks.sorted { a, b in + let va = sortValue(quote: appState.quotes[a.id], market: market, mode: usMode) + let vb = sortValue(quote: appState.quotes[b.id], market: market, mode: usMode) + return rule == .changeDesc ? va > vb : va < vb } - return s.isEmpty ? nil : (market, s) } + + var result: [(Market, [Stock], String?)] = [] + + // 持仓股单独分组 + if appState.config.groupHoldings { + let holdings = appState.stocks.filter { $0.holdingShares != nil } + if !holdings.isEmpty { + // 持仓组内按第一只股票的市场类型排序(混合市场) + let sortedHoldings = doSort ? holdings.sorted { a, b in + let va = sortValue(quote: appState.quotes[a.id], market: a.market, mode: usMode) + let vb = sortValue(quote: appState.quotes[b.id], market: b.market, mode: usMode) + return rule == .changeDesc ? va > vb : va < vb + } : holdings + result.append((.aStock, sortedHoldings, "持仓")) + } + let holdingIds = Set(holdings.map(\.id)) + for market in [Market.aStock, .hkStock, .usStock] { + let s = appState.stocks.filter { $0.market == market && !holdingIds.contains($0.id) } + if !s.isEmpty { result.append((market, sorted(s, market: market), nil)) } + } + } else { + for market in [Market.aStock, .hkStock, .usStock] { + let s = appState.stocks.filter { $0.market == market } + if !s.isEmpty { result.append((market, sorted(s, market: market), nil)) } + } + } + + return result } private func sortValue(quote: Quote?, market: Market, mode: USPriceMode) -> Double { @@ -53,8 +79,9 @@ struct DropdownView: View { ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 6) { - ForEach(groupedStocks, id: \.0) { market, stocks in - StockGroupView(market: market, stocks: stocks, + ForEach(Array(groupedStocks.enumerated()), id: \.offset) { _, group in + StockGroupView(market: group.0, stocks: group.1, + title: group.2, onSelect: { lastSelectedStockId = $0.id selectedStock = $0 diff --git a/StockMonitor/Views/SettingsView.swift b/StockMonitor/Views/SettingsView.swift index 15426e2..8b28c07 100644 --- a/StockMonitor/Views/SettingsView.swift +++ b/StockMonitor/Views/SettingsView.swift @@ -54,6 +54,14 @@ struct SettingsView: View { }.pickerStyle(.menu) } + // 持仓股分组 + section("持仓股展示") { + Toggle("持仓股单独分组置顶", isOn: Binding( + get: { appState.config.groupHoldings }, + set: { appState.config.groupHoldings = $0 } + )) + } + // 排序规则 section("排序规则") { Picker("", selection: Binding( diff --git a/StockMonitor/Views/StockGroupView.swift b/StockMonitor/Views/StockGroupView.swift index 8aaa36d..37bf89f 100644 --- a/StockMonitor/Views/StockGroupView.swift +++ b/StockMonitor/Views/StockGroupView.swift @@ -3,6 +3,7 @@ import SwiftUI struct StockGroupView: View { let market: Market let stocks: [Stock] + var title: String? = nil var onSelect: ((Stock) -> Void)? = nil @EnvironmentObject var appState: AppState @State private var isExpanded = true @@ -15,7 +16,7 @@ struct StockGroupView: View { HStack(spacing: 4) { Image(systemName: isExpanded ? "chevron.down" : "chevron.right") .font(.system(size: 10, weight: .medium)) - Text(market.rawValue) + Text(title ?? market.rawValue) .font(.system(size: 11, weight: .semibold)) Text("(\(stocks.count))") .font(.system(size: 11))