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> = ({ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
industryRank, industryRank,
bgColor, bgColor = 'white',
borderColor, 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) { if (!industryRank || !Array.isArray(industryRank) || industryRank.length === 0) {
return ( return (
<Card> <Card bg={cardBg} borderColor={borderColor} borderWidth="1px">
<CardBody> <CardBody>
<Text textAlign="center" color="gray.500" py={8}> <Text textAlign="center" color={resolvedLabelColor} py={8}>
</Text> </Text>
</CardBody> </CardBody>
@@ -39,17 +48,32 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
return ( return (
<VStack spacing={4} align="stretch"> <VStack spacing={4} align="stretch">
{industryRank.map((periodData, periodIdx) => ( {industryRank.map((periodData, periodIdx) => (
<Card key={periodIdx}> <Card
<CardHeader> key={periodIdx}
bg={cardBg}
borderColor={borderColor}
borderWidth="1px"
>
<CardHeader pb={2}>
<HStack justify="space-between"> <HStack justify="space-between">
<Heading size="sm">{periodData.report_type} </Heading> <Heading size="sm" color={headingColor}>
<Badge colorScheme="purple">{periodData.period}</Badge> {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> </HStack>
</CardHeader> </CardHeader>
<CardBody> <CardBody pt={2}>
{periodData.rankings?.map((ranking, idx) => ( {periodData.rankings?.map((ranking, idx) => (
<Box key={idx} mb={6}> <Box key={idx} mb={6}>
<Text fontWeight="bold" mb={3}> <Text fontWeight="bold" mb={3} color={resolvedTextColor}>
{ranking.industry_name} ({ranking.level_description}) {ranking.industry_name} ({ranking.level_description})
</Text> </Text>
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={3}> <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.includes('margin') ||
metric.key === 'roe'; 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 ( return (
<Box <Box
key={metric.key} key={metric.key}
@@ -74,14 +107,12 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
borderWidth="1px" borderWidth="1px"
borderColor={borderColor} borderColor={borderColor}
> >
<Text fontSize="xs" color="gray.500"> <Text fontSize="xs" color={resolvedLabelColor}>
{metric.name} {metric.name}
</Text> </Text>
<HStack mt={1}> <HStack mt={1} spacing={2}>
<Text fontWeight="bold"> <Text fontWeight="bold" fontSize="lg" color={resolvedTextColor}>
{isPercentMetric {formattedValue}
? formatUtils.formatPercent(metricData.value)
: metricData.value?.toFixed(2) || '-'}
</Text> </Text>
{metricData.rank && ( {metricData.rank && (
<Badge <Badge
@@ -92,11 +123,8 @@ export const IndustryRankingView: React.FC<IndustryRankingViewProps> = ({
</Badge> </Badge>
)} )}
</HStack> </HStack>
<Text fontSize="xs" color="gray.500" mt={1}> <Text fontSize="xs" color={resolvedLabelColor} mt={1}>
:{' '} : {formattedAvg}
{isPercentMetric
? formatUtils.formatPercent(metricData.industry_avg)
: metricData.industry_avg?.toFixed(2) || '-'}
</Text> </Text>
</Box> </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 React from 'react';
import { import {
Card, Box,
CardBody,
Grid, Grid,
GridItem, GridItem,
VStack, VStack,
@@ -18,93 +17,153 @@ import {
StatNumber, StatNumber,
Alert, Alert,
AlertIcon, AlertIcon,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { formatUtils } from '@services/financialService'; import { formatUtils } from '@services/financialService';
import type { StockInfoHeaderProps } from '../types'; 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> = ({ export const StockInfoHeader: React.FC<StockInfoHeaderProps> = ({
stockInfo, stockInfo,
positiveColor,
negativeColor,
}) => { }) => {
if (!stockInfo) return null; if (!stockInfo) return null;
return ( return (
<Card mb={4}> <Box
<CardBody> mb={4}
<Grid templateColumns="repeat(6, 1fr)" gap={4}> bg={darkGoldTheme.bgCard}
<GridItem colSpan={{ base: 6, md: 2 }}> border="1px solid"
<VStack align="start"> borderColor={darkGoldTheme.border}
<Text fontSize="xs" color="gray.500"> borderRadius="xl"
p={5}
</Text> boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
<HStack> transition="all 0.3s ease"
<Heading size="md">{stockInfo.stock_name}</Heading> _hover={{
<Badge>{stockInfo.stock_code}</Badge> borderColor: darkGoldTheme.borderHover,
</HStack> boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
</VStack> }}
</GridItem> >
<GridItem> <Grid templateColumns="repeat(6, 1fr)" gap={4} alignItems="center">
<Stat> <GridItem colSpan={{ base: 6, md: 2 }}>
<StatLabel>EPS</StatLabel> <VStack align="start" spacing={1}>
<StatNumber> <Text fontSize="xs" color={darkGoldTheme.textMuted}>
{stockInfo.key_metrics?.eps?.toFixed(3) || '-'}
</StatNumber> </Text>
</Stat> <HStack>
</GridItem> <Heading
<GridItem> size="md"
<Stat> bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
<StatLabel>ROE</StatLabel> bgClip="text"
<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'
}
> >
{formatUtils.formatPercent(stockInfo.growth_rates?.revenue_growth)} {stockInfo.stock_name}
</StatNumber> </Heading>
</Stat> <Badge
</GridItem> bg={darkGoldTheme.tagBg}
<GridItem> color={darkGoldTheme.gold}
<Stat> fontSize="xs"
<StatLabel></StatLabel> px={2}
<StatNumber py={1}
color={ borderRadius="md"
stockInfo.growth_rates?.profit_growth
? stockInfo.growth_rates.profit_growth > 0
? positiveColor
: negativeColor
: 'gray.500'
}
> >
{formatUtils.formatPercent(stockInfo.growth_rates?.profit_growth)} {stockInfo.stock_code}
</StatNumber> </Badge>
</Stat> </HStack>
</GridItem> </VStack>
</Grid> </GridItem>
{stockInfo.latest_forecast && ( <GridItem>
<Alert status="info" mt={4}> <Stat>
<AlertIcon /> <StatLabel color={darkGoldTheme.textMuted} fontSize="xs">
<Box> EPS
<Text fontWeight="bold">{stockInfo.latest_forecast.forecast_type}</Text> </StatLabel>
<Text fontSize="sm">{stockInfo.latest_forecast.content}</Text> <StatNumber color={darkGoldTheme.goldLight} fontSize="lg">
</Box> {stockInfo.key_metrics?.eps?.toFixed(3) || '-'}
</Alert> </StatNumber>
)} </Stat>
</CardBody> </GridItem>
</Card> <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 { StockInfoHeader } from './StockInfoHeader';
export { BalanceSheetTable } from './BalanceSheetTable'; export { BalanceSheetTable } from './BalanceSheetTable';
export { IncomeStatementTable } from './IncomeStatementTable'; 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 { import {
Box, Box,
Container, Container,
@@ -11,20 +11,12 @@ import {
HStack, HStack,
Card, Card,
CardBody, CardBody,
CardHeader,
Heading,
Text, Text,
Badge,
Select, Select,
IconButton, IconButton,
Alert, Alert,
AlertIcon, AlertIcon,
Skeleton, Skeleton,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Modal, Modal,
ModalOverlay, ModalOverlay,
ModalContent, ModalContent,
@@ -40,25 +32,25 @@ import {
Td, Td,
TableContainer, TableContainer,
Divider, Divider,
Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { RepeatIcon } from '@chakra-ui/icons'; import { RepeatIcon } from '@chakra-ui/icons';
import { BarChart3, DollarSign, TrendingUp } from 'lucide-react';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { formatUtils } from '@services/financialService'; import { formatUtils } from '@services/financialService';
// 通用组件
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
// 内部模块导入 // 内部模块导入
import { useFinancialData } from './hooks'; import { useFinancialData } from './hooks';
import { COLORS } from './constants'; import { COLORS } from './constants';
import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils'; import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils';
import { import {
StockInfoHeader, StockInfoHeader,
BalanceSheetTable,
IncomeStatementTable,
CashflowTable,
FinancialMetricsTable, FinancialMetricsTable,
MainBusinessAnalysis, MainBusinessAnalysis,
ComparisonAnalysis,
} from './components'; } from './components';
import { BalanceSheetTab, IncomeStatementTab, CashflowTab } from './tabs';
import type { FinancialPanoramaProps } from './types'; import type { FinancialPanoramaProps } from './types';
/** /**
@@ -73,29 +65,24 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
cashflow, cashflow,
financialMetrics, financialMetrics,
mainBusiness, mainBusiness,
forecast,
industryRank,
comparison,
loading, loading,
error, error,
refetch, refetch,
currentStockCode,
selectedPeriods, selectedPeriods,
setSelectedPeriods, setSelectedPeriods,
} = useFinancialData({ stockCode: propStockCode }); } = useFinancialData({ stockCode: propStockCode });
// UI 状态 // UI 状态
const [activeTab, setActiveTab] = useState(0);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [modalContent, setModalContent] = useState<ReactNode>(null); const [modalContent, setModalContent] = useState<ReactNode>(null);
// 颜色配置 // 颜色配置
const { bgColor, hoverBg, positiveColor, negativeColor, borderColor } = COLORS; const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS;
// 点击指标行显示图表 // 点击指标行显示图表
const showMetricChart = ( const showMetricChart = (
metricName: string, metricName: string,
metricKey: string, _metricKey: string,
data: Array<{ period: string; [key: string]: unknown }>, data: Array<{ period: string; [key: string]: unknown }>,
dataPath: string dataPath: string
) => { ) => {
@@ -204,6 +191,45 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
hoverBg, 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 ( return (
<Container maxW="container.xl" py={5}> <Container maxW="container.xl" py={5}>
<VStack spacing={6} align="stretch"> <VStack spacing={6} align="stretch">
@@ -250,124 +276,35 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
/> />
)} )}
{/* 主要内容区域 */} {/* 财务指标速览 */}
{!loading && stockInfo && ( {!loading && stockInfo && (
<Tabs <FinancialMetricsTable data={financialMetrics} {...tableProps} />
index={activeTab} )}
onChange={setActiveTab}
variant="enclosed"
colorScheme="blue"
>
<TabList>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
<Tab></Tab>
</TabList>
<TabPanels> {/* 主营业务 */}
{/* 财务概览 */} {!loading && stockInfo && (
<TabPanel> <Card>
<VStack spacing={4} align="stretch"> <CardBody>
<ComparisonAnalysis comparison={comparison} /> <Text fontSize="lg" fontWeight="bold" mb={4}>
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
</VStack> </Text>
</TabPanel> <MainBusinessAnalysis mainBusiness={mainBusiness} />
</CardBody>
</Card>
)}
{/* 资产负债表 */} {/* 三大财务报表 - 使用 SubTabContainer 二级导航 */}
<TabPanel> {!loading && stockInfo && (
<Card> <Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
<CardHeader> <CardBody p={0}>
<VStack align="stretch" spacing={2}> <SubTabContainer
<HStack justify="space-between"> tabs={tabConfigs}
<Heading size="md"></Heading> componentProps={componentProps}
<HStack spacing={2}> themePreset="blackGold"
<Badge colorScheme="blue"> isLazy
{Math.min(balanceSheet.length, 8)} />
</Badge> </CardBody>
<Text fontSize="sm" color="gray.500"> </Card>
绿 |
</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>
)} )}
{/* 错误提示 */} {/* 错误提示 */}

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 */ /** 行业排名 Props */
export interface IndustryRankingViewProps { export interface IndustryRankingViewProps {
industryRank: IndustryRankData[]; industryRank: IndustryRankData[];
bgColor: string; bgColor?: string;
borderColor: string; borderColor?: string;
textColor?: string;
labelColor?: string;
} }
/** 股票对比 Props */ /** 股票对比 Props */