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:
@@ -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;
|
||||
@@ -15,3 +15,5 @@ export { MainBusinessAnalysis } from './MainBusinessAnalysis';
|
||||
export { IndustryRankingView } from './IndustryRankingView';
|
||||
export { StockComparison } from './StockComparison';
|
||||
export { ComparisonAnalysis } from './ComparisonAnalysis';
|
||||
export { MetricChartModal } from './MetricChartModal';
|
||||
export type { MetricChartModalProps } from './MetricChartModal';
|
||||
|
||||
Reference in New Issue
Block a user