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,64 +306,90 @@ 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 (
<Tab <React.Fragment key={tab.key}>
key={tab.key} {/* 分组标签 */}
color={theme.tabUnselectedColor} {showGroupLabel && (
borderRadius={DEEP_SPACE.radius} <HStack
px={sizeConfig.px} spacing={2}
py={sizeConfig.py} flexShrink={0}
fontSize={sizeConfig.fontSize} pl={groupIndex > 0 ? 3 : 0}
fontWeight="500" pr={2}
whiteSpace="nowrap" borderLeft={groupIndex > 0 ? '1px solid' : 'none'}
flexShrink={0} borderColor={DEEP_SPACE.borderGold}
border="1px solid transparent" ml={groupIndex > 0 ? 2 : 0}
position="relative" >
letterSpacing="0.03em" <Text
transition={DEEP_SPACE.transition} fontSize="xs"
_before={{ color={DEEP_SPACE.textMuted}
content: '""', fontWeight="500"
position: 'absolute', whiteSpace="nowrap"
bottom: '-1px', letterSpacing="0.05em"
left: '50%', >
transform: 'translateX(-50%)', {groups[groupIndex].name}
width: isSelected ? '70%' : '0%', </Text>
height: '2px', </HStack>
bg: '#D4AF37', )}
borderRadius: 'full', <Tab
transition: 'width 0.3s ease', color={theme.tabUnselectedColor}
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none', borderRadius={DEEP_SPACE.radius}
}} px={sizeConfig.px}
_selected={{ py={sizeConfig.py}
bg: theme.tabSelectedBg, fontSize={sizeConfig.fontSize}
color: theme.tabSelectedColor, fontWeight="500"
fontWeight: '700', whiteSpace="nowrap"
boxShadow: DEEP_SPACE.glowGold, flexShrink={0}
border: `1px solid ${DEEP_SPACE.borderGoldHover}`, border="1px solid transparent"
transform: 'translateY(-2px)', position="relative"
}} letterSpacing="0.03em"
_hover={{ transition={DEEP_SPACE.transition}
bg: isSelected ? undefined : theme.tabHoverBg, _before={{
border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`, content: '""',
transform: 'translateY(-1px)', position: 'absolute',
}} bottom: '-1px',
_active={{ left: '50%',
transform: 'translateY(0)', transform: 'translateX(-50%)',
}} width: isSelected ? '70%' : '0%',
> height: '2px',
<HStack spacing={size === 'sm' ? 1.5 : 2}> bg: '#D4AF37',
{tab.icon && ( borderRadius: 'full',
<Icon transition: 'width 0.3s ease',
as={tab.icon} boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
boxSize={sizeConfig.iconSize} }}
opacity={isSelected ? 1 : 0.7} _selected={{
transition="opacity 0.2s" bg: theme.tabSelectedBg,
/> color: theme.tabSelectedColor,
)} fontWeight: '700',
<Text>{tab.name}</Text> boxShadow: DEEP_SPACE.glowGold,
</HStack> border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
</Tab> transform: 'translateY(-2px)',
}}
_hover={{
bg: isSelected ? undefined : theme.tabHoverBg,
border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`,
transform: 'translateY(-1px)',
}}
_active={{
transform: 'translateY(0)',
}}
>
<HStack spacing={size === 'sm' ? 1.5 : 2}>
{tab.icon && (
<Icon
as={tab.icon}
boxSize={sizeConfig.iconSize}
opacity={isSelected ? 1 : 0.7}
transition="opacity 0.2s"
/>
)}
<Text>{tab.name}</Text>
</HStack>
</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,21 +125,29 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
onOpen(); onOpen();
}, [onOpen]); }, [onOpen]);
// Tab 配置 - 财务指标分类 + 三大财务报表 // Tab 分组配置 - 基础报表 + 财务指标分析
const tabConfigs: SubTabConfig[] = useMemo( const tabGroups: SubTabGroup[] = useMemo(
() => [ () => [
// 财务指标分类7个 {
{ key: 'profitability', name: '盈利能力', icon: PieChart, component: ProfitabilityTab }, name: '基础报表',
{ key: 'perShare', name: '每股指标', icon: Percent, component: PerShareTab }, tabs: [
{ key: 'growth', name: '成长能力', icon: TrendingUp, component: GrowthTab }, { key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab },
{ key: 'operational', name: '运营效率', icon: Activity, component: OperationalTab }, { key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab },
{ key: 'solvency', name: '偿债能力', icon: Shield, component: SolvencyTab }, { key: 'cashflow', name: '现金流量表', icon: TrendingDown, component: CashflowTab },
{ key: 'expense', name: '费用率', icon: Receipt, component: ExpenseTab }, ],
{ key: 'cashflowMetrics', name: '现金流指标', icon: Banknote, component: CashflowMetricsTab }, },
// 三大财务报表 {
{ key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab }, name: '财务指标分析',
{ key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab }, tabs: [
{ key: 'cashflow', name: '现金流量表', icon: TrendingDown, component: CashflowTab }, { key: 'profitability', name: '盈利能力', icon: PieChart, component: ProfitabilityTab },
{ key: 'perShare', name: '每股指标', icon: Percent, component: PerShareTab },
{ key: 'growth', name: '成长能力', icon: TrendingUp, component: GrowthTab },
{ key: 'operational', name: '运营效率', icon: Activity, component: OperationalTab },
{ key: 'solvency', name: '偿债能力', icon: Shield, component: SolvencyTab },
{ key: 'expense', name: '费用率', icon: Receipt, component: ExpenseTab },
{ key: 'cashflowMetrics', name: '现金流指标', icon: Banknote, component: CashflowMetricsTab },
],
},
], ],
[] []
); );
@@ -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