update pay ui
This commit is contained in:
@@ -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();
|
||||
|
||||
// 交易时间内每分钟刷新
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
/** 深交所快照消息 */
|
||||
|
||||
Reference in New Issue
Block a user