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 { 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';
|
||||||
|
|||||||
Reference in New Issue
Block a user