From ff42b17119428242262e0c83c56882558cfb5c5c Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 11 Dec 2025 11:56:24 +0800 Subject: [PATCH] update pay ui --- .../components/MiniTimelineChart.tsx | 34 +++++- .../FlexScreen/hooks/useRealtimeQuote.ts | 104 ++++++++++++------ .../components/FlexScreen/types.ts | 8 +- 3 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/views/StockOverview/components/FlexScreen/components/MiniTimelineChart.tsx b/src/views/StockOverview/components/FlexScreen/components/MiniTimelineChart.tsx index a22d2a61..86fefb09 100644 --- a/src/views/StockOverview/components/FlexScreen/components/MiniTimelineChart.tsx +++ b/src/views/StockOverview/components/FlexScreen/components/MiniTimelineChart.tsx @@ -65,13 +65,20 @@ const MiniTimelineChart: React.FC = ({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + // 是否首次加载 + const isFirstLoad = useRef(true); + // 用 ref 追踪是否有数据(避免闭包问题) + const hasDataRef = useRef(false); + // 获取分钟数据 useEffect(() => { if (!code) return; const fetchData = async (): Promise => { - setLoading(true); - setError(null); + // 只在首次加载时显示 loading 状态 + if (isFirstLoad.current) { + setLoading(true); + } try { const apiPath = isIndex @@ -81,23 +88,40 @@ const MiniTimelineChart: React.FC = ({ const response = await fetch(apiPath); const result: KLineApiResponse = await response.json(); - if (result.success !== false && result.data) { + if (result.success !== false && result.data && result.data.length > 0) { // 格式化数据 const formatted: TimelineDataPoint[] = result.data.map(item => ({ time: item.time || item.timestamp || '', price: item.close || item.price || 0, })); setTimelineData(formatted); + hasDataRef.current = true; + setError(null); // 清除之前的错误 } else { - setError(result.error || '暂无数据'); + // 只有在没有原有数据时才设置错误(保留原有数据) + if (!hasDataRef.current) { + setError(result.error || '暂无数据'); + } + // 有原有数据时,静默失败,保持显示原有数据 } } catch (e) { - setError('加载失败'); + // 只有在没有原有数据时才设置错误(保留原有数据) + if (!hasDataRef.current) { + setError('加载失败'); + } + // 有原有数据时,静默失败,保持显示原有数据 } finally { setLoading(false); + isFirstLoad.current = false; } }; + // 重置首次加载标记(code 变化时) + isFirstLoad.current = true; + hasDataRef.current = false; + setTimelineData([]); // 切换股票时清空数据 + setError(null); + fetchData(); // 交易时间内每分钟刷新 diff --git a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts index d344af71..3db861c4 100644 --- a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts +++ b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts @@ -117,14 +117,14 @@ const extractSZSEPrices = (stockData: SZSEStockData) => { /** * 处理深交所批量行情消息(新 API 格式,与 SSE 一致) - * 格式:{ type: 'stock', data: { '000001': {...}, '000002': {...} }, timestamp: '...' } + * 格式:{ type: 'stock'/'index', data: { '000001': {...}, '399001': {...} }, timestamp: '...' } */ const handleSZSEBatchMessage = ( msg: SZSERealtimeMessage, subscribedCodes: Set, prevQuotes: QuotesMap ): QuotesMap | null => { - const { data, timestamp } = msg; + const { type, data, timestamp } = msg; // 新 API 格式:data 是 { code: quote, ... } 的字典 if (!data || typeof data !== 'object') { @@ -133,14 +133,15 @@ const handleSZSEBatchMessage = ( const updated: QuotesMap = { ...prevQuotes }; let hasUpdate = false; + const isIndexType = type === 'index'; - // 遍历所有股票数据 + // 遍历所有数据 Object.entries(data).forEach(([code, quote]) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const stockData = quote as any; - if (!stockData || typeof stockData !== 'object') return; + const quoteData = quote as any; + if (!quoteData || typeof quoteData !== 'object') return; - const rawCode = stockData.security_id || code; + const rawCode = quoteData.security_id || code; const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`; // 只处理已订阅的代码 @@ -149,32 +150,62 @@ const handleSZSEBatchMessage = ( } hasUpdate = true; - const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(stockData); - const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(stockData); - updated[fullCode] = { - code: fullCode, - name: prevQuotes[fullCode]?.name || '', - price: stockData.last_px, - prevClose, - open: stockData.open_px, - high: stockData.high_px, - low: stockData.low_px, - volume, - amount, - numTrades: stockData.num_trades, - upperLimit, - lowerLimit, - change: stockData.last_px - prevClose, - changePct: calcChangePct(stockData.last_px, prevClose), - bidPrices, - bidVolumes, - askPrices, - askVolumes, - tradingPhase, - updateTime: stockData.update_time || timestamp, - exchange: 'SZSE', - } as QuoteData; + if (isIndexType) { + // 指数数据格式 + const prevClose = quoteData.prev_close ?? 0; + const currentIndex = quoteData.current_index ?? quoteData.last_px ?? 0; + + updated[fullCode] = { + code: fullCode, + name: prevQuotes[fullCode]?.name || '', + price: currentIndex, + prevClose, + open: quoteData.open_index ?? quoteData.open_px ?? 0, + high: quoteData.high_index ?? quoteData.high_px ?? 0, + low: quoteData.low_index ?? quoteData.low_px ?? 0, + close: quoteData.close_index, + volume: quoteData.volume ?? 0, + amount: quoteData.amount ?? 0, + numTrades: quoteData.num_trades, + change: currentIndex - prevClose, + changePct: calcChangePct(currentIndex, prevClose), + bidPrices: [], + bidVolumes: [], + askPrices: [], + askVolumes: [], + updateTime: quoteData.update_time || timestamp, + exchange: 'SZSE', + } as QuoteData; + } else { + // 股票/基金/债券数据格式 + const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(quoteData); + const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(quoteData); + + updated[fullCode] = { + code: fullCode, + name: prevQuotes[fullCode]?.name || '', + price: quoteData.last_px, + prevClose, + open: quoteData.open_px, + high: quoteData.high_px, + low: quoteData.low_px, + volume, + amount, + numTrades: quoteData.num_trades, + upperLimit, + lowerLimit, + change: quoteData.last_px - prevClose, + changePct: calcChangePct(quoteData.last_px, prevClose), + bidPrices, + bidVolumes, + askPrices, + askVolumes, + tradingPhase, + updateTime: quoteData.update_time || timestamp, + exchange: 'SZSE', + } as QuoteData; + } }); return hasUpdate ? updated : null; @@ -182,7 +213,7 @@ const handleSZSEBatchMessage = ( /** * 处理深交所实时消息 (兼容新旧 API) - * 新 API (批量模式): type='stock'/'bond'/'fund', data = { code: quote, ... } + * 新 API (批量模式): type='stock'/'bond'/'fund'/'index', data = { code: quote, ... } * 旧 API (单条模式): type='realtime', category='stock', data = { security_id, ... } */ const handleSZSERealtimeMessage = ( @@ -197,7 +228,7 @@ const handleSZSERealtimeMessage = ( const anyData = data as any; const isBatchFormat = anyData && typeof anyData === 'object' && !anyData.security_id; - if (isBatchFormat && (type === 'stock' || type === 'bond' || type === 'fund')) { + if (isBatchFormat && (type === 'stock' || type === 'bond' || type === 'fund' || type === 'index')) { return handleSZSEBatchMessage(msg, subscribedCodes, prevQuotes); } @@ -597,14 +628,14 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = /** * 发送深交所订阅请求(新 API 格式) - * 格式:{ action: 'subscribe', channels: ['stock'], codes: ['000001', '000002'] } + * 格式:{ action: 'subscribe', channels: ['stock', 'index'], codes: ['000001', '000002'] } */ const sendSZSESubscribe = useCallback((baseCodes: string[]) => { const ws = wsRefs.current.SZSE; if (ws && ws.readyState === WebSocket.OPEN && baseCodes.length > 0) { ws.send(JSON.stringify({ action: 'subscribe', - channels: ['stock'], // 订阅股票频道 + channels: ['stock', 'index'], // 订阅股票和指数频道 codes: baseCodes, })); logger.info('FlexScreen', `SZSE 发送订阅请求`, { codes: baseCodes }); @@ -700,10 +731,11 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = }); break; - // 新 API:直接使用 type='stock'/'bond'/'fund' 作为消息类型 + // 新 API:直接使用 type='stock'/'bond'/'fund'/'index' 作为消息类型 case 'stock': case 'bond': case 'fund': + case 'index': setQuotes(prev => { const result = handleSZSERealtimeMessage( msg as SZSERealtimeMessage, diff --git a/src/views/StockOverview/components/FlexScreen/types.ts b/src/views/StockOverview/components/FlexScreen/types.ts index 317dcfd3..7ba15587 100644 --- a/src/views/StockOverview/components/FlexScreen/types.ts +++ b/src/views/StockOverview/components/FlexScreen/types.ts @@ -253,13 +253,15 @@ export interface SZSEAfterhoursData { num_trades?: number; } -/** 深交所实时消息(新 API 格式:type 直接是 'stock' | 'bond' | 'fund') */ +/** 深交所实时消息(新 API 格式:type 直接是 'stock' | 'index' | 'bond' | 'fund') */ export interface SZSERealtimeMessage { - type: 'stock' | 'bond' | 'fund' | 'realtime'; // 新 API 直接用 type='stock' 等 + type: 'stock' | 'index' | 'bond' | 'fund' | 'hkstock' | 'realtime'; // 新 API 直接用 type='stock' 等 category?: SZSECategory; // 旧 API 使用 category msg_type?: number; timestamp: string; - data: SZSEStockData | SZSEIndexData | SZSEBondData | SZSEHKStockData | SZSEAfterhoursData; + // 新 API 批量格式:data 是 { code: quote, ... } 字典 + // 旧 API 单条格式:data 是单个行情对象 + data: SZSEStockData | SZSEIndexData | SZSEBondData | SZSEHKStockData | SZSEAfterhoursData | Record; } /** 深交所快照消息 */