/** * useStockQuoteData - 股票行情数据获取 Hook * * 使用 /api/stock/{code}/quote-detail 接口获取完整行情数据 * 供 StockQuoteCard 内部使用 */ import { useState, useEffect, useCallback } from 'react'; import { logger } from '@utils/logger'; import axios from '@utils/axiosConfig'; import type { StockQuoteCardData } from '../types'; import type { BasicInfo } from '../../CompanyOverview/types'; /** * 将 API 响应数据转换为 StockQuoteCard 所需格式 */ const transformQuoteData = (apiData: any, stockCode: string): StockQuoteCardData | null => { if (!apiData) return null; return { // 基础信息 name: apiData.name || apiData.stock_name || '未知', code: apiData.code || apiData.stock_code || stockCode, indexTags: apiData.index_tags || apiData.indexTags || [], industry: apiData.industry || apiData.sw_industry_l2 || '', industryL1: apiData.industry_l1 || apiData.sw_industry_l1 || '', // 价格信息 currentPrice: apiData.current_price || apiData.currentPrice || apiData.close || 0, changePercent: apiData.change_percent || apiData.changePercent || apiData.pct_chg || 0, todayOpen: apiData.today_open || apiData.todayOpen || apiData.open || 0, yesterdayClose: apiData.yesterday_close || apiData.yesterdayClose || apiData.pre_close || 0, todayHigh: apiData.today_high || apiData.todayHigh || apiData.high || 0, todayLow: apiData.today_low || apiData.todayLow || apiData.low || 0, // 关键指标 pe: apiData.pe || apiData.pe_ttm || 0, eps: apiData.eps || apiData.basic_eps || undefined, pb: apiData.pb || apiData.pb_mrq || 0, marketCap: apiData.market_cap || apiData.marketCap || apiData.circ_mv || '0', totalShares: apiData.total_shares || apiData.totalShares || undefined, floatShares: apiData.float_shares || apiData.floatShares || undefined, week52Low: apiData.week52_low || apiData.week52Low || 0, week52High: apiData.week52_high || apiData.week52High || 0, // 主力动态 mainNetInflow: apiData.main_net_inflow || apiData.mainNetInflow || 0, institutionHolding: apiData.institution_holding || apiData.institutionHolding || 0, buyRatio: apiData.buy_ratio || apiData.buyRatio || 50, sellRatio: apiData.sell_ratio || apiData.sellRatio || 50, // 更新时间 updateTime: apiData.update_time || apiData.updateTime || new Date().toLocaleString(), }; }; interface UseStockQuoteDataResult { quoteData: StockQuoteCardData | null; basicInfo: BasicInfo | null; isLoading: boolean; error: string | null; refetch: () => void; } /** * 股票行情数据获取 Hook * 合并获取行情数据和基本信息 * * @param stockCode - 股票代码 */ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult => { const [quoteData, setQuoteData] = useState(null); const [basicInfo, setBasicInfo] = useState(null); const [quoteLoading, setQuoteLoading] = useState(false); const [basicLoading, setBasicLoading] = useState(false); const [error, setError] = useState(null); // 用于手动刷新的 ref const refetchRef = useCallback(async () => { if (!stockCode) return; // 标准化股票代码(去除后缀) const baseCode = stockCode.split('.')[0]; // 获取行情详情数据(使用新的 quote-detail 接口) setQuoteLoading(true); setError(null); try { logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode }); const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`); if (result.success && result.data) { const transformedData = transformQuoteData(result.data, stockCode); logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); setQuoteData(transformedData); } else { setError('获取行情数据失败'); setQuoteData(null); } } catch (err) { logger.error('useStockQuoteData', '获取行情失败', err); setError('获取行情数据失败'); setQuoteData(null); } finally { setQuoteLoading(false); } // 获取基本信息(公司简介等) setBasicLoading(true); try { const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`); if (result.success) { setBasicInfo(result.data); } } catch (err) { logger.error('useStockQuoteData', '获取基本信息失败', err); } finally { setBasicLoading(false); } }, [stockCode]); // stockCode 变化时重新获取数据(带取消支持) useEffect(() => { if (!stockCode) { setQuoteData(null); setBasicInfo(null); return; } const controller = new AbortController(); let isCancelled = false; // 标准化股票代码(去除后缀) const baseCode = stockCode.split('.')[0]; const fetchData = async () => { // 获取行情详情数据(使用新的 quote-detail 接口) setQuoteLoading(true); setError(null); try { logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode }); const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`, { signal: controller.signal, }); if (isCancelled) return; if (result.success && result.data) { const transformedData = transformQuoteData(result.data, stockCode); logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); setQuoteData(transformedData); } else { setError('获取行情数据失败'); setQuoteData(null); } } catch (err: any) { if (isCancelled || err.name === 'CanceledError') return; logger.error('useStockQuoteData', '获取行情失败', err); setError('获取行情数据失败'); setQuoteData(null); } finally { if (!isCancelled) setQuoteLoading(false); } // 获取基本信息(公司简介等) setBasicLoading(true); try { const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`, { signal: controller.signal, }); if (isCancelled) return; if (result.success) { setBasicInfo(result.data); } } catch (err: any) { if (isCancelled || err.name === 'CanceledError') return; logger.error('useStockQuoteData', '获取基本信息失败', err); } finally { if (!isCancelled) setBasicLoading(false); } }; fetchData(); return () => { isCancelled = true; controller.abort(); }; }, [stockCode]); return { quoteData, basicInfo, isLoading: quoteLoading || basicLoading, error, refetch: refetchRef, }; }; export default useStockQuoteData;