diff --git a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts index aa4b1df6..d344af71 100644 --- a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts +++ b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts @@ -116,56 +116,74 @@ const extractSZSEPrices = (stockData: SZSEStockData) => { }; /** - * 处理深交所股票行情消息(新 API 格式:type='stock') + * 处理深交所批量行情消息(新 API 格式,与 SSE 一致) + * 格式:{ type: 'stock', data: { '000001': {...}, '000002': {...} }, timestamp: '...' } */ -const handleSZSEStockMessage = ( - data: SZSEStockData, - timestamp: string, +const handleSZSEBatchMessage = ( + msg: SZSERealtimeMessage, subscribedCodes: Set, prevQuotes: QuotesMap ): QuotesMap | null => { - const rawCode = data.security_id; - const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`; + const { data, timestamp } = msg; - if (!subscribedCodes.has(rawCode) && !subscribedCodes.has(fullCode)) { + // 新 API 格式:data 是 { code: quote, ... } 的字典 + if (!data || typeof data !== 'object') { return null; } - const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(data); - const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(data); - const updated: QuotesMap = { ...prevQuotes }; - updated[fullCode] = { - code: fullCode, - name: prevQuotes[fullCode]?.name || '', - price: data.last_px, - prevClose, - open: data.open_px, - high: data.high_px, - low: data.low_px, - volume, - amount, - numTrades: data.num_trades, - upperLimit, - lowerLimit, - change: data.last_px - prevClose, - changePct: calcChangePct(data.last_px, prevClose), - bidPrices, - bidVolumes, - askPrices, - askVolumes, - tradingPhase, - updateTime: data.update_time || timestamp, - exchange: 'SZSE', - } as QuoteData; + let hasUpdate = false; - return updated; + // 遍历所有股票数据 + 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 rawCode = stockData.security_id || code; + const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`; + + // 只处理已订阅的代码 + if (!subscribedCodes.has(code) && !subscribedCodes.has(rawCode) && !subscribedCodes.has(fullCode)) { + return; + } + + 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; + }); + + return hasUpdate ? updated : null; }; /** * 处理深交所实时消息 (兼容新旧 API) - * 新 API: type='stock'/'bond'/'fund', data 直接是行情对象 - * 旧 API: type='realtime', category='stock'/'bond'/etc + * 新 API (批量模式): type='stock'/'bond'/'fund', data = { code: quote, ... } + * 旧 API (单条模式): type='realtime', category='stock', data = { security_id, ... } */ const handleSZSERealtimeMessage = ( msg: SZSERealtimeMessage, @@ -173,7 +191,22 @@ const handleSZSERealtimeMessage = ( prevQuotes: QuotesMap ): QuotesMap | null => { const { type, category, data, timestamp } = msg; - const rawCode = data.security_id; + + // 新 API 批量格式检测:data 是字典 { code: quote, ... },没有 security_id 在顶层 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anyData = data as any; + const isBatchFormat = anyData && typeof anyData === 'object' && !anyData.security_id; + + if (isBatchFormat && (type === 'stock' || type === 'bond' || type === 'fund')) { + return handleSZSEBatchMessage(msg, subscribedCodes, prevQuotes); + } + + // 旧 API 单条格式:data 直接是行情对象 { security_id, last_px, ... } + const rawCode = anyData?.security_id; + if (!rawCode) { + return null; + } + const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`; if (!subscribedCodes.has(rawCode) && !subscribedCodes.has(fullCode)) {