diff --git a/frontend/src/pages/Stocks.tsx b/frontend/src/pages/Stocks.tsx index f03c200..1cb882f 100644 --- a/frontend/src/pages/Stocks.tsx +++ b/frontend/src/pages/Stocks.tsx @@ -223,6 +223,33 @@ const emptyAccountForm: AccountForm = { name: '', available_funds: '0' } const round2 = (value: number) => Math.round(value * 100) / 100 +// --- 持仓摘要缓存(请求失败时兜底)--- +const PORTFOLIO_SUMMARY_CACHE_KEY = 'panwatch_portfolio_summary_cache_v1' + +interface PortfolioSummaryCache { + savedAt: string + data: PortfolioSummary +} + +const readPortfolioSummaryCache = (): PortfolioSummary | null => { + try { + const raw = localStorage.getItem(PORTFOLIO_SUMMARY_CACHE_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) as PortfolioSummaryCache + if (!parsed?.data?.accounts || !parsed?.data?.total) return null + return parsed.data + } catch { return null } +} + +const writePortfolioSummaryCache = (data: PortfolioSummary) => { + try { + localStorage.setItem(PORTFOLIO_SUMMARY_CACHE_KEY, JSON.stringify({ + savedAt: new Date().toISOString(), + data, + })) + } catch { /* ignore quota / private mode errors */ } +} + const mergePortfolioQuotes = ( portfolio: PortfolioSummary | null, quotes: Record @@ -342,6 +369,7 @@ export default function StocksPage() { const [portfolio, setPortfolio] = useState(null) const [portfolioRaw, setPortfolioRaw] = useState(null) const [portfolioLoading, setPortfolioLoading] = useState(false) + const [portfolioCacheUsed, setPortfolioCacheUsed] = useState(false) const [expandedAccounts, setExpandedAccounts] = useState>(new Set()) // Quotes for all stocks (used in stock list) @@ -583,12 +611,26 @@ export default function StocksPage() { } const loadPortfolio = async () => { + // 先尝试从缓存读取,避免请求未返回时页面空白 + const cached = readPortfolioSummaryCache() + if (cached && !portfolioRaw) { + setPortfolioRaw(cached) + setPortfolio(mergePortfolioQuotes(cached, quotes)) + setPortfolioCacheUsed(true) + } + setPortfolioLoading(true) try { - // 核心数据:仅本地账户/持仓 - const portfolioData = await fetchAPI('/portfolio/summary?include_quotes=false') + // 核心数据:仅本地账户/持仓(缩短超时,失败时快速降级到缓存) + const portfolioData = await fetchAPI( + '/portfolio/summary?include_quotes=false', + { timeoutMs: 8000 }, + ) setPortfolioRaw(portfolioData) setPortfolio(mergePortfolioQuotes(portfolioData, quotes)) + writePortfolioSummaryCache(portfolioData) + setPortfolioCacheUsed(false) + setPortfolio(mergePortfolioQuotes(portfolioData, quotes)) // 市场状态(非核心,失败不影响页面) try { @@ -599,6 +641,16 @@ export default function StocksPage() { } } catch (e) { console.error(e) + // 请求失败时用缓存兜底 + const fallback = readPortfolioSummaryCache() + if (fallback) { + setPortfolioRaw(fallback) + setPortfolio(mergePortfolioQuotes(fallback, quotes)) + setPortfolioCacheUsed(true) + toast('持仓数据请求失败,已显示上次缓存', 'info') + } else { + toast(e instanceof Error ? e.message : '持仓数据加载失败', 'error') + } } finally { setPortfolioLoading(false) } @@ -1425,6 +1477,10 @@ export default function StocksPage() { return stocks.length }, [stocks]) + const dataUpdateTimeText = lastRefreshTime + ? lastRefreshTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + : '—' + const toggleAccountExpanded = (id: number) => { setExpandedAccounts(prev => { const next = new Set(prev) @@ -1524,14 +1580,6 @@ export default function StocksPage() { )} - {lastRefreshTime && ( - <> -
- - {lastRefreshTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} - - - )}
{/* Buttons */} {(() => { const { suggestion, kline } = getSuggestionForStock(pos.symbol, pos.market, true) + if (poolSuggestionsLoading && !suggestion && !kline) { + return ( + +
+ + ) + } return (suggestion || kline) ? ( { const { suggestion, kline } = getSuggestionForStock(pos.symbol, pos.market, true) + if (poolSuggestionsLoading && !suggestion && !kline) { + return ( +
+
+
+ ) + } return (suggestion || kline) ? (
- {(suggestion || kline) ? ( + {poolSuggestionsLoading && !suggestion && !kline ? ( +
+ ) : (suggestion || kline) ? ( {newsLoading ? ( -
- - 加载中... +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+ ))}
) : news.length === 0 ? (