/** * 财务数据加载 Hook * 封装所有财务数据的加载逻辑,支持按 Tab 独立刷新 */ import { useState, useEffect, useCallback, useRef } from 'react'; import { useToast } from '@chakra-ui/react'; import axios from 'axios'; import { logger } from '@utils/logger'; import { financialService } from '@services/financialService'; import type { StockInfo, BalanceSheetData, IncomeStatementData, CashflowData, FinancialMetricsData, MainBusinessData, ComparisonData, } from '../types'; // 判断是否为取消请求的错误 const isCancelError = (error: unknown): boolean => { return axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError'); }; // Tab key 到数据类型的映射 export type DataTypeKey = | 'balance' | 'income' | 'cashflow' | 'profitability' | 'perShare' | 'growth' | 'operational' | 'solvency' | 'expense' | 'cashflowMetrics'; interface UseFinancialDataOptions { stockCode?: string; periods?: number; } interface UseFinancialDataReturn { // 数据状态 stockInfo: StockInfo | null; balanceSheet: BalanceSheetData[]; incomeStatement: IncomeStatementData[]; cashflow: CashflowData[]; financialMetrics: FinancialMetricsData[]; mainBusiness: MainBusinessData | null; comparison: ComparisonData[]; // 加载状态 loading: boolean; loadingTab: DataTypeKey | null; // 当前正在加载的 Tab error: string | null; // 操作方法 refetch: () => Promise; refetchByTab: (tabKey: DataTypeKey) => Promise; setStockCode: (code: string) => void; setSelectedPeriods: (periods: number) => void; setActiveTab: (tabKey: DataTypeKey) => void; handleTabChange: (tabKey: DataTypeKey) => void; // Tab 切换时自动检查期数 // 当前参数 currentStockCode: string; selectedPeriods: number; activeTab: DataTypeKey; } /** * 财务数据加载 Hook * @param options - 配置选项 * @returns 财务数据和操作方法 */ export const useFinancialData = ( options: UseFinancialDataOptions = {} ): UseFinancialDataReturn => { const { stockCode: initialStockCode = '600000', periods: initialPeriods = 8 } = options; // 参数状态 const [stockCode, setStockCode] = useState(initialStockCode); const [selectedPeriods, setSelectedPeriodsState] = useState(initialPeriods); const [activeTab, setActiveTab] = useState('profitability'); // 加载状态 const [loading, setLoading] = useState(false); const [loadingTab, setLoadingTab] = useState(null); const [error, setError] = useState(null); // 财务数据状态 const [stockInfo, setStockInfo] = useState(null); const [balanceSheet, setBalanceSheet] = useState([]); const [incomeStatement, setIncomeStatement] = useState([]); const [cashflow, setCashflow] = useState([]); const [financialMetrics, setFinancialMetrics] = useState([]); const [mainBusiness, setMainBusiness] = useState(null); const [comparison, setComparison] = useState([]); const toast = useToast(); const isInitialLoad = useRef(true); const prevPeriods = useRef(selectedPeriods); // AbortController refs - 用于取消请求 const coreDataControllerRef = useRef(null); const tabDataControllerRef = useRef(null); // 记录每种数据类型加载时使用的期数(用于 Tab 切换时判断是否需要重新加载) const dataPeriodsRef = useRef>({ balance: 0, income: 0, cashflow: 0, metrics: 0, }); // 判断 Tab key 对应的数据类型 const getDataTypeForTab = (tabKey: DataTypeKey): 'balance' | 'income' | 'cashflow' | 'metrics' => { switch (tabKey) { case 'balance': return 'balance'; case 'income': return 'income'; case 'cashflow': return 'cashflow'; default: // 所有财务指标类 tab 都使用 metrics 数据 return 'metrics'; } }; // 按数据类型加载数据 const loadDataByType = useCallback(async ( dataType: 'balance' | 'income' | 'cashflow' | 'metrics', periods: number, signal?: AbortSignal ) => { const options: { signal?: AbortSignal } = signal ? { signal } : {}; try { switch (dataType) { case 'balance': { const res = await financialService.getBalanceSheet(stockCode, periods, options); if (res.success) { setBalanceSheet(res.data); dataPeriodsRef.current.balance = periods; } break; } case 'income': { const res = await financialService.getIncomeStatement(stockCode, periods, options); if (res.success) { setIncomeStatement(res.data); dataPeriodsRef.current.income = periods; } break; } case 'cashflow': { const res = await financialService.getCashflow(stockCode, periods, options); if (res.success) { setCashflow(res.data); dataPeriodsRef.current.cashflow = periods; } break; } case 'metrics': { const res = await financialService.getFinancialMetrics(stockCode, periods, options); if (res.success) { setFinancialMetrics(res.data); dataPeriodsRef.current.metrics = periods; } break; } } } catch (err) { // 取消请求不作为错误处理 if (isCancelError(err)) return; logger.error('useFinancialData', 'loadDataByType', err, { dataType, periods }); throw err; } }, [stockCode]); // 按 Tab 刷新数据 const refetchByTab = useCallback(async (tabKey: DataTypeKey) => { if (!stockCode || stockCode.length !== 6) { return; } // 取消之前的 Tab 数据请求 tabDataControllerRef.current?.abort(); const controller = new AbortController(); tabDataControllerRef.current = controller; const dataType = getDataTypeForTab(tabKey); logger.debug('useFinancialData', '刷新单个 Tab 数据', { tabKey, dataType, selectedPeriods }); setLoadingTab(tabKey); setError(null); try { await loadDataByType(dataType, selectedPeriods, controller.signal); logger.info('useFinancialData', `${tabKey} 数据刷新成功`); } catch (err) { // 取消请求不作为错误处理 if (isCancelError(err)) return; const errorMessage = err instanceof Error ? err.message : '未知错误'; setError(errorMessage); } finally { // 只有当前请求没有被取消时才设置 loading 状态 if (!controller.signal.aborted) { setLoadingTab(null); } } }, [stockCode, selectedPeriods, loadDataByType]); // 设置期数(只刷新当前 Tab) const setSelectedPeriods = useCallback((periods: number) => { setSelectedPeriodsState(periods); }, []); // Tab 切换处理:检查期数是否需要重新加载 const handleTabChange = useCallback((tabKey: DataTypeKey) => { setActiveTab(tabKey); const dataType = getDataTypeForTab(tabKey); // 如果该 Tab 数据的期数与当前选择的期数不一致,重新加载 // 注:refetchByTab 使用传入的 tabKey,不依赖 activeTab 状态,无需延迟 if (dataPeriodsRef.current[dataType] !== selectedPeriods) { refetchByTab(tabKey); } }, [selectedPeriods, refetchByTab]); // 加载核心财务数据(初始加载:stockInfo + metrics + comparison) const loadCoreFinancialData = useCallback(async () => { if (!stockCode || stockCode.length !== 6) { logger.warn('useFinancialData', '无效的股票代码', { stockCode }); toast({ title: '请输入有效的6位股票代码', status: 'warning', duration: 3000, }); return; } // 取消之前的核心数据请求 coreDataControllerRef.current?.abort(); const controller = new AbortController(); coreDataControllerRef.current = controller; const options = { signal: controller.signal }; logger.debug('useFinancialData', '开始加载核心财务数据', { stockCode, selectedPeriods }); setLoading(true); setError(null); try { // 只加载核心数据(概览面板需要的) const [ stockInfoRes, metricsRes, comparisonRes, businessRes, ] = await Promise.all([ financialService.getStockInfo(stockCode, options), financialService.getFinancialMetrics(stockCode, selectedPeriods, options), financialService.getPeriodComparison(stockCode, selectedPeriods, options), financialService.getMainBusiness(stockCode, 4, options), ]); // 设置数据 if (stockInfoRes.success) setStockInfo(stockInfoRes.data); if (metricsRes.success) { setFinancialMetrics(metricsRes.data); dataPeriodsRef.current.metrics = selectedPeriods; } if (comparisonRes.success) setComparison(comparisonRes.data); if (businessRes.success) setMainBusiness(businessRes.data); logger.info('useFinancialData', '核心财务数据加载成功', { stockCode }); } catch (err) { // 取消请求不作为错误处理 if (isCancelError(err)) return; const errorMessage = err instanceof Error ? err.message : '未知错误'; setError(errorMessage); logger.error('useFinancialData', 'loadCoreFinancialData', err, { stockCode, selectedPeriods }); } finally { // 只有当前请求没有被取消时才设置 loading 状态 if (!controller.signal.aborted) { setLoading(false); } } }, [stockCode, selectedPeriods, toast]); // 加载所有财务数据(用于刷新) const loadAllFinancialData = useCallback(async () => { await loadCoreFinancialData(); }, [loadCoreFinancialData]); // 监听 props 中的 stockCode 变化 useEffect(() => { if (initialStockCode && initialStockCode !== stockCode) { setStockCode(initialStockCode); } }, [initialStockCode]); // 初始加载(仅股票代码变化时全量加载) useEffect(() => { if (stockCode) { // 立即清空所有旧数据,触发骨架屏 setStockInfo(null); setBalanceSheet([]); setIncomeStatement([]); setCashflow([]); setFinancialMetrics([]); setMainBusiness(null); setComparison([]); // 重置期数记录 dataPeriodsRef.current = { balance: 0, income: 0, cashflow: 0, metrics: 0, }; loadAllFinancialData(); isInitialLoad.current = false; } }, [stockCode]); // 注意:这里只依赖 stockCode // 期数变化时只刷新当前 Tab useEffect(() => { if (!isInitialLoad.current && prevPeriods.current !== selectedPeriods) { prevPeriods.current = selectedPeriods; refetchByTab(activeTab); } }, [selectedPeriods, activeTab, refetchByTab]); // 组件卸载时取消所有进行中的请求 useEffect(() => { return () => { coreDataControllerRef.current?.abort(); tabDataControllerRef.current?.abort(); }; }, []); return { // 数据状态 stockInfo, balanceSheet, incomeStatement, cashflow, financialMetrics, mainBusiness, comparison, // 加载状态 loading, loadingTab, error, // 操作方法 refetch: loadAllFinancialData, refetchByTab, setStockCode, setSelectedPeriods, setActiveTab, handleTabChange, // 当前参数 currentStockCode: stockCode, selectedPeriods, activeTab, }; }; export default useFinancialData;