feat(FinancialPanorama): SubTabContainer 支持分组,默认显示资产负债表

- SubTabContainer 新增 groups 属性支持 Tab 分组显示
- 财务全景 Tab 分为"基础报表"和"财务指标分析"两组
- 默认 Tab 改为资产负债表(基础报表第一个)
- 初始加载时并行请求资产负债表数据
- 表格操作列眼睛图标改为"详情"文字

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-29 16:44:57 +08:00
parent 8aedbf2bbb
commit 93af3b5b5b
4 changed files with 158 additions and 85 deletions

View File

@@ -50,6 +50,14 @@ export interface SubTabConfig {
fallback?: React.ReactNode; fallback?: React.ReactNode;
} }
/**
* Tab 分组配置
*/
export interface SubTabGroup {
name: string;
tabs: SubTabConfig[];
}
/** /**
* 深空 FUI 主题配置 * 深空 FUI 主题配置
*/ */
@@ -129,8 +137,10 @@ const THEME_PRESETS: Record<string, SubTabTheme> = {
}; };
export interface SubTabContainerProps { export interface SubTabContainerProps {
/** Tab 配置数组 */ /** Tab 配置数组(与 groups 二选一) */
tabs: SubTabConfig[]; tabs?: SubTabConfig[];
/** Tab 分组配置(与 tabs 二选一) */
groups?: SubTabGroup[];
/** 传递给 Tab 内容组件的 props */ /** 传递给 Tab 内容组件的 props */
componentProps?: Record<string, any>; componentProps?: Record<string, any>;
/** 默认选中的 Tab 索引 */ /** 默认选中的 Tab 索引 */
@@ -156,7 +166,8 @@ export interface SubTabContainerProps {
} }
const SubTabContainer: React.FC<SubTabContainerProps> = memo(({ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
tabs, tabs: tabsProp,
groups,
componentProps = {}, componentProps = {},
defaultIndex = 0, defaultIndex = 0,
index: controlledIndex, index: controlledIndex,
@@ -171,6 +182,21 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
}) => { }) => {
// 获取尺寸配置 // 获取尺寸配置
const sizeConfig = SIZE_CONFIG[size]; const sizeConfig = SIZE_CONFIG[size];
// 将分组展平为 tabs 数组,同时保留分组信息用于渲染分隔符
const { tabs, groupBoundaries } = React.useMemo(() => {
if (groups && groups.length > 0) {
const flatTabs: SubTabConfig[] = [];
const boundaries: number[] = []; // 记录每个分组的起始索引
groups.forEach((group) => {
boundaries.push(flatTabs.length);
flatTabs.push(...group.tabs);
});
return { tabs: flatTabs, groupBoundaries: boundaries };
}
return { tabs: tabsProp || [], groupBoundaries: [] };
}, [groups, tabsProp]);
// 内部状态(非受控模式) // 内部状态(非受控模式)
const [internalIndex, setInternalIndex] = useState(defaultIndex); const [internalIndex, setInternalIndex] = useState(defaultIndex);
@@ -280,10 +306,35 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
> >
{tabs.map((tab, idx) => { {tabs.map((tab, idx) => {
const isSelected = idx === currentIndex; const isSelected = idx === currentIndex;
// 检查是否需要在此 Tab 前显示分组标签
const groupIndex = groupBoundaries.indexOf(idx);
const showGroupLabel = groups && groupIndex !== -1;
return ( return (
<React.Fragment key={tab.key}>
{/* 分组标签 */}
{showGroupLabel && (
<HStack
spacing={2}
flexShrink={0}
pl={groupIndex > 0 ? 3 : 0}
pr={2}
borderLeft={groupIndex > 0 ? '1px solid' : 'none'}
borderColor={DEEP_SPACE.borderGold}
ml={groupIndex > 0 ? 2 : 0}
>
<Text
fontSize="xs"
color={DEEP_SPACE.textMuted}
fontWeight="500"
whiteSpace="nowrap"
letterSpacing="0.05em"
>
{groups[groupIndex].name}
</Text>
</HStack>
)}
<Tab <Tab
key={tab.key}
color={theme.tabUnselectedColor} color={theme.tabUnselectedColor}
borderRadius={DEEP_SPACE.radius} borderRadius={DEEP_SPACE.radius}
px={sizeConfig.px} px={sizeConfig.px}
@@ -338,6 +389,7 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
<Text>{tab.name}</Text> <Text>{tab.name}</Text>
</HStack> </HStack>
</Tab> </Tab>
</React.Fragment>
); );
})} })}
</TabList> </TabList>

View File

@@ -10,7 +10,6 @@ import React, { useMemo, memo } from 'react';
import { Box, Text, HStack, Badge as ChakraBadge, Button, Spinner, Center } from '@chakra-ui/react'; import { Box, Text, HStack, Badge as ChakraBadge, Button, Spinner, Center } from '@chakra-ui/react';
import { Table, ConfigProvider, Tooltip } from 'antd'; import { Table, ConfigProvider, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { Eye } from 'lucide-react';
import { formatUtils } from '@services/financialService'; import { formatUtils } from '@services/financialService';
import { BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY, getValueByPath, isNegativeIndicator } from '../utils'; import { BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY, getValueByPath, isNegativeIndicator } from '../utils';
import type { MetricConfig, MetricSectionConfig } from '../types'; import type { MetricConfig, MetricSectionConfig } from '../types';
@@ -308,15 +307,19 @@ const UnifiedFinancialTableInner: React.FC<UnifiedFinancialTableProps> = ({
if (type === 'metrics') { if (type === 'metrics') {
return ( return (
<Eye <Text
size={14} fontSize="xs"
color="#D4AF37" color="#D4AF37"
style={{ cursor: 'pointer', opacity: 0.7 }} cursor="pointer"
opacity={0.7}
_hover={{ opacity: 1 }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
showMetricChart(record.name, record.key, data, record.path); showMetricChart(record.name, record.key, data, record.path);
}} }}
/> >
</Text>
); );
} }

View File

@@ -83,7 +83,7 @@ export const useFinancialData = (
// 参数状态 // 参数状态
const [stockCode, setStockCode] = useState(initialStockCode); const [stockCode, setStockCode] = useState(initialStockCode);
const [selectedPeriods, setSelectedPeriodsState] = useState(initialPeriods); const [selectedPeriods, setSelectedPeriodsState] = useState(initialPeriods);
const [activeTab, setActiveTab] = useState<DataTypeKey>('profitability'); const [activeTab, setActiveTab] = useState<DataTypeKey>('balance');
// 加载状态 // 加载状态
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -252,19 +252,21 @@ export const useFinancialData = (
setError(null); setError(null);
try { try {
// 只加载核心数据(概览面板 + 归母净利润趋势图需要的) // 只加载核心数据(概览面板 + 归母净利润趋势图 + 默认Tab资产负债表需要的)
const [ const [
stockInfoRes, stockInfoRes,
metricsRes, metricsRes,
comparisonRes, comparisonRes,
businessRes, businessRes,
incomeRes, incomeRes,
balanceRes,
] = await Promise.all([ ] = await Promise.all([
financialService.getStockInfo(stockCode, options), financialService.getStockInfo(stockCode, options),
financialService.getFinancialMetrics(stockCode, selectedPeriods, options), financialService.getFinancialMetrics(stockCode, selectedPeriods, options),
financialService.getPeriodComparison(stockCode, selectedPeriods, options), financialService.getPeriodComparison(stockCode, selectedPeriods, options),
financialService.getMainBusiness(stockCode, 4, options), financialService.getMainBusiness(stockCode, 4, options),
financialService.getIncomeStatement(stockCode, selectedPeriods, options), financialService.getIncomeStatement(stockCode, selectedPeriods, options),
financialService.getBalanceSheet(stockCode, selectedPeriods, options),
]); ]);
// 设置数据 // 设置数据
@@ -279,6 +281,10 @@ export const useFinancialData = (
setIncomeStatement(incomeRes.data); setIncomeStatement(incomeRes.data);
dataPeriodsRef.current.income = selectedPeriods; dataPeriodsRef.current.income = selectedPeriods;
} }
if (balanceRes.success) {
setBalanceSheet(balanceRes.data);
dataPeriodsRef.current.balance = selectedPeriods;
}
logger.info('useFinancialData', '核心财务数据加载成功', { stockCode }); logger.info('useFinancialData', '核心财务数据加载成功', { stockCode });
} catch (err) { } catch (err) {

View File

@@ -27,7 +27,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
// 通用组件 // 通用组件
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer'; import SubTabContainer, { type SubTabConfig, type SubTabGroup } from '@components/SubTabContainer';
// 内部模块导入 // 内部模块导入
import { useFinancialData, type DataTypeKey } from './hooks'; import { useFinancialData, type DataTypeKey } from './hooks';
@@ -59,7 +59,13 @@ import type { FinancialPanoramaProps } from './types';
* 财务全景主组件 * 财务全景主组件
*/ */
// Tab key 映射表SubTabContainer index -> DataTypeKey // Tab key 映射表SubTabContainer index -> DataTypeKey
// 顺序基础报表3个 + 财务指标分析7个
const TAB_KEY_MAP: DataTypeKey[] = [ const TAB_KEY_MAP: DataTypeKey[] = [
// 基础报表
'balance',
'income',
'cashflow',
// 财务指标分析
'profitability', 'profitability',
'perShare', 'perShare',
'growth', 'growth',
@@ -67,9 +73,6 @@ const TAB_KEY_MAP: DataTypeKey[] = [
'solvency', 'solvency',
'expense', 'expense',
'cashflowMetrics', 'cashflowMetrics',
'balance',
'income',
'cashflow',
]; ];
const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propStockCode }) => { const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propStockCode }) => {
@@ -122,10 +125,20 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
onOpen(); onOpen();
}, [onOpen]); }, [onOpen]);
// Tab 配置 - 财务指标分类 + 三大财务报表 // Tab 分组配置 - 基础报表 + 财务指标分析
const tabConfigs: SubTabConfig[] = useMemo( const tabGroups: SubTabGroup[] = useMemo(
() => [ () => [
// 财务指标分类7个 {
name: '基础报表',
tabs: [
{ key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab },
{ key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab },
{ key: 'cashflow', name: '现金流量表', icon: TrendingDown, component: CashflowTab },
],
},
{
name: '财务指标分析',
tabs: [
{ key: 'profitability', name: '盈利能力', icon: PieChart, component: ProfitabilityTab }, { key: 'profitability', name: '盈利能力', icon: PieChart, component: ProfitabilityTab },
{ key: 'perShare', name: '每股指标', icon: Percent, component: PerShareTab }, { key: 'perShare', name: '每股指标', icon: Percent, component: PerShareTab },
{ key: 'growth', name: '成长能力', icon: TrendingUp, component: GrowthTab }, { key: 'growth', name: '成长能力', icon: TrendingUp, component: GrowthTab },
@@ -133,10 +146,8 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
{ key: 'solvency', name: '偿债能力', icon: Shield, component: SolvencyTab }, { key: 'solvency', name: '偿债能力', icon: Shield, component: SolvencyTab },
{ key: 'expense', name: '费用率', icon: Receipt, component: ExpenseTab }, { key: 'expense', name: '费用率', icon: Receipt, component: ExpenseTab },
{ key: 'cashflowMetrics', name: '现金流指标', icon: Banknote, component: CashflowMetricsTab }, { key: 'cashflowMetrics', name: '现金流指标', icon: Banknote, component: CashflowMetricsTab },
// 三大财务报表 ],
{ key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab }, },
{ key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab },
{ key: 'cashflow', name: '现金流量表', icon: TrendingDown, component: CashflowTab },
], ],
[] []
); );
@@ -206,7 +217,8 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)"> <Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
<CardBody p={0}> <CardBody p={0}>
<SubTabContainer <SubTabContainer
tabs={tabConfigs} groups={tabGroups}
defaultIndex={0}
componentProps={componentProps} componentProps={componentProps}
themePreset="blackGold" themePreset="blackGold"
isLazy isLazy