refactor(ForecastReport): 迁移至 TypeScript
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 通用图表卡片组件 - 黑金主题
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Heading } from '@chakra-ui/react';
|
||||
import { THEME } from '../constants';
|
||||
import type { ChartCardProps } from '../types';
|
||||
|
||||
const ChartCard: React.FC<ChartCardProps> = ({ title, children }) => {
|
||||
return (
|
||||
<Box
|
||||
bg={THEME.bgDark}
|
||||
border="1px solid"
|
||||
borderColor={THEME.goldBorder}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
px={4}
|
||||
py={3}
|
||||
borderBottom="1px solid"
|
||||
borderColor={THEME.goldBorder}
|
||||
bg={THEME.goldLight}
|
||||
>
|
||||
<Heading size="sm" color={THEME.gold}>
|
||||
{title}
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartCard;
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 详细数据表格 - 纯 Ant Design 黑金主题
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { Table, ConfigProvider, Tag, theme as antTheme } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { DetailTableProps, DetailTableRow } from '../types';
|
||||
|
||||
// Ant Design 黑金主题配置
|
||||
const BLACK_GOLD_THEME = {
|
||||
algorithm: antTheme.darkAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#D4AF37',
|
||||
colorBgContainer: '#1A202C',
|
||||
colorBgElevated: '#1a1a2e',
|
||||
colorBorder: 'rgba(212, 175, 55, 0.3)',
|
||||
colorText: '#e0e0e0',
|
||||
colorTextSecondary: '#a0a0a0',
|
||||
borderRadius: 4,
|
||||
fontSize: 13,
|
||||
},
|
||||
components: {
|
||||
Table: {
|
||||
headerBg: 'rgba(212, 175, 55, 0.1)',
|
||||
headerColor: '#D4AF37',
|
||||
rowHoverBg: 'rgba(212, 175, 55, 0.05)',
|
||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||
cellPaddingBlock: 8,
|
||||
cellPaddingInline: 12,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 表格样式
|
||||
const tableStyles = `
|
||||
.forecast-detail-table {
|
||||
background: #1A202C;
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.forecast-detail-table .table-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(212, 175, 55, 0.3);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
}
|
||||
.forecast-detail-table .table-header h4 {
|
||||
margin: 0;
|
||||
color: #D4AF37;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.forecast-detail-table .table-body {
|
||||
padding: 16px;
|
||||
}
|
||||
.forecast-detail-table .ant-table-cell-fix-left,
|
||||
.forecast-detail-table .ant-table-cell-fix-right {
|
||||
background: #1A202C !important;
|
||||
}
|
||||
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-left,
|
||||
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-right {
|
||||
background: rgba(26, 32, 44, 0.95) !important;
|
||||
}
|
||||
.forecast-detail-table .ant-table-tbody > tr:hover > td {
|
||||
background: rgba(212, 175, 55, 0.08) !important;
|
||||
}
|
||||
.forecast-detail-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-left {
|
||||
background: #242d3d !important;
|
||||
}
|
||||
.forecast-detail-table .ant-table-tbody > tr > td {
|
||||
background: #1A202C !important;
|
||||
}
|
||||
.forecast-detail-table .metric-tag {
|
||||
background: rgba(212, 175, 55, 0.15);
|
||||
border-color: rgba(212, 175, 55, 0.3);
|
||||
color: #D4AF37;
|
||||
}
|
||||
`;
|
||||
|
||||
interface TableRowData extends DetailTableRow {
|
||||
key: string;
|
||||
}
|
||||
|
||||
const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
|
||||
const { years, rows } = data;
|
||||
|
||||
// 构建列配置
|
||||
const columns: ColumnsType<TableRowData> = useMemo(() => {
|
||||
const cols: ColumnsType<TableRowData> = [
|
||||
{
|
||||
title: '关键指标',
|
||||
dataIndex: '指标',
|
||||
key: '指标',
|
||||
fixed: 'left',
|
||||
width: 160,
|
||||
render: (value: string) => (
|
||||
<Tag className="metric-tag">{value}</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 添加年份列
|
||||
years.forEach((year) => {
|
||||
cols.push({
|
||||
title: year,
|
||||
dataIndex: year,
|
||||
key: year,
|
||||
align: 'right',
|
||||
width: 100,
|
||||
render: (value: string | number | null) => value ?? '-',
|
||||
});
|
||||
});
|
||||
|
||||
return cols;
|
||||
}, [years]);
|
||||
|
||||
// 构建数据源
|
||||
const dataSource: TableRowData[] = useMemo(() => {
|
||||
return rows.map((row, idx) => ({
|
||||
...row,
|
||||
key: `row-${idx}`,
|
||||
}));
|
||||
}, [rows]);
|
||||
|
||||
return (
|
||||
<div className="forecast-detail-table">
|
||||
<style>{tableStyles}</style>
|
||||
<div className="table-header">
|
||||
<h4>详细数据表格</h4>
|
||||
</div>
|
||||
<div className="table-body">
|
||||
<ConfigProvider theme={BLACK_GOLD_THEME}>
|
||||
<Table<TableRowData>
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 'max-content' }}
|
||||
bordered
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailTable;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* EPS 趋势图
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ChartCard from './ChartCard';
|
||||
import { CHART_COLORS, BASE_CHART_CONFIG, CHART_HEIGHT, THEME } from '../constants';
|
||||
import type { EpsChartProps } from '../types';
|
||||
|
||||
const EpsChart: React.FC<EpsChartProps> = ({ data }) => {
|
||||
const option = useMemo(() => ({
|
||||
...BASE_CHART_CONFIG,
|
||||
color: [CHART_COLORS.eps],
|
||||
tooltip: {
|
||||
...BASE_CHART_CONFIG.tooltip,
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
...BASE_CHART_CONFIG.xAxis,
|
||||
type: 'category',
|
||||
data: data.years,
|
||||
},
|
||||
yAxis: {
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: '元/股',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'EPS(稀释)',
|
||||
type: 'line',
|
||||
data: data.eps,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 },
|
||||
areaStyle: { opacity: 0.15 },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
},
|
||||
],
|
||||
}), [data]);
|
||||
|
||||
return (
|
||||
<ChartCard title="EPS 趋势">
|
||||
<ReactECharts option={option} style={{ height: CHART_HEIGHT }} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default EpsChart;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 增长率分析图
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ChartCard from './ChartCard';
|
||||
import { BASE_CHART_CONFIG, CHART_HEIGHT, THEME } from '../constants';
|
||||
import type { GrowthChartProps } from '../types';
|
||||
|
||||
const GrowthChart: React.FC<GrowthChartProps> = ({ data }) => {
|
||||
const option = useMemo(() => ({
|
||||
...BASE_CHART_CONFIG,
|
||||
tooltip: {
|
||||
...BASE_CHART_CONFIG.tooltip,
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
...BASE_CHART_CONFIG.xAxis,
|
||||
type: 'category',
|
||||
data: data.years,
|
||||
},
|
||||
yAxis: {
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
...BASE_CHART_CONFIG.yAxis.axisLabel,
|
||||
formatter: '{value}%',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '营收增长率(%)',
|
||||
type: 'bar',
|
||||
data: data.revenue_growth_pct,
|
||||
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 ? `${params.value.toFixed(1)}%` : '',
|
||||
},
|
||||
},
|
||||
],
|
||||
}), [data]);
|
||||
|
||||
return (
|
||||
<ChartCard title="增长率分析">
|
||||
<ReactECharts option={option} style={{ height: CHART_HEIGHT }} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrowthChart;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 营业收入与净利润趋势图
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ChartCard from './ChartCard';
|
||||
import { CHART_COLORS, BASE_CHART_CONFIG, CHART_HEIGHT, THEME } from '../constants';
|
||||
import type { IncomeProfitChartProps } from '../types';
|
||||
|
||||
const IncomeProfitChart: React.FC<IncomeProfitChartProps> = ({ data }) => {
|
||||
const option = useMemo(() => ({
|
||||
...BASE_CHART_CONFIG,
|
||||
color: [CHART_COLORS.income, CHART_COLORS.profit],
|
||||
tooltip: {
|
||||
...BASE_CHART_CONFIG.tooltip,
|
||||
trigger: 'axis',
|
||||
},
|
||||
legend: {
|
||||
...BASE_CHART_CONFIG.legend,
|
||||
data: ['营业总收入(百万元)', '归母净利润(百万元)'],
|
||||
},
|
||||
xAxis: {
|
||||
...BASE_CHART_CONFIG.xAxis,
|
||||
type: 'category',
|
||||
data: data.years,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: '收入(百万元)',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
},
|
||||
{
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: '利润(百万元)',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '营业总收入(百万元)',
|
||||
type: 'line',
|
||||
data: data.income,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 },
|
||||
areaStyle: { opacity: 0.1 },
|
||||
},
|
||||
{
|
||||
name: '归母净利润(百万元)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: data.profit,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 },
|
||||
},
|
||||
],
|
||||
}), [data]);
|
||||
|
||||
return (
|
||||
<ChartCard title="营业收入与净利润趋势">
|
||||
<ReactECharts option={option} style={{ height: CHART_HEIGHT }} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncomeProfitChart;
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* PE 与 PEG 分析图
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ChartCard from './ChartCard';
|
||||
import { CHART_COLORS, BASE_CHART_CONFIG, CHART_HEIGHT, THEME } from '../constants';
|
||||
import type { PePegChartProps } from '../types';
|
||||
|
||||
const PePegChart: React.FC<PePegChartProps> = ({ data }) => {
|
||||
const option = useMemo(() => ({
|
||||
...BASE_CHART_CONFIG,
|
||||
color: [CHART_COLORS.pe, CHART_COLORS.peg],
|
||||
tooltip: {
|
||||
...BASE_CHART_CONFIG.tooltip,
|
||||
trigger: 'axis',
|
||||
},
|
||||
legend: {
|
||||
...BASE_CHART_CONFIG.legend,
|
||||
data: ['PE', 'PEG'],
|
||||
},
|
||||
xAxis: {
|
||||
...BASE_CHART_CONFIG.xAxis,
|
||||
type: 'category',
|
||||
data: data.years,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: 'PE(倍)',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
},
|
||||
{
|
||||
...BASE_CHART_CONFIG.yAxis,
|
||||
type: 'value',
|
||||
name: 'PEG',
|
||||
nameTextStyle: { color: THEME.textSecondary },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'PE',
|
||||
type: 'line',
|
||||
data: data.pe,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 },
|
||||
},
|
||||
{
|
||||
name: 'PEG',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: data.peg,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 },
|
||||
},
|
||||
],
|
||||
}), [data]);
|
||||
|
||||
return (
|
||||
<ChartCard title="PE 与 PEG 分析">
|
||||
<ReactECharts option={option} style={{ height: CHART_HEIGHT }} />
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default PePegChart;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* ForecastReport 子组件导出
|
||||
*/
|
||||
|
||||
export { default as ChartCard } from './ChartCard';
|
||||
export { default as IncomeProfitChart } from './IncomeProfitChart';
|
||||
export { default as GrowthChart } from './GrowthChart';
|
||||
export { default as EpsChart } from './EpsChart';
|
||||
export { default as PePegChart } from './PePegChart';
|
||||
export { default as DetailTable } from './DetailTable';
|
||||
84
src/views/Company/components/ForecastReport/constants.ts
Normal file
84
src/views/Company/components/ForecastReport/constants.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 盈利预测报表常量和图表配置
|
||||
*/
|
||||
|
||||
// 黑金主题配色
|
||||
export const THEME = {
|
||||
gold: '#D4AF37',
|
||||
goldLight: 'rgba(212, 175, 55, 0.1)',
|
||||
goldBorder: 'rgba(212, 175, 55, 0.3)',
|
||||
bgDark: '#1A202C',
|
||||
text: '#E2E8F0',
|
||||
textSecondary: '#A0AEC0',
|
||||
positive: '#E53E3E',
|
||||
negative: '#10B981',
|
||||
};
|
||||
|
||||
// 图表配色方案
|
||||
export const CHART_COLORS = {
|
||||
income: '#D4AF37', // 收入 - 金色
|
||||
profit: '#F6AD55', // 利润 - 橙金色
|
||||
growth: '#B8860B', // 增长 - 深金色
|
||||
eps: '#DAA520', // EPS - 金菊色
|
||||
pe: '#D4AF37', // PE - 金色
|
||||
peg: '#CD853F', // PEG - 秘鲁色
|
||||
};
|
||||
|
||||
// ECharts 基础配置(黑金主题)
|
||||
export const BASE_CHART_CONFIG = {
|
||||
backgroundColor: 'transparent',
|
||||
textStyle: {
|
||||
color: THEME.text,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(26, 32, 44, 0.95)',
|
||||
borderColor: THEME.goldBorder,
|
||||
textStyle: {
|
||||
color: THEME.text,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
textStyle: {
|
||||
color: THEME.textSecondary,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: 50,
|
||||
right: 20,
|
||||
bottom: 40,
|
||||
top: 40,
|
||||
containLabel: false,
|
||||
},
|
||||
xAxis: {
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: THEME.goldBorder,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: THEME.textSecondary,
|
||||
rotate: 30,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: THEME.goldBorder,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: THEME.textSecondary,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(212, 175, 55, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 图表高度
|
||||
export const CHART_HEIGHT = 280;
|
||||
@@ -1,161 +0,0 @@
|
||||
// 简易版公司盈利预测报表视图
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Flex, Input, Button, SimpleGrid, HStack, Text, Skeleton, VStack } from '@chakra-ui/react';
|
||||
import { Card, CardHeader, CardBody, Heading, Table, Thead, Tr, Th, Tbody, Td, Tag } from '@chakra-ui/react';
|
||||
import { RepeatIcon } from '@chakra-ui/icons';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { stockService } from '@services/eventService';
|
||||
|
||||
const ForecastReport = ({ stockCode: propStockCode }) => {
|
||||
const [code, setCode] = useState(propStockCode || '600000');
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
if (!code) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await stockService.getForecastReport(code);
|
||||
if (resp && resp.success) setData(resp.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听props中的stockCode变化
|
||||
useEffect(() => {
|
||||
if (propStockCode && propStockCode !== code) {
|
||||
setCode(propStockCode);
|
||||
}
|
||||
}, [propStockCode, code]);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
load();
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
const years = data?.detail_table?.years || [];
|
||||
|
||||
const colors = ['#805AD5', '#38B2AC', '#F6AD55', '#63B3ED', '#E53E3E', '#10B981'];
|
||||
|
||||
const incomeProfitOption = data ? {
|
||||
color: [colors[0], colors[4]],
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['营业总收入(百万元)', '归母净利润(百万元)'] },
|
||||
grid: { left: 40, right: 20, bottom: 40, top: 30 },
|
||||
xAxis: { type: 'category', data: data.income_profit_trend.years, axisLabel: { rotate: 30 } },
|
||||
yAxis: [
|
||||
{ type: 'value', name: '收入(百万元)' },
|
||||
{ type: 'value', name: '利润(百万元)' }
|
||||
],
|
||||
series: [
|
||||
{ name: '营业总收入(百万元)', type: 'line', data: data.income_profit_trend.income, smooth: true, lineStyle: { width: 2 }, areaStyle: { opacity: 0.08 } },
|
||||
{ name: '归母净利润(百万元)', type: 'line', yAxisIndex: 1, data: data.income_profit_trend.profit, smooth: true, lineStyle: { width: 2 } }
|
||||
]
|
||||
} : {};
|
||||
|
||||
const growthOption = data ? {
|
||||
color: [colors[2]],
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, bottom: 40, top: 30 },
|
||||
xAxis: { type: 'category', data: data.growth_bars.years, axisLabel: { rotate: 30 } },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
|
||||
series: [ {
|
||||
name: '营收增长率(%)',
|
||||
type: 'bar',
|
||||
data: data.growth_bars.revenue_growth_pct,
|
||||
itemStyle: { color: (params) => params.value >= 0 ? '#E53E3E' : '#10B981' }
|
||||
} ]
|
||||
} : {};
|
||||
|
||||
const epsOption = data ? {
|
||||
color: [colors[3]],
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, bottom: 40, top: 30 },
|
||||
xAxis: { type: 'category', data: data.eps_trend.years, axisLabel: { rotate: 30 } },
|
||||
yAxis: { type: 'value', name: '元/股' },
|
||||
series: [ { name: 'EPS(稀释)', type: 'line', data: data.eps_trend.eps, smooth: true, areaStyle: { opacity: 0.1 }, lineStyle: { width: 2 } } ]
|
||||
} : {};
|
||||
|
||||
const pePegOption = data ? {
|
||||
color: [colors[0], colors[1]],
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['PE', 'PEG'] },
|
||||
grid: { left: 40, right: 40, bottom: 40, top: 30 },
|
||||
xAxis: { type: 'category', data: data.pe_peg_axes.years, axisLabel: { rotate: 30 } },
|
||||
yAxis: [ { type: 'value', name: 'PE(倍)' }, { type: 'value', name: 'PEG' } ],
|
||||
series: [
|
||||
{ name: 'PE', type: 'line', data: data.pe_peg_axes.pe, smooth: true },
|
||||
{ name: 'PEG', type: 'line', yAxisIndex: 1, data: data.pe_peg_axes.peg, smooth: true }
|
||||
]
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<Box p={4}>
|
||||
<HStack align="center" justify="space-between" mb={4}>
|
||||
<Heading size="md">盈利预测报表</Heading>
|
||||
<Button
|
||||
leftIcon={<RepeatIcon />}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={load}
|
||||
isLoading={loading}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{loading && !data && (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{[1,2,3,4].map(i => (
|
||||
<Card key={i}>
|
||||
<CardHeader><Skeleton height="18px" width="140px" /></CardHeader>
|
||||
<CardBody>
|
||||
<Skeleton height="320px" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
<Card><CardHeader><Heading size="sm">营业收入与净利润趋势</Heading></CardHeader><CardBody><ReactECharts option={incomeProfitOption} style={{ height: 320 }} /></CardBody></Card>
|
||||
<Card><CardHeader><Heading size="sm">增长率分析</Heading></CardHeader><CardBody><ReactECharts option={growthOption} style={{ height: 320 }} /></CardBody></Card>
|
||||
<Card><CardHeader><Heading size="sm">EPS 趋势</Heading></CardHeader><CardBody><ReactECharts option={epsOption} style={{ height: 320 }} /></CardBody></Card>
|
||||
<Card><CardHeader><Heading size="sm">PE 与 PEG 分析</Heading></CardHeader><CardBody><ReactECharts option={pePegOption} style={{ height: 320 }} /></CardBody></Card>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<Card mt={4}>
|
||||
<CardHeader><Heading size="sm">详细数据表格</Heading></CardHeader>
|
||||
<CardBody>
|
||||
<Table size="sm" variant="simple">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>关键指标</Th>
|
||||
{years.map(y => <Th key={y}>{y}</Th>)}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data.detail_table.rows.map((row, idx) => (
|
||||
<Tr key={idx}>
|
||||
<Td><Tag>{row['指标']}</Tag></Td>
|
||||
{years.map(y => <Td key={y}>{row[y] ?? '-'}</Td>)}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForecastReport;
|
||||
|
||||
|
||||
115
src/views/Company/components/ForecastReport/index.tsx
Normal file
115
src/views/Company/components/ForecastReport/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 盈利预测报表视图 - 黑金主题
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, SimpleGrid, HStack, Heading, Skeleton, IconButton } from '@chakra-ui/react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { stockService } from '@services/eventService';
|
||||
import {
|
||||
IncomeProfitChart,
|
||||
GrowthChart,
|
||||
EpsChart,
|
||||
PePegChart,
|
||||
DetailTable,
|
||||
ChartCard,
|
||||
} from './components';
|
||||
import { THEME, CHART_HEIGHT } from './constants';
|
||||
import type { ForecastReportProps, ForecastData } from './types';
|
||||
|
||||
const ForecastReport: React.FC<ForecastReportProps> = ({ stockCode: propStockCode }) => {
|
||||
const [code, setCode] = useState(propStockCode || '600000');
|
||||
const [data, setData] = useState<ForecastData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!code) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await stockService.getForecastReport(code);
|
||||
if (resp && resp.success) {
|
||||
setData(resp.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
// 监听 props 中的 stockCode 变化
|
||||
useEffect(() => {
|
||||
if (propStockCode && propStockCode !== code) {
|
||||
setCode(propStockCode);
|
||||
}
|
||||
}, [propStockCode, code]);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
load();
|
||||
}
|
||||
}, [code, load]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* 标题栏 */}
|
||||
<HStack align="center" justify="space-between" mb={4}>
|
||||
<Heading size="md" color={THEME.gold}>
|
||||
盈利预测报表
|
||||
</Heading>
|
||||
<IconButton
|
||||
icon={<RefreshCw size={14} className={loading ? 'spin' : ''} />}
|
||||
onClick={load}
|
||||
isLoading={loading}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label="刷新数据"
|
||||
borderColor={THEME.goldBorder}
|
||||
color={THEME.gold}
|
||||
_hover={{
|
||||
bg: THEME.goldLight,
|
||||
borderColor: THEME.gold,
|
||||
}}
|
||||
/>
|
||||
<style>{`
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</HStack>
|
||||
|
||||
{/* 加载骨架屏 */}
|
||||
{loading && !data && (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<ChartCard key={i} title="加载中...">
|
||||
<Skeleton height={`${CHART_HEIGHT}px`} />
|
||||
</ChartCard>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{/* 图表区域 */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 详细数据表格 */}
|
||||
{data && (
|
||||
<Box mt={4}>
|
||||
<DetailTable data={data.detail_table} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForecastReport;
|
||||
81
src/views/Company/components/ForecastReport/types.ts
Normal file
81
src/views/Company/components/ForecastReport/types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 盈利预测报表类型定义
|
||||
*/
|
||||
|
||||
// 收入利润趋势数据
|
||||
export interface IncomeProfitTrend {
|
||||
years: string[];
|
||||
income: number[];
|
||||
profit: number[];
|
||||
}
|
||||
|
||||
// 增长率数据
|
||||
export interface GrowthBars {
|
||||
years: string[];
|
||||
revenue_growth_pct: number[];
|
||||
}
|
||||
|
||||
// EPS 趋势数据
|
||||
export interface EpsTrend {
|
||||
years: string[];
|
||||
eps: number[];
|
||||
}
|
||||
|
||||
// PE/PEG 数据
|
||||
export interface PePegAxes {
|
||||
years: string[];
|
||||
pe: number[];
|
||||
peg: number[];
|
||||
}
|
||||
|
||||
// 详细表格行数据
|
||||
export interface DetailTableRow {
|
||||
指标: string;
|
||||
[year: string]: string | number | null;
|
||||
}
|
||||
|
||||
// 详细表格数据
|
||||
export interface DetailTable {
|
||||
years: string[];
|
||||
rows: DetailTableRow[];
|
||||
}
|
||||
|
||||
// 完整的预测报表数据
|
||||
export interface ForecastData {
|
||||
income_profit_trend: IncomeProfitTrend;
|
||||
growth_bars: GrowthBars;
|
||||
eps_trend: EpsTrend;
|
||||
pe_peg_axes: PePegAxes;
|
||||
detail_table: DetailTable;
|
||||
}
|
||||
|
||||
// 组件 Props
|
||||
export interface ForecastReportProps {
|
||||
stockCode?: string;
|
||||
}
|
||||
|
||||
export interface ChartCardProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface IncomeProfitChartProps {
|
||||
data: IncomeProfitTrend;
|
||||
}
|
||||
|
||||
export interface GrowthChartProps {
|
||||
data: GrowthBars;
|
||||
}
|
||||
|
||||
export interface EpsChartProps {
|
||||
data: EpsTrend;
|
||||
}
|
||||
|
||||
export interface PePegChartProps {
|
||||
data: PePegAxes;
|
||||
}
|
||||
|
||||
export interface DetailTableProps {
|
||||
data: DetailTable;
|
||||
}
|
||||
Reference in New Issue
Block a user