Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion PanBar/Domain/Services/PortfolioService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ actor PortfolioService {
if allSymbols.isEmpty {
quotes = [:]
} else {
quotes = (try? await provider.fetch(Array(allSymbols))) ?? [:]
let fetched = try await provider.fetch(Array(allSymbols))
guard !fetched.isEmpty else { throw ProviderError.empty }
quotes = fetched
}

let converter = await fx.currentConverter()
Expand Down
31 changes: 16 additions & 15 deletions PanBar/Infrastructure/QuoteRefresher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,32 +224,33 @@ final class QuoteRefresher: ObservableObject {
}

private func tick() async {
await MainActor.run { self.isRefreshing = true }
guard !isRefreshing else { return }
isRefreshing = true
defer { isRefreshing = false }

do {
let snap = try await service.computeSnapshot()
// 网络空响应(provider.fetch 失败 -> quotes 空)时,不要拿一个全 nil quote 的
// snapshot 去覆盖磁盘 seed 的好数据;原样保留 cached snapshot,等下次 tick。
let hasFreshData = !snap.allQuotes.isEmpty
await MainActor.run {
if hasFreshData {
self.snapshot = snap
self.snapshotIsFromCache = false
self.quotes.merge(snap.allQuotes) { _, new in new }
self.lastUpdated = Date()
self.lastError = nil
self.alertEngine?.evaluate(quotes: self.quotes)
let isEmptyPortfolio = snap.positions.isEmpty && snap.allQuotes.isEmpty
if hasFreshData || isEmptyPortfolio {
snapshot = snap
snapshotIsFromCache = false
if isEmptyPortfolio {
quotes = [:]
} else {
quotes.merge(snap.allQuotes) { _, new in new }
}
lastUpdated = Date()
lastError = nil
alertEngine?.evaluate(quotes: quotes)
}
// 拉到了就异步写盘,下次冷启动可以秒读
if hasFreshData {
quoteCacheRepo?.upsertMany(snap.allQuotes)
}
} catch {
await MainActor.run {
self.lastError = String(describing: error)
}
lastError = String(describing: error)
Log.quote.error("refresh failed: \(String(describing: error), privacy: .public)")
}
await MainActor.run { self.isRefreshing = false }
}
}
11 changes: 8 additions & 3 deletions PanBar/Popover/Views/PopoverRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,12 @@ struct PopoverRoot: View {
}

private var footerStatus: String {
if let err = refresher.lastError, !err.isEmpty {
if refresher.isOffline {
return L("footer.offline", comment: "")
}
if let err = refresher.lastError, !err.isEmpty {
return L("footer.quoteFailed", comment: "")
}
// 还没拿到任何 fresh 数据,但有磁盘 seed 进来的旧数据
if refresher.snapshotIsFromCache {
return L("footer.cached", comment: "")
Expand All @@ -247,16 +250,18 @@ struct PopoverRoot: View {

/// 文字统一用 .secondary 灰色保证两个模式下都清晰可读,状态用图标颜色区分:
/// - 离线:橙色 wifi.slash
/// - 行情失败:橙色感叹号
/// - cached:橙色 clock(让用户知道在看的不是最新值,但不刺眼)
/// - 正常:灰色刷新箭头
private var footerIcon: String {
if refresher.lastError != nil { return "wifi.slash" }
if refresher.isOffline { return "wifi.slash" }
if refresher.lastError != nil { return "exclamationmark.triangle" }
if refresher.snapshotIsFromCache { return "clock.arrow.circlepath" }
return "arrow.clockwise"
}

private var footerIconColor: Color {
if refresher.lastError != nil { return .orange }
if refresher.isOffline || refresher.lastError != nil { return .orange }
if refresher.snapshotIsFromCache { return .orange }
return .secondary
}
Expand Down
6 changes: 6 additions & 0 deletions PanBar/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "离线" } }
}
},
"footer.quoteFailed" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Quote refresh failed" } },
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "行情刷新失败" } }
}
},
"holdings.empty" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "No holdings yet." } },
Expand Down