refactor(FinancialPanorama): 优化主组件 Props 传递

- 使用 MetricChartModal 替代内联 Modal
- 简化 showMetricChart 回调
- componentProps 使用展开语法传递颜色常量
- 简化 useMemo 依赖数组
- 移除未使用的 imports

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 14:44:42 +08:00
parent 41da6fa372
commit ff951972ee

View File

@@ -3,7 +3,7 @@
* 重构后的主组件,使用模块化结构和 SubTabContainer 二级导航 * 重构后的主组件,使用模块化结构和 SubTabContainer 二级导航
*/ */
import React, { useState, useMemo, useCallback, ReactNode } from 'react'; import React, { useState, useMemo, useCallback } from 'react';
import { import {
Box, Box,
Container, Container,
@@ -13,21 +13,7 @@ import {
Text, Text,
Alert, Alert,
AlertIcon, AlertIcon,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure, useDisclosure,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Divider,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
BarChart3, BarChart3,
@@ -41,8 +27,6 @@ import {
Receipt, Receipt,
Banknote, Banknote,
} from 'lucide-react'; } from 'lucide-react';
import ReactECharts from 'echarts-for-react';
import { formatUtils } from '@services/financialService';
// 通用组件 // 通用组件
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer'; import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
@@ -51,8 +35,14 @@ import LoadingState from '../LoadingState';
// 内部模块导入 // 内部模块导入
import { useFinancialData, type DataTypeKey } from './hooks'; import { useFinancialData, type DataTypeKey } from './hooks';
import { COLORS } from './constants'; import { COLORS } from './constants';
import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils'; import { calculateYoYChange, getCellBackground } from './utils';
import { PeriodSelector, FinancialOverviewPanel, MainBusinessAnalysis, ComparisonAnalysis } from './components'; import {
PeriodSelector,
FinancialOverviewPanel,
MainBusinessAnalysis,
ComparisonAnalysis,
MetricChartModal,
} from './components';
import { import {
BalanceSheetTab, BalanceSheetTab,
IncomeStatementTab, IncomeStatementTab,
@@ -117,111 +107,22 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
// UI 状态 // UI 状态
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [modalContent, setModalContent] = useState<ReactNode>(null); const [modalProps, setModalProps] = useState<{
metricName: string;
data: Array<{ period: string; [key: string]: unknown }>;
dataPath: string;
}>({ metricName: '', data: [], dataPath: '' });
// 颜色配置 // 点击指标行显示图表
const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS;
// 点击指标行显示图表(使用 useCallback 避免不必要的重渲染)
const showMetricChart = useCallback(( const showMetricChart = useCallback((
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
) => { ) => {
const chartData = data setModalProps({ metricName, data, dataPath });
.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(); onOpen();
}, [onOpen, positiveColor, negativeColor]); }, [onOpen]);
// Tab 配置 - 财务指标分类 + 三大财务报表 // Tab 配置 - 财务指标分类 + 三大财务报表
const tabConfigs: SubTabConfig[] = useMemo( const tabConfigs: SubTabConfig[] = useMemo(
@@ -242,7 +143,7 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
[] []
); );
// 传递给 Tab 组件的 props // 传递给 Tab 组件的 props(颜色使用常量,不需要在依赖数组中)
const componentProps = useMemo( const componentProps = useMemo(
() => ({ () => ({
// 数据 // 数据
@@ -257,11 +158,8 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
showMetricChart, showMetricChart,
calculateYoYChange, calculateYoYChange,
getCellBackground, getCellBackground,
// 颜色配置 // 颜色配置(使用常量)
positiveColor, ...COLORS,
negativeColor,
bgColor,
hoverBg,
}), }),
[ [
balanceSheet, balanceSheet,
@@ -271,10 +169,6 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
loading, loading,
loadingTab, loadingTab,
showMetricChart, showMetricChart,
positiveColor,
negativeColor,
bgColor,
hoverBg,
] ]
); );
@@ -335,15 +229,14 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
</Alert> </Alert>
)} )}
{/* 弹出模态框 */} {/* 指标图表弹窗 */}
<Modal isOpen={isOpen} onClose={onClose} size="xl"> <MetricChartModal
<ModalOverlay /> isOpen={isOpen}
<ModalContent maxW="900px"> onClose={onClose}
<ModalHeader></ModalHeader> metricName={modalProps.metricName}
<ModalCloseButton /> data={modalProps.data}
<ModalBody pb={6}>{modalContent}</ModalBody> dataPath={modalProps.dataPath}
</ModalContent> />
</Modal>
</VStack> </VStack>
</Container> </Container>
); );