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';
|
import type { SubTabConfig } from '@components/SubTabContainer';
|
||||||
|
|
||||||
// 实时行情 Hook
|
// 实时行情 Hook
|
||||||
import { useRealtimeQuote } from '@hooks/useRealtimeQuote';
|
import { useStockRealtimeQuote } from '@hooks/useStockRealtimeQuote';
|
||||||
|
|
||||||
// 内部模块导入
|
// 内部模块导入
|
||||||
import { themes, DEFAULT_PERIOD } from './constants';
|
import { themes, DEFAULT_PERIOD } from './constants';
|
||||||
@@ -70,16 +70,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
|||||||
loadDataByType,
|
loadDataByType,
|
||||||
} = useMarketData(stockCode, selectedPeriod);
|
} = useMarketData(stockCode, selectedPeriod);
|
||||||
|
|
||||||
// 获取实时行情数据
|
// 获取实时行情数据(通过 API 轮询,交易时段内自动刷新)
|
||||||
const subscribedCodes = useMemo(() => {
|
const { quote: realtimeQuote } = useStockRealtimeQuote(stockCode);
|
||||||
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;
|
|
||||||
|
|
||||||
// Tab 切换时按需加载数据
|
// Tab 切换时按需加载数据
|
||||||
const handleTabChange = useCallback((index: number) => {
|
const handleTabChange = useCallback((index: number) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user