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;
}
/**
* Tab 分组配置
*/
export interface SubTabGroup {
name: string;
tabs: SubTabConfig[];
}
/**
* 深空 FUI 主题配置
*/
@@ -129,8 +137,10 @@ const THEME_PRESETS: Record<string, SubTabTheme> = {
};
export interface SubTabContainerProps {
/** Tab 配置数组 */
tabs: SubTabConfig[];
/** Tab 配置数组(与 groups 二选一) */
tabs?: SubTabConfig[];
/** Tab 分组配置(与 tabs 二选一) */
groups?: SubTabGroup[];
/** 传递给 Tab 内容组件的 props */
componentProps?: Record<string, any>;
/** 默认选中的 Tab 索引 */
@@ -156,7 +166,8 @@ export interface SubTabContainerProps {
}
const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
tabs,
tabs: tabsProp,
groups,
componentProps = {},
defaultIndex = 0,
index: controlledIndex,
@@ -171,6 +182,21 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
}) => {
// 获取尺寸配置
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);
@@ -280,64 +306,90 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
>
{tabs.map((tab, idx) => {
const isSelected = idx === currentIndex;
// 检查是否需要在此 Tab 前显示分组标签
const groupIndex = groupBoundaries.indexOf(idx);
const showGroupLabel = groups && groupIndex !== -1;
return (
<Tab
key={tab.key}
color={theme.tabUnselectedColor}
borderRadius={DEEP_SPACE.radius}
px={sizeConfig.px}
py={sizeConfig.py}
fontSize={sizeConfig.fontSize}
fontWeight="500"
whiteSpace="nowrap"
flexShrink={0}
border="1px solid transparent"
position="relative"
letterSpacing="0.03em"
transition={DEEP_SPACE.transition}
_before={{
content: '""',
position: 'absolute',
bottom: '-1px',
left: '50%',
transform: 'translateX(-50%)',
width: isSelected ? '70%' : '0%',
height: '2px',
bg: '#D4AF37',
borderRadius: 'full',
transition: 'width 0.3s ease',
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
}}
_selected={{
bg: theme.tabSelectedBg,
color: theme.tabSelectedColor,
fontWeight: '700',
boxShadow: DEEP_SPACE.glowGold,
border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
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 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
color={theme.tabUnselectedColor}
borderRadius={DEEP_SPACE.radius}
px={sizeConfig.px}
py={sizeConfig.py}
fontSize={sizeConfig.fontSize}
fontWeight="500"
whiteSpace="nowrap"
flexShrink={0}
border="1px solid transparent"
position="relative"
letterSpacing="0.03em"
transition={DEEP_SPACE.transition}
_before={{
content: '""',
position: 'absolute',
bottom: '-1px',
left: '50%',
transform: 'translateX(-50%)',
width: isSelected ? '70%' : '0%',
height: '2px',
bg: '#D4AF37',
borderRadius: 'full',
transition: 'width 0.3s ease',
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
}}
_selected={{
bg: theme.tabSelectedBg,
color: theme.tabSelectedColor,
fontWeight: '700',
boxShadow: DEEP_SPACE.glowGold,
border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
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>

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

View File

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

View File

@@ -27,7 +27,7 @@ import {
} 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';
@@ -59,7 +59,13 @@ import type { FinancialPanoramaProps } from './types';
* 财务全景主组件
*/
// Tab key 映射表SubTabContainer index -> DataTypeKey
// 顺序基础报表3个 + 财务指标分析7个
const TAB_KEY_MAP: DataTypeKey[] = [
// 基础报表
'balance',
'income',
'cashflow',
// 财务指标分析
'profitability',
'perShare',
'growth',
@@ -67,9 +73,6 @@ const TAB_KEY_MAP: DataTypeKey[] = [
'solvency',
'expense',
'cashflowMetrics',
'balance',
'income',
'cashflow',
];
const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propStockCode }) => {
@@ -122,21 +125,29 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
onOpen();
}, [onOpen]);
// Tab 配置 - 财务指标分类 + 三大财务报表
const tabConfigs: SubTabConfig[] = useMemo(
// Tab 分组配置 - 基础报表 + 财务指标分析
const tabGroups: SubTabGroup[] = useMemo(
() => [
// 财务指标分类7个
{ 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 },
// 三大财务报表
{ key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab },
{ key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab },
{ key: 'cashflow', name: '现金流量表', icon: TrendingDown, component: CashflowTab },
{
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: '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)">
<CardBody p={0}>
<SubTabContainer
tabs={tabConfigs}
groups={tabGroups}
defaultIndex={0}
componentProps={componentProps}
themePreset="blackGold"
isLazy