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:
zdl
2025-12-16 19:59:16 +08:00
parent dde78d8a04
commit ccb752419e
20 changed files with 2414 additions and 1082 deletions

View File

@@ -3,4 +3,5 @@
*/
export { useFinancialData } from './useFinancialData';
export type { DataTypeKey } from './useFinancialData';
export type { default as UseFinancialDataReturn } from './useFinancialData';

View File

@@ -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,
};
};