v1.2.1: Add sort settings (change% asc/desc, US session/regular price mode)

- Add SortRule enum (涨跌幅升序/降序) and USPriceMode enum (时段价格/盘中价)
- Settings page: sort rule segmented picker + US price mode picker
- Toolbar sort button applies configured rule; off = default order
- US stocks: session price mode uses extendedPrice for change% calculation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vgearen
2026-03-20 15:30:41 +08:00
co-authored by Claude Opus 4.6
parent 94b14693d5
commit c7b19e4f49
4 changed files with 74 additions and 9 deletions
+29 -1
View File
@@ -22,6 +22,30 @@ enum DisplayCurrency: String, Codable, CaseIterable {
}
}
enum SortRule: String, Codable, CaseIterable {
case changeDesc = "changeDesc"
case changeAsc = "changeAsc"
var displayName: String {
switch self {
case .changeDesc: return "涨跌幅降序"
case .changeAsc: return "涨跌幅升序"
}
}
}
enum USPriceMode: String, Codable, CaseIterable {
case sessionPrice = "session"
case regularPrice = "regular"
var displayName: String {
switch self {
case .sessionPrice: return "当前价(时段价格)"
case .regularPrice: return "盘中价"
}
}
}
enum ColorTheme: String, Codable, CaseIterable {
case chinese = "chinese" // 红涨绿跌
case western = "western" // 绿涨红跌
@@ -39,6 +63,8 @@ struct AppSettings {
var refreshInterval: Int = 5
var colorScheme: ColorTheme = .chinese
var displayCurrency: DisplayCurrency = .cny
var sortRule: SortRule = .changeDesc
var usPriceMode: USPriceMode = .sessionPrice
static let validRefreshIntervals = [3, 5, 10, 30]
@@ -49,7 +75,7 @@ struct AppSettings {
// MARK: - Codable(容错:缺失字段使用默认值)
extension AppSettings: Codable {
enum CodingKeys: String, CodingKey {
case statusBarStockId, refreshInterval, colorScheme, displayCurrency
case statusBarStockId, refreshInterval, colorScheme, displayCurrency, sortRule, usPriceMode
}
init(from decoder: Decoder) throws {
@@ -58,5 +84,7 @@ extension AppSettings: Codable {
refreshInterval = (try? c.decodeIfPresent(Int.self, forKey: .refreshInterval)) ?? 5
colorScheme = (try? c.decodeIfPresent(ColorTheme.self, forKey: .colorScheme)) ?? .chinese
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
}
}
+17 -4
View File
@@ -10,16 +10,29 @@ struct DropdownView: View {
[Market.aStock, .hkStock, .usStock].compactMap { market in
var s = appState.stocks.filter { $0.market == market }
if sortByChange {
s.sort {
let a = appState.quotes[$0.id]?.changePercent ?? -Double.infinity
let b = appState.quotes[$1.id]?.changePercent ?? -Double.infinity
return a > b
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
}
}
return s.isEmpty ? nil : (market, s)
}
}
private func sortValue(quote: Quote?, market: Market, mode: USPriceMode) -> Double {
guard let q = quote else { return -Double.infinity }
if market == .usStock && mode == .sessionPrice, let ext = q.extendedPrice {
// 用时段价格算涨跌幅:(盘外价 - 正盘价) / 正盘价
return q.price > 0 ? (ext - q.price) / q.price * 100 : q.changePercent
}
return q.changePercent
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
if showSettings {
+24
View File
@@ -54,6 +54,30 @@ struct SettingsView: View {
}.pickerStyle(.menu)
}
// 排序规则
section("排序规则") {
Picker("", selection: Binding(
get: { appState.config.sortRule },
set: { appState.config.sortRule = $0 }
)) {
ForEach(SortRule.allCases, id: \.rawValue) { rule in
Text(rule.displayName).tag(rule)
}
}.pickerStyle(.segmented)
}
// 美股排序价格
section("美股排序价格") {
Picker("", selection: Binding(
get: { appState.config.usPriceMode },
set: { appState.config.usPriceMode = $0 }
)) {
ForEach(USPriceMode.allCases, id: \.rawValue) { mode in
Text(mode.displayName).tag(mode)
}
}.pickerStyle(.segmented)
}
// 刷新间隔
section("刷新间隔") {
Picker("", selection: Binding(