refactor(ForecastReport): 合并营收/利润趋势与增长率图表
- 新增 IncomeProfitGrowthChart 合并组件 - 柱状图显示营业收入(左Y轴) - 折线图显示净利润(左Y轴,渐变填充) - 虚线显示增长率(右Y轴,红涨绿跌) - 布局调整:合并图表独占一行,EPS/PE-PEG 两列 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 营业收入、净利润趋势与增长率分析 - 合并图表
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ChartCard from './ChartCard';
|
||||
import { CHART_COLORS, BASE_CHART_CONFIG, THEME } from '../constants';
|
||||
import type { IncomeProfitTrend, GrowthBars } from '../types';
|
||||
|
||||
interface IncomeProfitGrowthChartProps {
|
||||
incomeProfitData: IncomeProfitTrend;
|
||||
growthData: GrowthBars;
|
||||
}
|
||||
|
||||
const IncomeProfitGrowthChart: React.FC<IncomeProfitGrowthChartProps> = ({
|
||||
incomeProfitData,
|
||||
growthData,
|
||||
}) => {
|
||||
const option = useMemo(() => ({
|
||||
...BASE_CHART_CONFIG,
|
||||
tooltip: {
|
||||
...BASE_CHART_CONFIG.tooltip,
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: {
|
||||
color: 'rgba(212, 175, 55, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
...BASE_CHART_CONFIG.legend,
|
||||
data: ['营业总收入', '归母净利润', '营收增长率'],
|
||||
bottom: 0,
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 60,
|
||||
bottom: 50,
|
||||
top: 40,
|
||||
containLabel: false,
|
||||
},
|
||||
xAxis: {
|
||||
...BASE_CHART_CONFIG.xAxis,
|
||||
type: 'category',
|
||||
data: incomeProfitData.years,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: '金额(百万元)',
|
||||
position: 'left',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
axisLabel: {
|
||||
color: THEME.textSecondary,
|
||||
formatter: (value: number) => {
|
||||
if (Math.abs(value) >= 1000) {
|
||||
return (value / 1000).toFixed(0) + 'k';
|
||||
}
|
||||
return value.toFixed(0);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: '增长率(%)',
|
||||
position: 'right',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
axisLabel: {
|
||||
color: THEME.textSecondary,
|
||||
formatter: '{value}%',
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '营业总收入',
|
||||
type: 'bar',
|
||||
data: incomeProfitData.income,
|
||||
itemStyle: {
|
||||
color: CHART_COLORS.income,
|
||||
},
|
||||
barMaxWidth: 30,
|
||||
},
|
||||
{
|
||||
name: '归母净利润',
|
||||
type: 'line',
|
||||
data: incomeProfitData.profit,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2, color: CHART_COLORS.profit },
|
||||
itemStyle: { color: CHART_COLORS.profit },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(246, 173, 85, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(246, 173, 85, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '营收增长率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: growthData.revenue_growth_pct,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2, type: 'dashed', color: '#10B981' },
|
||||
itemStyle: {
|
||||
color: (params: { value: number }) =>
|
||||
params.value >= 0 ? THEME.positive : THEME.negative,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
color: THEME.textSecondary,
|
||||
fontSize: 10,
|
||||
formatter: (params: { value: number }) =>
|
||||
params.value !== null && params.value !== undefined
|
||||
? `${params.value.toFixed(1)}%`
|
||||
: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
}), [incomeProfitData, growthData]);
|
||||
|
||||
return (
|
||||
<ChartCard title="营业收入与净利润趋势 · 增长率分析" height={320}>
|
||||
<ReactECharts option={option} style={{ height: 320 }} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncomeProfitGrowthChart;
|
||||
@@ -5,6 +5,7 @@
|
||||
export { default as ChartCard } from './ChartCard';
|
||||
export { default as IncomeProfitChart } from './IncomeProfitChart';
|
||||
export { default as GrowthChart } from './GrowthChart';
|
||||
export { default as IncomeProfitGrowthChart } from './IncomeProfitGrowthChart';
|
||||
export { default as EpsChart } from './EpsChart';
|
||||
export { default as PePegChart } from './PePegChart';
|
||||
export { default as DetailTable } from './DetailTable';
|
||||
|
||||
@@ -7,8 +7,7 @@ import { Box, SimpleGrid, HStack, Heading, Skeleton, IconButton } from '@chakra-
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { stockService } from '@services/eventService';
|
||||
import {
|
||||
IncomeProfitChart,
|
||||
GrowthChart,
|
||||
IncomeProfitGrowthChart,
|
||||
EpsChart,
|
||||
PePegChart,
|
||||
DetailTable,
|
||||
@@ -94,12 +93,20 @@ const ForecastReport: React.FC<ForecastReportProps> = ({ stockCode: propStockCod
|
||||
|
||||
{/* 图表区域 */}
|
||||
{data && (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
<IncomeProfitChart data={data.income_profit_trend} />
|
||||
<GrowthChart data={data.growth_bars} />
|
||||
<EpsChart data={data.eps_trend} />
|
||||
<PePegChart data={data.pe_peg_axes} />
|
||||
</SimpleGrid>
|
||||
<>
|
||||
{/* 合并图表:营收/利润/增长率 */}
|
||||
<Box mb={4}>
|
||||
<IncomeProfitGrowthChart
|
||||
incomeProfitData={data.income_profit_trend}
|
||||
growthData={data.growth_bars}
|
||||
/>
|
||||
</Box>
|
||||
{/* EPS 和 PE/PEG */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
<EpsChart data={data.eps_trend} />
|
||||
<PePegChart data={data.pe_peg_axes} />
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 详细数据表格 */}
|
||||
|
||||
Reference in New Issue
Block a user