refactor(FinancialPanorama): 重构为 7+3 Tab 架构
- 财务指标拆分为 7 个分类 Tab(盈利/每股/成长/运营/偿债/费用/现金流) - 保留 3 大报表 Tab(资产负债表/利润表/现金流量表) - 新增 KeyMetricsOverview 关键指标速览组件 - 新增 FinancialTable 通用表格组件 - Hook 支持按 Tab 独立刷新数据 - PeriodSelector 整合到 SubTabContainer 右侧 - 删除废弃的 OverviewTab/MainBusinessTab 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,4 +3,5 @@
|
||||
*/
|
||||
|
||||
export { useFinancialData } from './useFinancialData';
|
||||
export type { DataTypeKey } from './useFinancialData';
|
||||
export type { default as UseFinancialDataReturn } from './useFinancialData';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 财务数据加载 Hook
|
||||
* 封装所有财务数据的加载逻辑
|
||||
* 封装所有财务数据的加载逻辑,支持按 Tab 独立刷新
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { logger } from '@utils/logger';
|
||||
import { financialService } from '@services/financialService';
|
||||
@@ -19,6 +19,19 @@ import type {
|
||||
ComparisonData,
|
||||
} from '../types';
|
||||
|
||||
// Tab key 到数据类型的映射
|
||||
export type DataTypeKey =
|
||||
| 'balance'
|
||||
| 'income'
|
||||
| 'cashflow'
|
||||
| 'profitability'
|
||||
| 'perShare'
|
||||
| 'growth'
|
||||
| 'operational'
|
||||
| 'solvency'
|
||||
| 'expense'
|
||||
| 'cashflowMetrics';
|
||||
|
||||
interface UseFinancialDataOptions {
|
||||
stockCode?: string;
|
||||
periods?: number;
|
||||
@@ -38,16 +51,20 @@ interface UseFinancialDataReturn {
|
||||
|
||||
// 加载状态
|
||||
loading: boolean;
|
||||
loadingTab: DataTypeKey | null; // 当前正在加载的 Tab
|
||||
error: string | null;
|
||||
|
||||
// 操作方法
|
||||
refetch: () => Promise<void>;
|
||||
refetchByTab: (tabKey: DataTypeKey) => Promise<void>;
|
||||
setStockCode: (code: string) => void;
|
||||
setSelectedPeriods: (periods: number) => void;
|
||||
setActiveTab: (tabKey: DataTypeKey) => void;
|
||||
|
||||
// 当前参数
|
||||
currentStockCode: string;
|
||||
selectedPeriods: number;
|
||||
activeTab: DataTypeKey;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,10 +79,12 @@ export const useFinancialData = (
|
||||
|
||||
// 参数状态
|
||||
const [stockCode, setStockCode] = useState(initialStockCode);
|
||||
const [selectedPeriods, setSelectedPeriods] = useState(initialPeriods);
|
||||
const [selectedPeriods, setSelectedPeriodsState] = useState(initialPeriods);
|
||||
const [activeTab, setActiveTab] = useState<DataTypeKey>('profitability');
|
||||
|
||||
// 加载状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingTab, setLoadingTab] = useState<DataTypeKey | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 财务数据状态
|
||||
@@ -80,9 +99,88 @@ export const useFinancialData = (
|
||||
const [comparison, setComparison] = useState<ComparisonData[]>([]);
|
||||
|
||||
const toast = useToast();
|
||||
const isInitialLoad = useRef(true);
|
||||
const prevPeriods = useRef(selectedPeriods);
|
||||
|
||||
// 加载所有财务数据
|
||||
const loadFinancialData = useCallback(async () => {
|
||||
// 判断 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
|
||||
) => {
|
||||
try {
|
||||
switch (dataType) {
|
||||
case 'balance': {
|
||||
const res = await financialService.getBalanceSheet(stockCode, periods);
|
||||
if (res.success) setBalanceSheet(res.data);
|
||||
break;
|
||||
}
|
||||
case 'income': {
|
||||
const res = await financialService.getIncomeStatement(stockCode, periods);
|
||||
if (res.success) setIncomeStatement(res.data);
|
||||
break;
|
||||
}
|
||||
case 'cashflow': {
|
||||
const res = await financialService.getCashflow(stockCode, periods);
|
||||
if (res.success) setCashflow(res.data);
|
||||
break;
|
||||
}
|
||||
case 'metrics': {
|
||||
const res = await financialService.getFinancialMetrics(stockCode, periods);
|
||||
if (res.success) setFinancialMetrics(res.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('useFinancialData', 'loadDataByType', err, { dataType, periods });
|
||||
throw err;
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// 按 Tab 刷新数据
|
||||
const refetchByTab = useCallback(async (tabKey: DataTypeKey) => {
|
||||
if (!stockCode || stockCode.length !== 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataType = getDataTypeForTab(tabKey);
|
||||
logger.debug('useFinancialData', '刷新单个 Tab 数据', { tabKey, dataType, selectedPeriods });
|
||||
|
||||
setLoadingTab(tabKey);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await loadDataByType(dataType, selectedPeriods);
|
||||
logger.info('useFinancialData', `${tabKey} 数据刷新成功`);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '未知错误';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoadingTab(null);
|
||||
}
|
||||
}, [stockCode, selectedPeriods, loadDataByType]);
|
||||
|
||||
// 设置期数(只刷新当前 Tab)
|
||||
const setSelectedPeriods = useCallback((periods: number) => {
|
||||
setSelectedPeriodsState(periods);
|
||||
}, []);
|
||||
|
||||
// 加载所有财务数据(初始加载)
|
||||
const loadAllFinancialData = useCallback(async () => {
|
||||
if (!stockCode || stockCode.length !== 6) {
|
||||
logger.warn('useFinancialData', '无效的股票代码', { stockCode });
|
||||
toast({
|
||||
@@ -93,7 +191,7 @@ export const useFinancialData = (
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('useFinancialData', '开始加载财务数据', { stockCode, selectedPeriods });
|
||||
logger.debug('useFinancialData', '开始加载全部财务数据', { stockCode, selectedPeriods });
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -132,11 +230,11 @@ export const useFinancialData = (
|
||||
if (rankRes.success) setIndustryRank(rankRes.data);
|
||||
if (comparisonRes.success) setComparison(comparisonRes.data);
|
||||
|
||||
logger.info('useFinancialData', '财务数据加载成功', { stockCode });
|
||||
logger.info('useFinancialData', '全部财务数据加载成功', { stockCode });
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '未知错误';
|
||||
setError(errorMessage);
|
||||
logger.error('useFinancialData', 'loadFinancialData', err, { stockCode, selectedPeriods });
|
||||
logger.error('useFinancialData', 'loadAllFinancialData', err, { stockCode, selectedPeriods });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -149,12 +247,21 @@ export const useFinancialData = (
|
||||
}
|
||||
}, [initialStockCode]);
|
||||
|
||||
// 初始加载和参数变化时重新加载
|
||||
// 初始加载(仅股票代码变化时全量加载)
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
loadFinancialData();
|
||||
loadAllFinancialData();
|
||||
isInitialLoad.current = false;
|
||||
}
|
||||
}, [stockCode, selectedPeriods, loadFinancialData]);
|
||||
}, [stockCode]); // 注意:这里只依赖 stockCode
|
||||
|
||||
// 期数变化时只刷新当前 Tab
|
||||
useEffect(() => {
|
||||
if (!isInitialLoad.current && prevPeriods.current !== selectedPeriods) {
|
||||
prevPeriods.current = selectedPeriods;
|
||||
refetchByTab(activeTab);
|
||||
}
|
||||
}, [selectedPeriods, activeTab, refetchByTab]);
|
||||
|
||||
return {
|
||||
// 数据状态
|
||||
@@ -170,16 +277,20 @@ export const useFinancialData = (
|
||||
|
||||
// 加载状态
|
||||
loading,
|
||||
loadingTab,
|
||||
error,
|
||||
|
||||
// 操作方法
|
||||
refetch: loadFinancialData,
|
||||
refetch: loadAllFinancialData,
|
||||
refetchByTab,
|
||||
setStockCode,
|
||||
setSelectedPeriods,
|
||||
setActiveTab,
|
||||
|
||||
// 当前参数
|
||||
currentStockCode: stockCode,
|
||||
selectedPeriods,
|
||||
activeTab,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user