Files
vf_react/src/views/Company/components/FinancialPanorama/index.tsx
zdl 4954373b5b style(ComparisonAnalysis): 应用黑金主题样式
- 图表配置:金色标题、深色 tooltip、金色坐标轴
- 净利润折线改为金色渐变填充
- 营收柱状图首个柱子使用金色
- 组件容器:透明背景 + 金色边框
- 移除外部重复标题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 20:35:14 +08:00

350 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 财务全景组件
* 重构后的主组件,使用模块化结构和 SubTabContainer 二级导航
*/
import React, { useState, useMemo, useCallback, ReactNode } from 'react';
import {
Box,
Container,
VStack,
Card,
CardBody,
Text,
Alert,
AlertIcon,
Skeleton,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Divider,
} from '@chakra-ui/react';
import {
BarChart3,
DollarSign,
TrendingUp,
PieChart,
Percent,
TrendingDown,
Activity,
Shield,
Receipt,
Banknote,
} from 'lucide-react';
import ReactECharts from 'echarts-for-react';
import { formatUtils } from '@services/financialService';
// 通用组件
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
// 内部模块导入
import { useFinancialData, type DataTypeKey } from './hooks';
import { COLORS } from './constants';
import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils';
import { PeriodSelector, FinancialOverviewPanel, MainBusinessAnalysis, ComparisonAnalysis } from './components';
import {
BalanceSheetTab,
IncomeStatementTab,
CashflowTab,
ProfitabilityTab,
PerShareTab,
GrowthTab,
OperationalTab,
SolvencyTab,
ExpenseTab,
CashflowMetricsTab,
} from './tabs';
import type { FinancialPanoramaProps } from './types';
/**
* 财务全景主组件
*/
// Tab key 映射表SubTabContainer index -> DataTypeKey
const TAB_KEY_MAP: DataTypeKey[] = [
'profitability',
'perShare',
'growth',
'operational',
'solvency',
'expense',
'cashflowMetrics',
'balance',
'income',
'cashflow',
];
const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propStockCode }) => {
// 使用数据加载 Hook
const {
stockInfo,
balanceSheet,
incomeStatement,
cashflow,
financialMetrics,
mainBusiness,
comparison,
loading,
loadingTab,
error,
refetchByTab,
selectedPeriods,
setSelectedPeriods,
setActiveTab,
activeTab,
} = useFinancialData({ stockCode: propStockCode });
// 处理 Tab 切换
const handleTabChange = useCallback((index: number, tabKey: string) => {
const dataTypeKey = TAB_KEY_MAP[index] || (tabKey as DataTypeKey);
setActiveTab(dataTypeKey);
}, [setActiveTab]);
// 处理刷新 - 只刷新当前 Tab
const handleRefresh = useCallback(() => {
refetchByTab(activeTab);
}, [refetchByTab, activeTab]);
// UI 状态
const { isOpen, onOpen, onClose } = useDisclosure();
const [modalContent, setModalContent] = useState<ReactNode>(null);
// 颜色配置
const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS;
// 点击指标行显示图表
const showMetricChart = (
metricName: string,
_metricKey: string,
data: Array<{ period: string; [key: string]: unknown }>,
dataPath: string
) => {
const chartData = data
.map((item) => {
const value = dataPath.split('.').reduce((obj: unknown, key: string) => {
if (obj && typeof obj === 'object') {
return (obj as Record<string, unknown>)[key];
}
return undefined;
}, item) as number | undefined;
return {
period: formatUtils.getReportType(item.period),
date: item.period,
value: value ?? 0,
};
})
.reverse();
const option = getMetricChartOption(metricName, chartData);
setModalContent(
<Box>
<ReactECharts option={option} style={{ height: '400px', width: '100%' }} />
<Divider my={4} />
<TableContainer>
<Table size="sm">
<Thead>
<Tr>
<Th></Th>
<Th isNumeric></Th>
<Th isNumeric></Th>
<Th isNumeric></Th>
</Tr>
</Thead>
<Tbody>
{chartData.map((item, idx) => {
// 计算环比
const qoq =
idx > 0
? ((item.value - chartData[idx - 1].value) /
Math.abs(chartData[idx - 1].value)) *
100
: null;
// 计算同比
const currentDate = new Date(item.date);
const lastYearItem = chartData.find((d) => {
const date = new Date(d.date);
return (
date.getFullYear() === currentDate.getFullYear() - 1 &&
date.getMonth() === currentDate.getMonth()
);
});
const yoy = lastYearItem
? ((item.value - lastYearItem.value) / Math.abs(lastYearItem.value)) * 100
: null;
return (
<Tr key={idx}>
<Td>{item.period}</Td>
<Td isNumeric>{formatUtils.formatLargeNumber(item.value)}</Td>
<Td
isNumeric
color={
yoy !== null && yoy > 0
? positiveColor
: yoy !== null && yoy < 0
? negativeColor
: 'gray.500'
}
>
{yoy !== null ? `${yoy.toFixed(2)}%` : '-'}
</Td>
<Td
isNumeric
color={
qoq !== null && qoq > 0
? positiveColor
: qoq !== null && qoq < 0
? negativeColor
: 'gray.500'
}
>
{qoq !== null ? `${qoq.toFixed(2)}%` : '-'}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</Box>
);
onOpen();
};
// Tab 配置 - 财务指标分类 + 三大财务报表
const tabConfigs: SubTabConfig[] = 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 },
],
[]
);
// 传递给 Tab 组件的 props
const componentProps = useMemo(
() => ({
// 数据
balanceSheet,
incomeStatement,
cashflow,
financialMetrics,
// 工具函数
showMetricChart,
calculateYoYChange,
getCellBackground,
// 颜色配置
positiveColor,
negativeColor,
bgColor,
hoverBg,
}),
[
balanceSheet,
incomeStatement,
cashflow,
financialMetrics,
showMetricChart,
positiveColor,
negativeColor,
bgColor,
hoverBg,
]
);
return (
<Container maxW="container.xl" py={5}>
<VStack spacing={6} align="stretch">
{/* 财务全景面板(三列布局:成长能力、盈利与回报、风险与运营) */}
{loading ? (
<Skeleton height="100px" />
) : (
<FinancialOverviewPanel
stockInfo={stockInfo}
financialMetrics={financialMetrics}
/>
)}
{/* 营收与利润趋势 */}
{!loading && comparison && comparison.length > 0 && (
<ComparisonAnalysis comparison={comparison} />
)}
{/* 主营业务 */}
{!loading && stockInfo && (
<Box>
<Text fontSize="lg" fontWeight="bold" mb={4} color="#D4AF37">
</Text>
<MainBusinessAnalysis mainBusiness={mainBusiness} />
</Box>
)}
{/* 三大财务报表 - 使用 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
onTabChange={handleTabChange}
rightElement={
<PeriodSelector
selectedPeriods={selectedPeriods}
onPeriodsChange={setSelectedPeriods}
onRefresh={handleRefresh}
isLoading={loadingTab !== null}
/>
}
/>
</CardBody>
</Card>
)}
{/* 错误提示 */}
{error && (
<Alert status="error">
<AlertIcon />
{error}
</Alert>
)}
{/* 弹出模态框 */}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent maxW="900px">
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>{modalContent}</ModalBody>
</ModalContent>
</Modal>
</VStack>
</Container>
);
};
export default FinancialPanorama;