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