update pay ui
This commit is contained in:
@@ -293,6 +293,102 @@ const handleSZSESnapshotMessage = (
|
||||
return updated;
|
||||
};
|
||||
|
||||
/** API 响应中的行情数据 */
|
||||
interface ApiQuoteData {
|
||||
security_id: string;
|
||||
name: string;
|
||||
last_px: number;
|
||||
prev_close_px: number;
|
||||
open_px: number;
|
||||
high_px: number;
|
||||
low_px: number;
|
||||
total_volume_trade: number;
|
||||
total_value_trade: number;
|
||||
num_trades?: number;
|
||||
upper_limit_px?: number;
|
||||
lower_limit_px?: number;
|
||||
trading_phase_code?: string;
|
||||
change: number;
|
||||
change_pct: number;
|
||||
bid_prices?: number[];
|
||||
bid_volumes?: number[];
|
||||
ask_prices?: number[];
|
||||
ask_volumes?: number[];
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
/** API 响应结构 */
|
||||
interface FlexScreenQuotesResponse {
|
||||
success: boolean;
|
||||
data: Record<string, ApiQuoteData>;
|
||||
source: 'realtime' | 'minute' | 'mixed';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端 API 获取初始行情数据
|
||||
* 用于盘后或 WebSocket 无数据时的回退
|
||||
*/
|
||||
const fetchInitialQuotes = async (
|
||||
codes: string[],
|
||||
includeOrderBook = true
|
||||
): Promise<QuotesMap> => {
|
||||
if (codes.length === 0) return {};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/flex-screen/quotes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ codes, include_order_book: includeOrderBook }),
|
||||
});
|
||||
|
||||
const result: FlexScreenQuotesResponse = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
logger.warn('FlexScreen', '获取初始行情失败', { error: result.error });
|
||||
return {};
|
||||
}
|
||||
|
||||
logger.info('FlexScreen', `获取初始行情成功`, { source: result.source, count: Object.keys(result.data).length });
|
||||
|
||||
// 转换 API 响应为 QuotesMap 格式
|
||||
const quotesMap: QuotesMap = {};
|
||||
|
||||
Object.entries(result.data).forEach(([code, apiQuote]) => {
|
||||
const exchange: Exchange = code.endsWith('.SH') ? 'SSE' : 'SZSE';
|
||||
|
||||
quotesMap[code] = {
|
||||
code,
|
||||
name: apiQuote.name || '',
|
||||
price: apiQuote.last_px,
|
||||
prevClose: apiQuote.prev_close_px,
|
||||
open: apiQuote.open_px,
|
||||
high: apiQuote.high_px,
|
||||
low: apiQuote.low_px,
|
||||
volume: apiQuote.total_volume_trade,
|
||||
amount: apiQuote.total_value_trade,
|
||||
numTrades: apiQuote.num_trades,
|
||||
upperLimit: apiQuote.upper_limit_px,
|
||||
lowerLimit: apiQuote.lower_limit_px,
|
||||
change: apiQuote.change,
|
||||
changePct: apiQuote.change_pct,
|
||||
bidPrices: apiQuote.bid_prices || [],
|
||||
bidVolumes: apiQuote.bid_volumes || [],
|
||||
askPrices: apiQuote.ask_prices || [],
|
||||
askVolumes: apiQuote.ask_volumes || [],
|
||||
tradingPhase: apiQuote.trading_phase_code,
|
||||
updateTime: apiQuote.update_time,
|
||||
exchange,
|
||||
} as QuoteData;
|
||||
});
|
||||
|
||||
return quotesMap;
|
||||
} catch (e) {
|
||||
logger.error('FlexScreen', '获取初始行情失败', e);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 实时行情 Hook
|
||||
* @param codes - 订阅的证券代码列表(带后缀,如 000001.SZ)
|
||||
@@ -305,6 +401,8 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
const heartbeatRefs = useRef<Record<Exchange, NodeJS.Timeout | null>>({ SSE: null, SZSE: null });
|
||||
const reconnectRefs = useRef<Record<Exchange, NodeJS.Timeout | null>>({ SSE: null, SZSE: null });
|
||||
const reconnectCountRef = useRef<Record<Exchange, number>>({ SSE: 0, SZSE: 0 });
|
||||
// 是否已加载过初始数据
|
||||
const initialLoadedRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const subscribedCodes = useRef<Record<Exchange, Set<string>>>({
|
||||
SSE: new Set(),
|
||||
@@ -634,6 +732,32 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
}
|
||||
});
|
||||
|
||||
const allNewCodes = [...newSseCodes, ...newSzseCodes];
|
||||
|
||||
// 检查是否有新增的代码需要加载初始数据
|
||||
const codesToLoad = allNewCodes.filter(c => !initialLoadedRef.current.has(c));
|
||||
|
||||
if (codesToLoad.length > 0) {
|
||||
// 标记为已加载(避免重复请求)
|
||||
codesToLoad.forEach(c => initialLoadedRef.current.add(c));
|
||||
|
||||
// 从后端获取初始行情数据(异步,不阻塞 WebSocket 连接)
|
||||
fetchInitialQuotes(codesToLoad, true).then(initialQuotes => {
|
||||
if (Object.keys(initialQuotes).length > 0) {
|
||||
setQuotes(prev => {
|
||||
// 只设置当前没有数据的股票(避免覆盖 WebSocket 实时数据)
|
||||
const merged = { ...prev };
|
||||
Object.entries(initialQuotes).forEach(([code, quote]) => {
|
||||
if (!merged[code] || !merged[code].price) {
|
||||
merged[code] = quote;
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新上交所订阅
|
||||
const oldSseCodes = subscribedCodes.current.SSE;
|
||||
const sseToAdd = [...newSseCodes].filter(c => !oldSseCodes.has(c));
|
||||
@@ -684,13 +808,16 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已取消订阅的 quotes
|
||||
const allNewCodes = new Set([...newSseCodes, ...newSzseCodes]);
|
||||
// 清理已取消订阅的 quotes 和初始加载记录
|
||||
const allNewCodesSet = new Set(allNewCodes);
|
||||
setQuotes(prev => {
|
||||
const updated: QuotesMap = {};
|
||||
Object.keys(prev).forEach(code => {
|
||||
if (allNewCodes.has(code)) {
|
||||
if (allNewCodesSet.has(code)) {
|
||||
updated[code] = prev[code];
|
||||
} else {
|
||||
// 清理初始加载记录
|
||||
initialLoadedRef.current.delete(code);
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
|
||||
Reference in New Issue
Block a user