From 941f90054e086656705ea0b00bea29314550773e Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Tue, 3 Feb 2026 17:30:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=82=A1=E7=A5=A8?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E8=A1=8C=E6=83=85Hook=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DMarketDataView=E6=9E=84=E5=BB=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useStockRealtimeQuote Hook(API轮询方式获取实时行情) - 修复 MarketDataView 导入不存在的 @hooks/useRealtimeQuote 问题 - 交易时段内自动刷新股票价格 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/hooks/useStockRealtimeQuote.ts | 213 ++++++++++++++++++ .../components/MarketDataView/index.tsx | 14 +- 2 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 src/hooks/useStockRealtimeQuote.ts diff --git a/src/hooks/useStockRealtimeQuote.ts b/src/hooks/useStockRealtimeQuote.ts new file mode 100644 index 00000000..181da2df --- /dev/null +++ b/src/hooks/useStockRealtimeQuote.ts @@ -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 => { + 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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isTrading, setIsTrading] = useState(false); + + const intervalRef = useRef(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; diff --git a/src/views/Company/components/MarketDataView/index.tsx b/src/views/Company/components/MarketDataView/index.tsx index 3ff46add..7b0be878 100644 --- a/src/views/Company/components/MarketDataView/index.tsx +++ b/src/views/Company/components/MarketDataView/index.tsx @@ -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 = ({ 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) => {