refactor(FinancialPanorama): 提取 MetricChartModal 组件

- 从 index.tsx 提取独立的指标图表弹窗组件
- 使用 memo 包装优化性能
- 包含图表展示和同比/环比计算表格
- 减少主组件约 100 行代码

🤖 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:20 +08:00
parent 7b58f83490
commit 0e29f1aff4
2 changed files with 175 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
/**
* 指标图表弹窗组件
*
* 显示指标的趋势图表和详细数据表格
*/
import React, { useMemo, memo } from 'react';
import {
Box,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Divider,
} from '@chakra-ui/react';
import ReactECharts from 'echarts-for-react';
import { formatUtils } from '@services/financialService';
import { getMetricChartOption } from '../utils';
import { COLORS } from '../constants';
/** 图表数据项 */
interface ChartDataItem {
period: string;
date: string;
value: number;
}
/** 组件 Props */
export interface MetricChartModalProps {
/** 是否打开 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 指标名称 */
metricName: string;
/** 原始数据 */
data: Array<{ period: string; [key: string]: unknown }>;
/** 数据路径 */
dataPath: string;
}
/**
* 从数据路径获取值
*/
const getValueByPath = (item: Record<string, unknown>, path: string): number | undefined => {
return path.split('.').reduce((obj: unknown, key: string) => {
if (obj && typeof obj === 'object') {
return (obj as Record<string, unknown>)[key];
}
return undefined;
}, item) as number | undefined;
};
/**
* 指标图表弹窗
*/
const MetricChartModalInner: React.FC<MetricChartModalProps> = ({
isOpen,
onClose,
metricName,
data,
dataPath,
}) => {
const { positiveColor, negativeColor } = COLORS;
// 计算图表数据
const chartData = useMemo((): ChartDataItem[] => {
if (!data || data.length === 0) return [];
return data
.map((item) => {
const value = getValueByPath(item as Record<string, unknown>, dataPath);
return {
period: formatUtils.getReportType(item.period),
date: item.period,
value: value ?? 0,
};
})
.reverse();
}, [data, dataPath]);
// 图表配置
const chartOption = useMemo(() => {
return getMetricChartOption(metricName, chartData);
}, [metricName, chartData]);
// 计算同比环比
const tableRows = useMemo(() => {
return chartData.map((item, idx) => {
// 计算环比 (QoQ)
const qoq =
idx > 0 && chartData[idx - 1].value !== 0
? ((item.value - chartData[idx - 1].value) / Math.abs(chartData[idx - 1].value)) * 100
: null;
// 计算同比 (YoY)
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 && lastYearItem.value !== 0
? ((item.value - lastYearItem.value) / Math.abs(lastYearItem.value)) * 100
: null;
return { ...item, yoy, qoq };
});
}, [chartData]);
// 获取变化颜色
const getChangeColor = (value: number | null) => {
if (value === null) return 'gray.500';
return value > 0 ? positiveColor : value < 0 ? negativeColor : 'gray.500';
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent maxW="900px">
<ModalHeader>{metricName} - </ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<Box>
<ReactECharts option={chartOption} 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>
{tableRows.map((item, idx) => (
<Tr key={idx}>
<Td>{item.period}</Td>
<Td isNumeric>{formatUtils.formatLargeNumber(item.value)}</Td>
<Td isNumeric color={getChangeColor(item.yoy)}>
{item.yoy !== null ? `${item.yoy.toFixed(2)}%` : '-'}
</Td>
<Td isNumeric color={getChangeColor(item.qoq)}>
{item.qoq !== null ? `${item.qoq.toFixed(2)}%` : '-'}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</ModalBody>
</ModalContent>
</Modal>
);
};
export const MetricChartModal = memo(MetricChartModalInner);
export default MetricChartModal;

View File

@@ -15,3 +15,5 @@ export { MainBusinessAnalysis } from './MainBusinessAnalysis';
export { IndustryRankingView } from './IndustryRankingView'; export { IndustryRankingView } from './IndustryRankingView';
export { StockComparison } from './StockComparison'; export { StockComparison } from './StockComparison';
export { ComparisonAnalysis } from './ComparisonAnalysis'; export { ComparisonAnalysis } from './ComparisonAnalysis';
export { MetricChartModal } from './MetricChartModal';
export type { MetricChartModalProps } from './MetricChartModal';