feat: 添加股票实时行情Hook,修复MarketDataView构建错误
- 新增 useStockRealtimeQuote Hook(API轮询方式获取实时行情) - 修复 MarketDataView 导入不存在的 @hooks/useRealtimeQuote 问题 - 交易时段内自动刷新股票价格 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
213
src/hooks/useStockRealtimeQuote.ts
Normal file
213
src/hooks/useStockRealtimeQuote.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 股票实时行情 Hook(简化版)
|
||||
* 通过 API 轮询获取单只股票的实时行情
|
||||
* 用于 MarketDataView 等需要显示实时价格的组件
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
|
||||
// 交易时段
|
||||
const TRADING_SESSIONS = [
|
||||
{ start: { hour: 9, minute: 30 }, end: { hour: 11, minute: 30 } },
|
||||
{ start: { hour: 13, minute: 0 }, end: { hour: 15, minute: 0 } },
|
||||
];
|
||||
|
||||
/**
|
||||
* 判断当前时间是否在交易时段内
|
||||
*/
|
||||
const isInTradingSession = (): boolean => {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
// 周末不交易
|
||||
if (day === 0 || day === 6) return false;
|
||||
|
||||
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
return TRADING_SESSIONS.some(session => {
|
||||
const startMinutes = session.start.hour * 60 + session.start.minute;
|
||||
const endMinutes = session.end.hour * 60 + session.end.minute;
|
||||
return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
|
||||
});
|
||||
};
|
||||
|
||||
export interface StockQuoteData {
|
||||
code: string;
|
||||
name?: string;
|
||||
price: number;
|
||||
prevClose: number;
|
||||
open?: number;
|
||||
high?: number;
|
||||
low?: number;
|
||||
volume?: number;
|
||||
amount?: number;
|
||||
change: number;
|
||||
changePct: number;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
export interface UseStockRealtimeQuoteReturn {
|
||||
quote: StockQuoteData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
isTrading: boolean;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取股票实时行情
|
||||
*/
|
||||
const fetchStockQuote = async (stockCode: string): Promise<StockQuoteData | null> => {
|
||||
try {
|
||||
// 确保股票代码带后缀
|
||||
let code = stockCode;
|
||||
if (!code.includes('.')) {
|
||||
if (code.startsWith('6') || code.startsWith('5')) {
|
||||
code = `${code}.SH`;
|
||||
} else {
|
||||
code = `${code}.SZ`;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${getApiBase()}/api/stock/${code}/realtime`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.data) {
|
||||
const data = result.data;
|
||||
return {
|
||||
code: data.code || code,
|
||||
name: data.name,
|
||||
price: data.price || data.last_px || data.close || 0,
|
||||
prevClose: data.prev_close || data.prev_close_px || 0,
|
||||
open: data.open || data.open_px,
|
||||
high: data.high || data.high_px,
|
||||
low: data.low || data.low_px,
|
||||
volume: data.volume || data.total_volume_trade,
|
||||
amount: data.amount || data.total_value_trade,
|
||||
change: data.change || (data.price - data.prev_close) || 0,
|
||||
changePct: data.change_pct || data.changePct || 0,
|
||||
updateTime: data.update_time || data.updateTime,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取股票实时行情失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 股票实时行情 Hook
|
||||
*
|
||||
* @param stockCode - 股票代码,如 '600000' 或 '600000.SH'
|
||||
* @param options - 配置选项
|
||||
* @param options.refreshInterval - 刷新间隔(毫秒),默认 60000(1分钟)
|
||||
* @param options.autoRefresh - 是否自动刷新,默认 true
|
||||
*
|
||||
* @returns { quote, loading, error, isTrading, refresh }
|
||||
*/
|
||||
export const useStockRealtimeQuote = (
|
||||
stockCode: string | undefined,
|
||||
options: { refreshInterval?: number; autoRefresh?: boolean } = {}
|
||||
): UseStockRealtimeQuoteReturn => {
|
||||
const { refreshInterval = 60000, autoRefresh = true } = options;
|
||||
|
||||
const [quote, setQuote] = useState<StockQuoteData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isTrading, setIsTrading] = useState(false);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// 加载数据
|
||||
const loadQuote = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
try {
|
||||
const data = await fetchStockQuote(stockCode);
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (data) {
|
||||
setQuote(data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError('无法获取行情数据');
|
||||
}
|
||||
} catch (err) {
|
||||
if (isMountedRef.current) {
|
||||
setError(err instanceof Error ? err.message : '未知错误');
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// 手动刷新
|
||||
const refresh = useCallback(() => {
|
||||
setLoading(true);
|
||||
loadQuote();
|
||||
}, [loadQuote]);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
if (stockCode) {
|
||||
loadQuote();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, [loadQuote, stockCode]);
|
||||
|
||||
// 自动刷新逻辑
|
||||
useEffect(() => {
|
||||
if (!autoRefresh || !stockCode) return;
|
||||
|
||||
// 清除旧的定时器
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
// 设置定时器,检查是否在交易时间内
|
||||
const checkAndRefresh = () => {
|
||||
const inSession = isInTradingSession();
|
||||
setIsTrading(inSession);
|
||||
|
||||
if (inSession) {
|
||||
loadQuote();
|
||||
}
|
||||
};
|
||||
|
||||
// 立即检查一次
|
||||
checkAndRefresh();
|
||||
|
||||
// 设置定时刷新
|
||||
intervalRef.current = setInterval(checkAndRefresh, refreshInterval);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [autoRefresh, stockCode, refreshInterval, loadQuote]);
|
||||
|
||||
return {
|
||||
quote,
|
||||
loading,
|
||||
error,
|
||||
isTrading,
|
||||
refresh,
|
||||
};
|
||||
};
|
||||
|
||||
export default useStockRealtimeQuote;
|
||||
@@ -17,7 +17,7 @@ import SubTabContainer from '@components/SubTabContainer';
|
||||
import type { SubTabConfig } from '@components/SubTabContainer';
|
||||
|
||||
// 实时行情 Hook
|
||||
import { useRealtimeQuote } from '@hooks/useRealtimeQuote';
|
||||
import { useStockRealtimeQuote } from '@hooks/useStockRealtimeQuote';
|
||||
|
||||
// 内部模块导入
|
||||
import { themes, DEFAULT_PERIOD } from './constants';
|
||||
@@ -70,16 +70,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
||||
loadDataByType,
|
||||
} = useMarketData(stockCode, selectedPeriod);
|
||||
|
||||
// 获取实时行情数据
|
||||
const subscribedCodes = useMemo(() => {
|
||||
if (!stockCode) return [];
|
||||
const baseCode = stockCode.split('.')[0];
|
||||
const isShanghai = baseCode.startsWith('6') || baseCode.startsWith('5');
|
||||
return [isShanghai ? `${baseCode}.SH` : `${baseCode}.SZ`];
|
||||
}, [stockCode]);
|
||||
|
||||
const { quotes: realtimeQuotes } = useRealtimeQuote(subscribedCodes);
|
||||
const realtimeQuote = subscribedCodes.length > 0 ? realtimeQuotes[subscribedCodes[0]] : null;
|
||||
// 获取实时行情数据(通过 API 轮询,交易时段内自动刷新)
|
||||
const { quote: realtimeQuote } = useStockRealtimeQuote(stockCode);
|
||||
|
||||
// Tab 切换时按需加载数据
|
||||
const handleTabChange = useCallback((index: number) => {
|
||||
|
||||
Reference in New Issue
Block a user