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:
2026-02-03 17:30:39 +08:00
parent 49597b97f3
commit 941f90054e
2 changed files with 216 additions and 11 deletions

View 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 - 刷新间隔(毫秒),默认 600001分钟
* @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;

View File

@@ -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) => {