diff --git a/src/views/Company/components/StockQuoteCard/hooks/index.ts b/src/views/Company/components/StockQuoteCard/hooks/index.ts new file mode 100644 index 00000000..ed671e79 --- /dev/null +++ b/src/views/Company/components/StockQuoteCard/hooks/index.ts @@ -0,0 +1,6 @@ +/** + * StockQuoteCard Hooks 导出索引 + */ + +export { useStockQuoteData } from './useStockQuoteData'; +export { useStockCompare } from './useStockCompare'; diff --git a/src/views/Company/components/StockQuoteCard/hooks/useStockCompare.ts b/src/views/Company/components/StockQuoteCard/hooks/useStockCompare.ts new file mode 100644 index 00000000..e9260626 --- /dev/null +++ b/src/views/Company/components/StockQuoteCard/hooks/useStockCompare.ts @@ -0,0 +1,91 @@ +/** + * useStockCompare - 股票对比逻辑 Hook + * + * 管理股票对比所需的数据获取和状态 + */ + +import { useState, useEffect, useCallback } from 'react'; +import { useToast } from '@chakra-ui/react'; +import { financialService } from '@services/financialService'; +import { logger } from '@utils/logger'; +import type { StockInfo } from '../../FinancialPanorama/types'; + +interface UseStockCompareResult { + currentStockInfo: StockInfo | null; + compareStockInfo: StockInfo | null; + isCompareLoading: boolean; + handleCompare: (compareCode: string) => Promise; + clearCompare: () => void; +} + +/** + * 股票对比 Hook + * + * @param stockCode - 当前股票代码 + */ +export const useStockCompare = (stockCode?: string): UseStockCompareResult => { + const toast = useToast(); + const [currentStockInfo, setCurrentStockInfo] = useState(null); + const [compareStockInfo, setCompareStockInfo] = useState(null); + const [isCompareLoading, setIsCompareLoading] = useState(false); + + // 加载当前股票财务信息(用于对比) + useEffect(() => { + const loadCurrentStockInfo = async () => { + if (!stockCode) { + setCurrentStockInfo(null); + return; + } + + try { + const res = await financialService.getStockInfo(stockCode); + setCurrentStockInfo(res.data); + } catch (error) { + logger.error('useStockCompare', 'loadCurrentStockInfo', error, { stockCode }); + } + }; + + loadCurrentStockInfo(); + // 股票代码变化时清除对比数据 + setCompareStockInfo(null); + }, [stockCode]); + + // 处理股票对比 + const handleCompare = useCallback(async (compareCode: string) => { + if (!compareCode) return; + + logger.debug('useStockCompare', '开始加载对比数据', { stockCode, compareCode }); + setIsCompareLoading(true); + + try { + const res = await financialService.getStockInfo(compareCode); + setCompareStockInfo(res.data); + logger.info('useStockCompare', '对比数据加载成功', { stockCode, compareCode }); + } catch (error) { + logger.error('useStockCompare', 'handleCompare', error, { stockCode, compareCode }); + toast({ + title: '加载对比数据失败', + description: '请检查股票代码是否正确', + status: 'error', + duration: 3000, + }); + } finally { + setIsCompareLoading(false); + } + }, [stockCode, toast]); + + // 清除对比数据 + const clearCompare = useCallback(() => { + setCompareStockInfo(null); + }, []); + + return { + currentStockInfo, + compareStockInfo, + isCompareLoading, + handleCompare, + clearCompare, + }; +}; + +export default useStockCompare; diff --git a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts new file mode 100644 index 00000000..bdf5bef3 --- /dev/null +++ b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts @@ -0,0 +1,152 @@ +/** + * useStockQuoteData - 股票行情数据获取 Hook + * + * 合并获取行情数据和基本信息,供 StockQuoteCard 内部使用 + */ + +import { useState, useEffect, useCallback } from 'react'; +import { stockService } from '@services/eventService'; +import { logger } from '@utils/logger'; +import { getApiBase } from '@utils/apiConfig'; +import type { StockQuoteCardData } from '../types'; +import type { BasicInfo } from '../../CompanyOverview/types'; + +const API_BASE_URL = getApiBase(); + +/** + * 将 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', + 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); + + // 获取行情数据 + const fetchQuote = useCallback(async () => { + if (!stockCode) { + setQuoteData(null); + return; + } + + setQuoteLoading(true); + setError(null); + + try { + logger.debug('useStockQuoteData', '获取股票行情', { stockCode }); + const quotes = await stockService.getQuotes([stockCode]); + + // API 返回格式: { [stockCode]: quoteData } + const quoteResult = quotes?.[stockCode] || quotes; + const transformedData = transformQuoteData(quoteResult, stockCode); + + logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData }); + setQuoteData(transformedData); + } catch (err) { + logger.error('useStockQuoteData', '获取行情失败', err); + setError('获取行情数据失败'); + setQuoteData(null); + } finally { + setQuoteLoading(false); + } + }, [stockCode]); + + // 获取基本信息 + const fetchBasicInfo = useCallback(async () => { + if (!stockCode) { + setBasicInfo(null); + return; + } + + setBasicLoading(true); + + try { + const response = await fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`); + const result = await response.json(); + + if (result.success) { + setBasicInfo(result.data); + } + } catch (err) { + logger.error('useStockQuoteData', '获取基本信息失败', err); + // 基本信息获取失败不影响主流程,只记录日志 + } finally { + setBasicLoading(false); + } + }, [stockCode]); + + // stockCode 变化时重新获取数据 + useEffect(() => { + fetchQuote(); + fetchBasicInfo(); + }, [fetchQuote, fetchBasicInfo]); + + // 手动刷新 + const refetch = useCallback(() => { + fetchQuote(); + fetchBasicInfo(); + }, [fetchQuote, fetchBasicInfo]); + + return { + quoteData, + basicInfo, + isLoading: quoteLoading || basicLoading, + error, + refetch, + }; +}; + +export default useStockQuoteData;