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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user