refactor(FinancialPanorama): 重构为 SubTabContainer 二级导航

- 主组件从 Chakra Tabs 迁移到 SubTabContainer
  - 新增 PeriodSelector 时间选择器组件
  - IndustryRankingView 增加深色主题支持
  - 拆分出 6 个独立 Tab 组件到 tabs/ 目录
  - 类型定义优化,props 改为可选
This commit is contained in:
zdl
2025-12-16 16:33:25 +08:00
parent e08b9d2104
commit 6a4c475d3a
13 changed files with 697 additions and 238 deletions

View File

@@ -21,14 +21,23 @@ import type { IndustryRankingViewProps } from '../types';
export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
industryRank,
bgColor,
borderColor,
bgColor = 'white',
borderColor = 'gray.200',
textColor,
labelColor,
}) => {
// 判断是否为深色主题
const isDarkTheme = bgColor === 'gray.800' || bgColor === 'gray.900';
const resolvedTextColor = textColor || (isDarkTheme ? 'white' : 'gray.800');
const resolvedLabelColor = labelColor || (isDarkTheme ? 'gray.400' : 'gray.500');
const cardBg = isDarkTheme ? 'transparent' : 'white';
const headingColor = isDarkTheme ? 'yellow.500' : 'gray.800';
if (!industryRank || !Array.isArray(industryRank) || industryRank.length === 0) {
return (
<Card>
<Card bg={cardBg} borderColor={borderColor} borderWidth="1px">
<CardBody>
<Text textAlign="center" color="gray.500" py={8}>
<Text textAlign="center" color={resolvedLabelColor} py={8}>
</Text>
</CardBody>
@@ -39,17 +48,32 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
return (
<VStack spacing={4} align="stretch">
{industryRank.map((periodData, periodIdx) => (
<Card key={periodIdx}>
<CardHeader>
<Card
key={periodIdx}
bg={cardBg}
borderColor={borderColor}
borderWidth="1px"
>
<CardHeader pb={2}>
<HStack justify="space-between">
<Heading size="sm">{periodData.report_type} </Heading>
<Badge colorScheme="purple">{periodData.period}</Badge>
<Heading size="sm" color={headingColor}>
{periodData.report_type}
</Heading>
<Badge
bg={isDarkTheme ? 'transparent' : undefined}
borderWidth={isDarkTheme ? '1px' : 0}
borderColor={isDarkTheme ? 'yellow.600' : undefined}
color={isDarkTheme ? 'yellow.500' : undefined}
colorScheme={isDarkTheme ? undefined : 'purple'}
>
{periodData.period}
</Badge>
</HStack>
</CardHeader>
<CardBody>
<CardBody pt={2}>
{periodData.rankings?.map((ranking, idx) => (
<Box key={idx} mb={6}>
<Text fontWeight="bold" mb={3}>
<Text fontWeight="bold" mb={3} color={resolvedTextColor}>
{ranking.industry_name} ({ranking.level_description})
</Text>
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={3}>
@@ -65,6 +89,15 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
metric.key.includes('margin') ||
metric.key === 'roe';
// 格式化数值
const formattedValue = isPercentMetric
? formatUtils.formatPercent(metricData.value)
: metricData.value?.toFixed(2) ?? '-';
const formattedAvg = isPercentMetric
? formatUtils.formatPercent(metricData.industry_avg)
: metricData.industry_avg?.toFixed(2) ?? '-';
return (
<Box
key={metric.key}
@@ -74,14 +107,12 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
borderWidth="1px"
borderColor={borderColor}
>
<Text fontSize="xs" color="gray.500">
<Text fontSize="xs" color={resolvedLabelColor}>
{metric.name}
</Text>
<HStack mt={1}>
<Text fontWeight="bold">
{isPercentMetric
? formatUtils.formatPercent(metricData.value)
: metricData.value?.toFixed(2) || '-'}
<HStack mt={1} spacing={2}>
<Text fontWeight="bold" fontSize="lg" color={resolvedTextColor}>
{formattedValue}
</Text>
{metricData.rank && (
<Badge
@@ -92,11 +123,8 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
</Badge>
)}
</HStack>
<Text fontSize="xs" color="gray.500" mt={1}>
:{' '}
{isPercentMetric
? formatUtils.formatPercent(metricData.industry_avg)
: metricData.industry_avg?.toFixed(2) || '-'}
<Text fontSize="xs" color={resolvedLabelColor} mt={1}>
: {formattedAvg}
</Text>
</Box>
);

View File

@@ -0,0 +1,78 @@
/**
* 期数选择器组件
* 用于选择显示的财务报表期数,并提供刷新功能
*/
import React, { memo } from 'react';
import {
Card,
CardBody,
HStack,
Text,
Select,
IconButton,
} from '@chakra-ui/react';
import { RepeatIcon } from '@chakra-ui/icons';
export interface PeriodSelectorProps {
/** 当前选中的期数 */
selectedPeriods: number;
/** 期数变更回调 */
onPeriodsChange: (periods: number) => void;
/** 刷新回调 */
onRefresh: () => void;
/** 是否加载中 */
isLoading?: boolean;
/** 可选期数列表,默认 [4, 8, 12, 16] */
periodOptions?: number[];
/** 标签文本 */
label?: string;
}
const PeriodSelector: React.FC<PeriodSelectorProps> = memo(({
selectedPeriods,
onPeriodsChange,
onRefresh,
isLoading = false,
periodOptions = [4, 8, 12, 16],
label = '显示期数:',
}) => {
return (
<Card>
<CardBody>
<HStack justify="space-between">
<HStack>
<Text fontSize="sm" color="gray.600">
{label}
</Text>
<Select
value={selectedPeriods}
onChange={(e) => onPeriodsChange(Number(e.target.value))}
w="150px"
size="sm"
>
{periodOptions.map((period) => (
<option key={period} value={period}>
{period}
</option>
))}
</Select>
</HStack>
<IconButton
icon={<RepeatIcon />}
onClick={onRefresh}
isLoading={isLoading}
variant="outline"
size="sm"
aria-label="刷新数据"
/>
</HStack>
</CardBody>
</Card>
);
});
PeriodSelector.displayName = 'PeriodSelector';
export { PeriodSelector };
export default PeriodSelector;

View File

@@ -1,11 +1,10 @@
/**
* 股票信息头部组件
* 股票信息头部组件 - 黑金主题
*/
import React from 'react';
import {
Card,
CardBody,
Box,
Grid,
GridItem,
VStack,
@@ -18,93 +17,153 @@ import {
StatNumber,
Alert,
AlertIcon,
Box,
} from '@chakra-ui/react';
import { formatUtils } from '@services/financialService';
import type { StockInfoHeaderProps } from '../types';
// 黑金主题配置
const darkGoldTheme = {
bgCard: 'rgba(26, 32, 44, 0.95)',
border: 'rgba(212, 175, 55, 0.3)',
borderHover: 'rgba(212, 175, 55, 0.5)',
gold: '#D4AF37',
goldLight: '#F4D03F',
orange: '#FF9500',
red: '#FF4444',
green: '#00C851',
textPrimary: 'rgba(255, 255, 255, 0.92)',
textSecondary: 'rgba(255, 255, 255, 0.7)',
textMuted: 'rgba(255, 255, 255, 0.5)',
tagBg: 'rgba(212, 175, 55, 0.15)',
};
export const StockInfoHeader: React.FC<StockInfoHeaderProps> = ({
stockInfo,
positiveColor,
negativeColor,
}) => {
if (!stockInfo) return null;
return (
<Card mb={4}>
<CardBody>
<Grid templateColumns="repeat(6, 1fr)" gap={4}>
<GridItem colSpan={{ base: 6, md: 2 }}>
<VStack align="start">
<Text fontSize="xs" color="gray.500">
</Text>
<HStack>
<Heading size="md">{stockInfo.stock_name}</Heading>
<Badge>{stockInfo.stock_code}</Badge>
</HStack>
</VStack>
</GridItem>
<GridItem>
<Stat>
<StatLabel>EPS</StatLabel>
<StatNumber>
{stockInfo.key_metrics?.eps?.toFixed(3) || '-'}
</StatNumber>
</Stat>
</GridItem>
<GridItem>
<Stat>
<StatLabel>ROE</StatLabel>
<StatNumber>
{formatUtils.formatPercent(stockInfo.key_metrics?.roe)}
</StatNumber>
</Stat>
</GridItem>
<GridItem>
<Stat>
<StatLabel></StatLabel>
<StatNumber
color={
stockInfo.growth_rates?.revenue_growth
? stockInfo.growth_rates.revenue_growth > 0
? positiveColor
: negativeColor
: 'gray.500'
}
<Box
mb={4}
bg={darkGoldTheme.bgCard}
border="1px solid"
borderColor={darkGoldTheme.border}
borderRadius="xl"
p={5}
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
transition="all 0.3s ease"
_hover={{
borderColor: darkGoldTheme.borderHover,
boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
}}
>
<Grid templateColumns="repeat(6, 1fr)" gap={4} alignItems="center">
<GridItem colSpan={{ base: 6, md: 2 }}>
<VStack align="start" spacing={1}>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
</Text>
<HStack>
<Heading
size="md"
bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
bgClip="text"
>
{formatUtils.formatPercent(stockInfo.growth_rates?.revenue_growth)}
</StatNumber>
</Stat>
</GridItem>
<GridItem>
<Stat>
<StatLabel></StatLabel>
<StatNumber
color={
stockInfo.growth_rates?.profit_growth
? stockInfo.growth_rates.profit_growth > 0
? positiveColor
: negativeColor
: 'gray.500'
}
{stockInfo.stock_name}
</Heading>
<Badge
bg={darkGoldTheme.tagBg}
color={darkGoldTheme.gold}
fontSize="xs"
px={2}
py={1}
borderRadius="md"
>
{formatUtils.formatPercent(stockInfo.growth_rates?.profit_growth)}
</StatNumber>
</Stat>
</GridItem>
</Grid>
{stockInfo.latest_forecast && (
<Alert status="info" mt={4}>
<AlertIcon />
<Box>
<Text fontWeight="bold">{stockInfo.latest_forecast.forecast_type}</Text>
<Text fontSize="sm">{stockInfo.latest_forecast.content}</Text>
</Box>
</Alert>
)}
</CardBody>
</Card>
{stockInfo.stock_code}
</Badge>
</HStack>
</VStack>
</GridItem>
<GridItem>
<Stat>
<StatLabel color={darkGoldTheme.textMuted} fontSize="xs">
EPS
</StatLabel>
<StatNumber color={darkGoldTheme.goldLight} fontSize="lg">
{stockInfo.key_metrics?.eps?.toFixed(3) || '-'}
</StatNumber>
</Stat>
</GridItem>
<GridItem>
<Stat>
<StatLabel color={darkGoldTheme.textMuted} fontSize="xs">
ROE
</StatLabel>
<StatNumber color={darkGoldTheme.goldLight} fontSize="lg">
{formatUtils.formatPercent(stockInfo.key_metrics?.roe)}
</StatNumber>
</Stat>
</GridItem>
<GridItem>
<Stat>
<StatLabel color={darkGoldTheme.textMuted} fontSize="xs">
</StatLabel>
<StatNumber
fontSize="lg"
color={
stockInfo.growth_rates?.revenue_growth
? stockInfo.growth_rates.revenue_growth > 0
? darkGoldTheme.red
: darkGoldTheme.green
: darkGoldTheme.textMuted
}
>
{formatUtils.formatPercent(stockInfo.growth_rates?.revenue_growth)}
</StatNumber>
</Stat>
</GridItem>
<GridItem>
<Stat>
<StatLabel color={darkGoldTheme.textMuted} fontSize="xs">
</StatLabel>
<StatNumber
fontSize="lg"
color={
stockInfo.growth_rates?.profit_growth
? stockInfo.growth_rates.profit_growth > 0
? darkGoldTheme.red
: darkGoldTheme.green
: darkGoldTheme.textMuted
}
>
{formatUtils.formatPercent(stockInfo.growth_rates?.profit_growth)}
</StatNumber>
</Stat>
</GridItem>
</Grid>
{stockInfo.latest_forecast && (
<Alert
status="info"
mt={4}
bg="rgba(212, 175, 55, 0.1)"
borderRadius="lg"
border="1px solid"
borderColor={darkGoldTheme.border}
>
<AlertIcon color={darkGoldTheme.gold} />
<Box>
<Text fontWeight="bold" color={darkGoldTheme.gold}>
{stockInfo.latest_forecast.forecast_type}
</Text>
<Text fontSize="sm" color={darkGoldTheme.textSecondary}>
{stockInfo.latest_forecast.content}
</Text>
</Box>
</Alert>
)}
</Box>
);
};

View File

@@ -2,6 +2,7 @@
* 组件统一导出
*/
export { PeriodSelector } from './PeriodSelector';
export { StockInfoHeader } from './StockInfoHeader';
export { BalanceSheetTable } from './BalanceSheetTable';
export { IncomeStatementTable } from './IncomeStatementTable';

View File

@@ -1,9 +1,9 @@
/**
* 财务全景组件
* 重构后的主组件,使用模块化结构
* 重构后的主组件,使用模块化结构和 SubTabContainer 二级导航
*/
import React, { useState, ReactNode } from 'react';
import React, { useState, useMemo, ReactNode } from 'react';
import {
Box,
Container,
@@ -11,20 +11,12 @@ import {
HStack,
Card,
CardBody,
CardHeader,
Heading,
Text,
Badge,
Select,
IconButton,
Alert,
AlertIcon,
Skeleton,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Modal,
ModalOverlay,
ModalContent,
@@ -40,25 +32,25 @@ import {
Td,
TableContainer,
Divider,
Tooltip,
} from '@chakra-ui/react';
import { RepeatIcon } from '@chakra-ui/icons';
import { BarChart3, DollarSign, TrendingUp } from 'lucide-react';
import ReactECharts from 'echarts-for-react';
import { formatUtils } from '@services/financialService';
// 通用组件
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
// 内部模块导入
import { useFinancialData } from './hooks';
import { COLORS } from './constants';
import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils';
import {
StockInfoHeader,
BalanceSheetTable,
IncomeStatementTable,
CashflowTable,
FinancialMetricsTable,
MainBusinessAnalysis,
ComparisonAnalysis,
} from './components';
import { BalanceSheetTab, IncomeStatementTab, CashflowTab } from './tabs';
import type { FinancialPanoramaProps } from './types';
/**
@@ -73,29 +65,24 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
cashflow,
financialMetrics,
mainBusiness,
forecast,
industryRank,
comparison,
loading,
error,
refetch,
currentStockCode,
selectedPeriods,
setSelectedPeriods,
} = useFinancialData({ stockCode: propStockCode });
// UI 状态
const [activeTab, setActiveTab] = useState(0);
const { isOpen, onOpen, onClose } = useDisclosure();
const [modalContent, setModalContent] = useState<ReactNode>(null);
// 颜色配置
const { bgColor, hoverBg, positiveColor, negativeColor, borderColor } = COLORS;
const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS;
// 点击指标行显示图表
const showMetricChart = (
metricName: string,
metricKey: string,
_metricKey: string,
data: Array<{ period: string; [key: string]: unknown }>,
dataPath: string
) => {
@@ -204,6 +191,45 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
hoverBg,
};
// Tab 配置 - 只保留三大财务报表
const tabConfigs: SubTabConfig[] = useMemo(
() => [
{ key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab },
{ key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab },
{ key: 'cashflow', name: '现金流量表', icon: TrendingUp, component: CashflowTab },
],
[]
);
// 传递给 Tab 组件的 props
const componentProps = useMemo(
() => ({
// 数据
balanceSheet,
incomeStatement,
cashflow,
// 工具函数
showMetricChart,
calculateYoYChange,
getCellBackground,
// 颜色配置
positiveColor,
negativeColor,
bgColor,
hoverBg,
}),
[
balanceSheet,
incomeStatement,
cashflow,
showMetricChart,
positiveColor,
negativeColor,
bgColor,
hoverBg,
]
);
return (
<Container maxW="container.xl" py={5}>
<VStack spacing={6} align="stretch">
@@ -250,124 +276,35 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
/>
)}
{/* 主要内容区域 */}
{/* 财务指标速览 */}
{!loading && stockInfo && (
<Tabs
index={activeTab}
onChange={setActiveTab}
variant="enclosed"
colorScheme="blue"
>
<TabList>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
</TabList>
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
)}
<TabPanels>
{/* 财务概览 */}
<TabPanel>
<VStack spacing={4} align="stretch">
<ComparisonAnalysis comparison={comparison} />
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
</VStack>
</TabPanel>
{/* 主营业务 */}
{!loading && stockInfo && (
<Card>
<CardBody>
<Text fontSize="lg" fontWeight="bold" mb={4}>
</Text>
<MainBusinessAnalysis mainBusiness={mainBusiness} />
</CardBody>
</Card>
)}
{/* 资产负债表 */}
<TabPanel>
<Card>
<CardHeader>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Heading size="md"></Heading>
<HStack spacing={2}>
<Badge colorScheme="blue">
{Math.min(balanceSheet.length, 8)}
</Badge>
<Text fontSize="sm" color="gray.500">
绿 |
</Text>
</HStack>
</HStack>
<Text fontSize="xs" color="gray.500">
</Text>
</VStack>
</CardHeader>
<CardBody>
<BalanceSheetTable data={balanceSheet} {...tableProps} />
</CardBody>
</Card>
</TabPanel>
{/* 利润表 */}
<TabPanel>
<Card>
<CardHeader>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Heading size="md"></Heading>
<HStack spacing={2}>
<Badge colorScheme="blue">
{Math.min(incomeStatement.length, 8)}
</Badge>
<Text fontSize="sm" color="gray.500">
绿 |
</Text>
</HStack>
</HStack>
<Text fontSize="xs" color="gray.500">
Q1Q3
</Text>
</VStack>
</CardHeader>
<CardBody>
<IncomeStatementTable data={incomeStatement} {...tableProps} />
</CardBody>
</Card>
</TabPanel>
{/* 现金流量表 */}
<TabPanel>
<Card>
<CardHeader>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Heading size="md"></Heading>
<HStack spacing={2}>
<Badge colorScheme="blue">
{Math.min(cashflow.length, 8)}
</Badge>
<Text fontSize="sm" color="gray.500">
绿 |
</Text>
</HStack>
</HStack>
<Text fontSize="xs" color="gray.500">
绿
</Text>
</VStack>
</CardHeader>
<CardBody>
<CashflowTable data={cashflow} {...tableProps} />
</CardBody>
</Card>
</TabPanel>
{/* 财务指标 */}
<TabPanel>
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
</TabPanel>
{/* 主营业务 */}
<TabPanel>
<MainBusinessAnalysis mainBusiness={mainBusiness} />
</TabPanel>
</TabPanels>
</Tabs>
{/* 三大财务报表 - 使用 SubTabContainer 二级导航 */}
{!loading && stockInfo && (
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
<CardBody p={0}>
<SubTabContainer
tabs={tabConfigs}
componentProps={componentProps}
themePreset="blackGold"
isLazy
/>
</CardBody>
</Card>
)}
{/* 错误提示 */}

View File

@@ -0,0 +1,77 @@
/**
* 资产负债表 Tab
*/
import React from 'react';
import {
Card,
CardBody,
CardHeader,
VStack,
HStack,
Heading,
Badge,
Text,
} from '@chakra-ui/react';
import { BalanceSheetTable } from '../components';
import type { BalanceSheetData } from '../types';
export interface BalanceSheetTabProps {
balanceSheet: BalanceSheetData[];
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
getCellBackground: (change: number, intensity: number) => string;
positiveColor: string;
negativeColor: string;
bgColor: string;
hoverBg: string;
}
const BalanceSheetTab: React.FC<BalanceSheetTabProps> = ({
balanceSheet,
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
}) => {
const tableProps = {
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
};
return (
<Card>
<CardHeader>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Heading size="md"></Heading>
<HStack spacing={2}>
<Badge colorScheme="blue">
{Math.min(balanceSheet.length, 8)}
</Badge>
<Text fontSize="sm" color="gray.500">
绿 |
</Text>
</HStack>
</HStack>
<Text fontSize="xs" color="gray.500">
</Text>
</VStack>
</CardHeader>
<CardBody>
<BalanceSheetTable data={balanceSheet} {...tableProps} />
</CardBody>
</Card>
);
};
export default BalanceSheetTab;

View File

@@ -0,0 +1,77 @@
/**
* 现金流量表 Tab
*/
import React from 'react';
import {
Card,
CardBody,
CardHeader,
VStack,
HStack,
Heading,
Badge,
Text,
} from '@chakra-ui/react';
import { CashflowTable } from '../components';
import type { CashflowData } from '../types';
export interface CashflowTabProps {
cashflow: CashflowData[];
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
getCellBackground: (change: number, intensity: number) => string;
positiveColor: string;
negativeColor: string;
bgColor: string;
hoverBg: string;
}
const CashflowTab: React.FC<CashflowTabProps> = ({
cashflow,
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
}) => {
const tableProps = {
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
};
return (
<Card>
<CardHeader>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Heading size="md"></Heading>
<HStack spacing={2}>
<Badge colorScheme="blue">
{Math.min(cashflow.length, 8)}
</Badge>
<Text fontSize="sm" color="gray.500">
绿 |
</Text>
</HStack>
</HStack>
<Text fontSize="xs" color="gray.500">
绿
</Text>
</VStack>
</CardHeader>
<CardBody>
<CashflowTable data={cashflow} {...tableProps} />
</CardBody>
</Card>
);
};
export default CashflowTab;

View File

@@ -0,0 +1,77 @@
/**
* 利润表 Tab
*/
import React from 'react';
import {
Card,
CardBody,
CardHeader,
VStack,
HStack,
Heading,
Badge,
Text,
} from '@chakra-ui/react';
import { IncomeStatementTable } from '../components';
import type { IncomeStatementData } from '../types';
export interface IncomeStatementTabProps {
incomeStatement: IncomeStatementData[];
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
getCellBackground: (change: number, intensity: number) => string;
positiveColor: string;
negativeColor: string;
bgColor: string;
hoverBg: string;
}
const IncomeStatementTab: React.FC<IncomeStatementTabProps> = ({
incomeStatement,
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
}) => {
const tableProps = {
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
};
return (
<Card>
<CardHeader>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Heading size="md"></Heading>
<HStack spacing={2}>
<Badge colorScheme="blue">
{Math.min(incomeStatement.length, 8)}
</Badge>
<Text fontSize="sm" color="gray.500">
绿 |
</Text>
</HStack>
</HStack>
<Text fontSize="xs" color="gray.500">
Q1Q3
</Text>
</VStack>
</CardHeader>
<CardBody>
<IncomeStatementTable data={incomeStatement} {...tableProps} />
</CardBody>
</Card>
);
};
export default IncomeStatementTab;

View File

@@ -0,0 +1,17 @@
/**
* 主营业务 Tab
*/
import React from 'react';
import { MainBusinessAnalysis } from '../components';
import type { MainBusinessData } from '../types';
export interface MainBusinessTabProps {
mainBusiness: MainBusinessData | null;
}
const MainBusinessTab: React.FC<MainBusinessTabProps> = ({ mainBusiness }) => {
return <MainBusinessAnalysis mainBusiness={mainBusiness} />;
};
export default MainBusinessTab;

View File

@@ -0,0 +1,43 @@
/**
* 财务指标 Tab
*/
import React from 'react';
import { FinancialMetricsTable } from '../components';
import type { FinancialMetricsData } from '../types';
export interface MetricsTabProps {
financialMetrics: FinancialMetricsData[];
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
getCellBackground: (change: number, intensity: number) => string;
positiveColor: string;
negativeColor: string;
bgColor: string;
hoverBg: string;
}
const MetricsTab: React.FC<MetricsTabProps> = ({
financialMetrics,
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
}) => {
const tableProps = {
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
};
return <FinancialMetricsTable data={financialMetrics} {...tableProps} />;
};
export default MetricsTab;

View File

@@ -0,0 +1,51 @@
/**
* 财务概览 Tab
*/
import React from 'react';
import { VStack } from '@chakra-ui/react';
import { ComparisonAnalysis, FinancialMetricsTable } from '../components';
import type { FinancialMetricsData, ComparisonData } from '../types';
export interface OverviewTabProps {
comparison: ComparisonData[];
financialMetrics: FinancialMetricsData[];
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
getCellBackground: (change: number, intensity: number) => string;
positiveColor: string;
negativeColor: string;
bgColor: string;
hoverBg: string;
}
const OverviewTab: React.FC<OverviewTabProps> = ({
comparison,
financialMetrics,
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
}) => {
const tableProps = {
showMetricChart,
calculateYoYChange,
getCellBackground,
positiveColor,
negativeColor,
bgColor,
hoverBg,
};
return (
<VStack spacing={4} align="stretch">
<ComparisonAnalysis comparison={comparison} />
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
</VStack>
);
};
export default OverviewTab;

View File

@@ -0,0 +1,12 @@
/**
* Tab 组件统一导出
* 仅保留三大财务报表 Tab
*/
export { default as BalanceSheetTab } from './BalanceSheetTab';
export { default as IncomeStatementTab } from './IncomeStatementTab';
export { default as CashflowTab } from './CashflowTab';
export type { BalanceSheetTabProps } from './BalanceSheetTab';
export type { IncomeStatementTabProps } from './IncomeStatementTab';
export type { CashflowTabProps } from './CashflowTab';

View File

@@ -392,8 +392,10 @@ export interface MainBusinessAnalysisProps {
/** 行业排名 Props */
export interface IndustryRankingViewProps {
industryRank: IndustryRankData[];
bgColor: string;
borderColor: string;
bgColor?: string;
borderColor?: string;
textColor?: string;
labelColor?: string;
}
/** 股票对比 Props */