update pay ui

This commit is contained in:
2025-12-11 11:56:24 +08:00
parent 86e31fd2bf
commit ff42b17119
3 changed files with 102 additions and 44 deletions

View File

@@ -65,13 +65,20 @@ const MiniTimelineChart: React.FC<MiniTimelineChartProps> = ({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// 是否首次加载
const isFirstLoad = useRef(true);
// 用 ref 追踪是否有数据(避免闭包问题)
const hasDataRef = useRef(false);
// 获取分钟数据 // 获取分钟数据
useEffect(() => { useEffect(() => {
if (!code) return; if (!code) return;
const fetchData = async (): Promise<void> => { const fetchData = async (): Promise<void> => {
// 只在首次加载时显示 loading 状态
if (isFirstLoad.current) {
setLoading(true); setLoading(true);
setError(null); }
try { try {
const apiPath = isIndex const apiPath = isIndex
@@ -81,23 +88,40 @@ const MiniTimelineChart: React.FC<MiniTimelineChartProps> = ({
const response = await fetch(apiPath); const response = await fetch(apiPath);
const result: KLineApiResponse = await response.json(); 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 => ({ const formatted: TimelineDataPoint[] = result.data.map(item => ({
time: item.time || item.timestamp || '', time: item.time || item.timestamp || '',
price: item.close || item.price || 0, price: item.close || item.price || 0,
})); }));
setTimelineData(formatted); setTimelineData(formatted);
hasDataRef.current = true;
setError(null); // 清除之前的错误
} else { } else {
// 只有在没有原有数据时才设置错误(保留原有数据)
if (!hasDataRef.current) {
setError(result.error || '暂无数据'); setError(result.error || '暂无数据');
} }
// 有原有数据时,静默失败,保持显示原有数据
}
} catch (e) { } catch (e) {
// 只有在没有原有数据时才设置错误(保留原有数据)
if (!hasDataRef.current) {
setError('加载失败'); setError('加载失败');
}
// 有原有数据时,静默失败,保持显示原有数据
} finally { } finally {
setLoading(false); setLoading(false);
isFirstLoad.current = false;
} }
}; };
// 重置首次加载标记code 变化时)
isFirstLoad.current = true;
hasDataRef.current = false;
setTimelineData([]); // 切换股票时清空数据
setError(null);
fetchData(); fetchData();
// 交易时间内每分钟刷新 // 交易时间内每分钟刷新

View File

@@ -117,14 +117,14 @@ const extractSZSEPrices = (stockData: SZSEStockData) => {
/** /**
* 处理深交所批量行情消息(新 API 格式,与 SSE 一致) * 处理深交所批量行情消息(新 API 格式,与 SSE 一致)
* 格式:{ type: 'stock', data: { '000001': {...}, '000002': {...} }, timestamp: '...' } * 格式:{ type: 'stock'/'index', data: { '000001': {...}, '399001': {...} }, timestamp: '...' }
*/ */
const handleSZSEBatchMessage = ( const handleSZSEBatchMessage = (
msg: SZSERealtimeMessage, msg: SZSERealtimeMessage,
subscribedCodes: Set<string>, subscribedCodes: Set<string>,
prevQuotes: QuotesMap prevQuotes: QuotesMap
): QuotesMap | null => { ): QuotesMap | null => {
const { data, timestamp } = msg; const { type, data, timestamp } = msg;
// 新 API 格式data 是 { code: quote, ... } 的字典 // 新 API 格式data 是 { code: quote, ... } 的字典
if (!data || typeof data !== 'object') { if (!data || typeof data !== 'object') {
@@ -133,14 +133,15 @@ const handleSZSEBatchMessage = (
const updated: QuotesMap = { ...prevQuotes }; const updated: QuotesMap = { ...prevQuotes };
let hasUpdate = false; let hasUpdate = false;
const isIndexType = type === 'index';
// 遍历所有股票数据 // 遍历所有数据
Object.entries(data).forEach(([code, quote]) => { Object.entries(data).forEach(([code, quote]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const stockData = quote as any; const quoteData = quote as any;
if (!stockData || typeof stockData !== 'object') return; 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`; const fullCode = rawCode.includes('.') ? rawCode : `${rawCode}.SZ`;
// 只处理已订阅的代码 // 只处理已订阅的代码
@@ -149,32 +150,62 @@ const handleSZSEBatchMessage = (
} }
hasUpdate = true; hasUpdate = true;
const { bidPrices, bidVolumes, askPrices, askVolumes } = extractSZSEOrderBook(stockData);
const { prevClose, volume, amount, upperLimit, lowerLimit, tradingPhase } = extractSZSEPrices(stockData); if (isIndexType) {
// 指数数据格式
const prevClose = quoteData.prev_close ?? 0;
const currentIndex = quoteData.current_index ?? quoteData.last_px ?? 0;
updated[fullCode] = { updated[fullCode] = {
code: fullCode, code: fullCode,
name: prevQuotes[fullCode]?.name || '', name: prevQuotes[fullCode]?.name || '',
price: stockData.last_px, price: currentIndex,
prevClose, prevClose,
open: stockData.open_px, open: quoteData.open_index ?? quoteData.open_px ?? 0,
high: stockData.high_px, high: quoteData.high_index ?? quoteData.high_px ?? 0,
low: stockData.low_px, 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, volume,
amount, amount,
numTrades: stockData.num_trades, numTrades: quoteData.num_trades,
upperLimit, upperLimit,
lowerLimit, lowerLimit,
change: stockData.last_px - prevClose, change: quoteData.last_px - prevClose,
changePct: calcChangePct(stockData.last_px, prevClose), changePct: calcChangePct(quoteData.last_px, prevClose),
bidPrices, bidPrices,
bidVolumes, bidVolumes,
askPrices, askPrices,
askVolumes, askVolumes,
tradingPhase, tradingPhase,
updateTime: stockData.update_time || timestamp, updateTime: quoteData.update_time || timestamp,
exchange: 'SZSE', exchange: 'SZSE',
} as QuoteData; } as QuoteData;
}
}); });
return hasUpdate ? updated : null; return hasUpdate ? updated : null;
@@ -182,7 +213,7 @@ const handleSZSEBatchMessage = (
/** /**
* 处理深交所实时消息 (兼容新旧 API) * 处理深交所实时消息 (兼容新旧 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, ... } * 旧 API (单条模式): type='realtime', category='stock', data = { security_id, ... }
*/ */
const handleSZSERealtimeMessage = ( const handleSZSERealtimeMessage = (
@@ -197,7 +228,7 @@ const handleSZSERealtimeMessage = (
const anyData = data as any; const anyData = data as any;
const isBatchFormat = anyData && typeof anyData === 'object' && !anyData.security_id; 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); return handleSZSEBatchMessage(msg, subscribedCodes, prevQuotes);
} }
@@ -597,14 +628,14 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
/** /**
* 发送深交所订阅请求(新 API 格式) * 发送深交所订阅请求(新 API 格式)
* 格式:{ action: 'subscribe', channels: ['stock'], codes: ['000001', '000002'] } * 格式:{ action: 'subscribe', channels: ['stock', 'index'], codes: ['000001', '000002'] }
*/ */
const sendSZSESubscribe = useCallback((baseCodes: string[]) => { const sendSZSESubscribe = useCallback((baseCodes: string[]) => {
const ws = wsRefs.current.SZSE; const ws = wsRefs.current.SZSE;
if (ws && ws.readyState === WebSocket.OPEN && baseCodes.length > 0) { if (ws && ws.readyState === WebSocket.OPEN && baseCodes.length > 0) {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
action: 'subscribe', action: 'subscribe',
channels: ['stock'], // 订阅股票频道 channels: ['stock', 'index'], // 订阅股票和指数频道
codes: baseCodes, codes: baseCodes,
})); }));
logger.info('FlexScreen', `SZSE 发送订阅请求`, { codes: baseCodes }); logger.info('FlexScreen', `SZSE 发送订阅请求`, { codes: baseCodes });
@@ -700,10 +731,11 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
}); });
break; break;
// 新 API直接使用 type='stock'/'bond'/'fund' 作为消息类型 // 新 API直接使用 type='stock'/'bond'/'fund'/'index' 作为消息类型
case 'stock': case 'stock':
case 'bond': case 'bond':
case 'fund': case 'fund':
case 'index':
setQuotes(prev => { setQuotes(prev => {
const result = handleSZSERealtimeMessage( const result = handleSZSERealtimeMessage(
msg as SZSERealtimeMessage, msg as SZSERealtimeMessage,

View File

@@ -253,13 +253,15 @@ export interface SZSEAfterhoursData {
num_trades?: number; num_trades?: number;
} }
/** 深交所实时消息(新 API 格式type 直接是 'stock' | 'bond' | 'fund' */ /** 深交所实时消息(新 API 格式type 直接是 'stock' | 'index' | 'bond' | 'fund' */
export interface SZSERealtimeMessage { 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 category?: SZSECategory; // 旧 API 使用 category
msg_type?: number; msg_type?: number;
timestamp: string; timestamp: string;
data: SZSEStockData | SZSEIndexData | SZSEBondData | SZSEHKStockData | SZSEAfterhoursData; // 新 API 批量格式data 是 { code: quote, ... } 字典
// 旧 API 单条格式data 是单个行情对象
data: SZSEStockData | SZSEIndexData | SZSEBondData | SZSEHKStockData | SZSEAfterhoursData | Record<string, unknown>;
} }
/** 深交所快照消息 */ /** 深交所快照消息 */