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 [error, setError] = useState<string | null>(null);
// 是否首次加载
const isFirstLoad = useRef(true);
// 用 ref 追踪是否有数据(避免闭包问题)
const hasDataRef = useRef(false);
// 获取分钟数据
useEffect(() => {
if (!code) return;
const fetchData = async (): Promise<void> => {
setLoading(true);
setError(null);
// 只在首次加载时显示 loading 状态
if (isFirstLoad.current) {
setLoading(true);
}
try {
const apiPath = isIndex
@@ -81,23 +88,40 @@ const MiniTimelineChart: React.FC<MiniTimelineChartProps> = ({
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();
// 交易时间内每分钟刷新

View File

@@ -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<string>,
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,

View File

@@ -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<string, unknown>;
}
/** 深交所快照消息 */