mirror of
https://github.com/VGEAREN/Stockbar.git
synced 2026-10-06 14:33:11 +08:00
v1.2.3: Add holdings group option to pin held stocks at top
- New setting: "持仓股单独分组置顶" toggle - When enabled, stocks with holdings are grouped into a "持仓" section at top - Remaining non-held stocks still grouped by A/HK/US market as usual - StockGroupView supports custom title for holdings group Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
d898185745
commit
7e502232d3
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user