update pay ui
This commit is contained in:
@@ -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> => {
|
||||||
setLoading(true);
|
// 只在首次加载时显示 loading 状态
|
||||||
setError(null);
|
if (isFirstLoad.current) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
setError(result.error || '暂无数据');
|
// 只有在没有原有数据时才设置错误(保留原有数据)
|
||||||
|
if (!hasDataRef.current) {
|
||||||
|
setError(result.error || '暂无数据');
|
||||||
|
}
|
||||||
|
// 有原有数据时,静默失败,保持显示原有数据
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('加载失败');
|
// 只有在没有原有数据时才设置错误(保留原有数据)
|
||||||
|
if (!hasDataRef.current) {
|
||||||
|
setError('加载失败');
|
||||||
|
}
|
||||||
|
// 有原有数据时,静默失败,保持显示原有数据
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
isFirstLoad.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 重置首次加载标记(code 变化时)
|
||||||
|
isFirstLoad.current = true;
|
||||||
|
hasDataRef.current = false;
|
||||||
|
setTimelineData([]); // 切换股票时清空数据
|
||||||
|
setError(null);
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
||||||
// 交易时间内每分钟刷新
|
// 交易时间内每分钟刷新
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|
||||||
updated[fullCode] = {
|
if (isIndexType) {
|
||||||
code: fullCode,
|
// 指数数据格式
|
||||||
name: prevQuotes[fullCode]?.name || '',
|
const prevClose = quoteData.prev_close ?? 0;
|
||||||
price: stockData.last_px,
|
const currentIndex = quoteData.current_index ?? quoteData.last_px ?? 0;
|
||||||
prevClose,
|
|
||||||
open: stockData.open_px,
|
updated[fullCode] = {
|
||||||
high: stockData.high_px,
|
code: fullCode,
|
||||||
low: stockData.low_px,
|
name: prevQuotes[fullCode]?.name || '',
|
||||||
volume,
|
price: currentIndex,
|
||||||
amount,
|
prevClose,
|
||||||
numTrades: stockData.num_trades,
|
open: quoteData.open_index ?? quoteData.open_px ?? 0,
|
||||||
upperLimit,
|
high: quoteData.high_index ?? quoteData.high_px ?? 0,
|
||||||
lowerLimit,
|
low: quoteData.low_index ?? quoteData.low_px ?? 0,
|
||||||
change: stockData.last_px - prevClose,
|
close: quoteData.close_index,
|
||||||
changePct: calcChangePct(stockData.last_px, prevClose),
|
volume: quoteData.volume ?? 0,
|
||||||
bidPrices,
|
amount: quoteData.amount ?? 0,
|
||||||
bidVolumes,
|
numTrades: quoteData.num_trades,
|
||||||
askPrices,
|
change: currentIndex - prevClose,
|
||||||
askVolumes,
|
changePct: calcChangePct(currentIndex, prevClose),
|
||||||
tradingPhase,
|
bidPrices: [],
|
||||||
updateTime: stockData.update_time || timestamp,
|
bidVolumes: [],
|
||||||
exchange: 'SZSE',
|
askPrices: [],
|
||||||
} as QuoteData;
|
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;
|
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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 深交所快照消息 */
|
/** 深交所快照消息 */
|
||||||
|
|||||||
Reference in New Issue
Block a user