refactor(TradingSimulation): 迁移 ApexCharts 图表到 ECharts
图表组件迁移: - AssetTrendChart: 资产走势折线图 → ECharts 面积图 - AssetAllocationChart: 资产配置环形图 → ECharts 饼图 - PositionDistributionChart: 持仓分布饼图 → ECharts 饼图 - ProfitAnalysisChart: 盈亏分析柱状图 → ECharts 柱状图 删除的 ApexCharts 组件: - src/components/Charts/LineChart.js - src/components/Charts/BarChart.js - src/components/Charts/PieChart.js - src/components/Charts/DonutChart.js 技术改进: - 统一使用 ECharts 作为通用图表库 - 新组件使用 TypeScript,类型安全 - 为后续移除 apexcharts 依赖做准备
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Argon Dashboard Chakra PRO - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
|
||||
|
||||
* Designed and Coded by Simmmple & Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
|
||||
import React, { Component } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
|
||||
class BarChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
chartData: [],
|
||||
chartOptions: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
chartData: this.props.chartData,
|
||||
chartOptions: this.props.chartOptions,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.chartOptions}
|
||||
series={this.state.chartData}
|
||||
type="bar"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BarChart;
|
||||
@@ -1,51 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Argon Dashboard Chakra PRO - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
|
||||
|
||||
* Designed and Coded by Simmmple & Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
|
||||
class DonutChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
chartData: [],
|
||||
chartOptions: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
chartData: this.props.chartData,
|
||||
chartOptions: this.props.chartOptions,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactApexChart
|
||||
options={this.state.chartOptions}
|
||||
series={this.state.chartData}
|
||||
type="donut"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DonutChart;
|
||||
@@ -1,51 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Argon Dashboard Chakra PRO - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
|
||||
|
||||
* Designed and Coded by Simmmple & Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
|
||||
class LineChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
chartData: [],
|
||||
chartOptions: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
chartData: this.props.chartData,
|
||||
chartOptions: this.props.chartOptions,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactApexChart
|
||||
options={this.state.chartOptions}
|
||||
series={this.state.chartData}
|
||||
type="area"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LineChart;
|
||||
@@ -1,51 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Argon Dashboard Chakra PRO - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
|
||||
|
||||
* Designed and Coded by Simmmple & Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import ReactApexChart from "react-apexcharts";
|
||||
|
||||
class PieChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
chartData: [],
|
||||
chartOptions: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
chartData: this.props.chartData,
|
||||
chartOptions: this.props.chartOptions,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactApexChart
|
||||
options={this.state.chartOptions}
|
||||
series={this.state.chartData}
|
||||
type="pie"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PieChart;
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiActivity } from 'react-icons/fi';
|
||||
|
||||
// 导入现有的图表组件
|
||||
import DonutChart from '../../../components/Charts/DonutChart';
|
||||
// 导入图表组件
|
||||
import { AssetAllocationChart } from './AssetAllocationChart';
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
|
||||
export default function AccountOverview({ account, tradingEvents }) {
|
||||
@@ -65,68 +65,6 @@ export default function AccountOverview({ account, tradingEvents }) {
|
||||
return `${(percent || 0) >= 0 ? '+' : ''}${(percent || 0).toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// 安全地准备资产配置饼图数据
|
||||
const assetAllocationData = account?.totalAssets > 0 ? [
|
||||
(account.availableCash / account.totalAssets) * 100,
|
||||
(account.marketValue / account.totalAssets) * 100
|
||||
] : [100, 0];
|
||||
|
||||
const assetAllocationOptions = {
|
||||
labels: ['现金资产', '股票资产'],
|
||||
colors: ['#4299E1', '#48BB78'],
|
||||
chart: {
|
||||
width: "100%",
|
||||
height: "280px"
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
fontSize: '12px',
|
||||
labels: {
|
||||
colors: textColor
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
colors: ['#fff']
|
||||
},
|
||||
formatter: function (val) {
|
||||
return (val || 0).toFixed(1) + "%"
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
expandOnClick: false,
|
||||
donut: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
colors: ['#4299E1', '#48BB78'],
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return formatCurrency(val / 100 * (account?.totalAssets || 0))
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||
@@ -330,12 +268,11 @@ export default function AccountOverview({ account, tradingEvents }) {
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="280px">
|
||||
<DonutChart
|
||||
chartData={assetAllocationData}
|
||||
chartOptions={assetAllocationOptions}
|
||||
/>
|
||||
</Box>
|
||||
<AssetAllocationChart
|
||||
cashAmount={account?.availableCash || 0}
|
||||
stockAmount={account?.marketValue || 0}
|
||||
height={280}
|
||||
/>
|
||||
|
||||
{/* 详细配置信息 */}
|
||||
<VStack spacing={3} mt={4}>
|
||||
|
||||
143
src/views/TradingSimulation/components/AssetAllocationChart.tsx
Normal file
143
src/views/TradingSimulation/components/AssetAllocationChart.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 资产配置环形图组件
|
||||
* 使用 ECharts 展示现金/股票资产占比
|
||||
*/
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface AssetAllocationChartProps {
|
||||
cashAmount: number;
|
||||
stockAmount: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产配置环形图
|
||||
* @param cashAmount - 现金资产金额
|
||||
* @param stockAmount - 股票资产金额
|
||||
* @param height - 图表高度,默认 280px
|
||||
*/
|
||||
export const AssetAllocationChart = ({
|
||||
cashAmount,
|
||||
stockAmount,
|
||||
height = 280
|
||||
}: AssetAllocationChartProps) => {
|
||||
const total = cashAmount + stockAmount;
|
||||
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: (params: { name: string; value: number; percent: number }) => {
|
||||
return `${params.name}<br/>${formatCurrency(params.value)}<br/>占比: ${params.percent.toFixed(1)}%`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
},
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
itemGap: 20
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '资产配置',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
center: ['50%', '45%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: (params: { name: string; percent: number }) => {
|
||||
return `${params.name}\n${params.percent.toFixed(1)}%`;
|
||||
},
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
lineHeight: 18
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 15,
|
||||
length2: 10,
|
||||
smooth: true
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: cashAmount,
|
||||
name: '现金资产',
|
||||
itemStyle: { color: '#4299E1' }
|
||||
},
|
||||
{
|
||||
value: stockAmount,
|
||||
name: '股票资产',
|
||||
itemStyle: { color: '#48BB78' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无数据
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetAllocationChart;
|
||||
180
src/views/TradingSimulation/components/AssetTrendChart.tsx
Normal file
180
src/views/TradingSimulation/components/AssetTrendChart.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 资产走势图表组件
|
||||
* 使用 ECharts 的面积图模式展示资产走势
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface AssetHistoryItem {
|
||||
date: string;
|
||||
closing_assets?: number;
|
||||
total_assets?: number;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
interface AssetTrendChartProps {
|
||||
data: AssetHistoryItem[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产走势图表
|
||||
* @param data - 资产历史数据数组
|
||||
* @param height - 图表高度,默认 350px
|
||||
*/
|
||||
export const AssetTrendChart = ({ data, height = 350 }: AssetTrendChartProps) => {
|
||||
const option = useMemo(() => {
|
||||
if (!data || data.length === 0) {
|
||||
return {
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#999', fontSize: 14 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 提取数据
|
||||
const dates = data.map(item => item.date || '');
|
||||
const values = data.map(item => item.closing_assets || item.total_assets || item.value || 0);
|
||||
|
||||
// 判断涨跌(首尾对比)
|
||||
const isUp = values[values.length - 1] >= values[0];
|
||||
const lineColor = isUp ? '#48BB78' : '#F56565';
|
||||
const areaColorStart = isUp ? 'rgba(72, 187, 120, 0.4)' : 'rgba(245, 101, 101, 0.4)';
|
||||
const areaColorEnd = isUp ? 'rgba(72, 187, 120, 0.05)' : 'rgba(245, 101, 101, 0.05)';
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: { color: '#fff' },
|
||||
formatter: (params: { name: string; value: number }[]) => {
|
||||
const item = params[0];
|
||||
const value = item.value || 0;
|
||||
const formatted = new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY'
|
||||
}).format(value);
|
||||
return `${item.name}<br/>总资产: ${formatted}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: { color: '#e0e0e0' }
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: '#888888',
|
||||
fontSize: 12,
|
||||
formatter: (value: string) => {
|
||||
// 简化日期显示:MM-DD
|
||||
if (value && value.length >= 10) {
|
||||
return value.substring(5, 10);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true, // 关键:启用 scale 让 Y 轴自适应数据范围,不从 0 开始
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: '#888888',
|
||||
fontSize: 12,
|
||||
formatter: (value: number) => {
|
||||
if (value >= 10000) {
|
||||
return `¥${(value / 10000).toFixed(1)}万`;
|
||||
}
|
||||
return `¥${value.toFixed(0)}`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#e0e0e0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总资产',
|
||||
type: 'line',
|
||||
data: values,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
color: lineColor,
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: lineColor
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: areaColorStart },
|
||||
{ offset: 1, color: areaColorEnd }
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无数据
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetTrendChart;
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 持仓分布饼图组件
|
||||
* 使用 ECharts 展示各股票持仓占比
|
||||
*/
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface PositionItem {
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
marketValue: number;
|
||||
}
|
||||
|
||||
interface PositionDistributionChartProps {
|
||||
positions: PositionItem[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// 预设颜色调色板
|
||||
const COLORS = [
|
||||
'#4299E1', // 蓝色
|
||||
'#48BB78', // 绿色
|
||||
'#ED8936', // 橙色
|
||||
'#9F7AEA', // 紫色
|
||||
'#F56565', // 红色
|
||||
'#38B2AC', // 青色
|
||||
'#ECC94B', // 黄色
|
||||
'#667EEA', // 靛蓝色
|
||||
'#FC8181', // 浅红色
|
||||
'#68D391' // 浅绿色
|
||||
];
|
||||
|
||||
/**
|
||||
* 持仓分布饼图
|
||||
* @param positions - 持仓列表
|
||||
* @param height - 图表高度,默认 300px
|
||||
*/
|
||||
export const PositionDistributionChart = ({
|
||||
positions,
|
||||
height = 300
|
||||
}: PositionDistributionChartProps) => {
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// 准备数据
|
||||
const data = positions.map((pos, index) => ({
|
||||
value: pos.marketValue || 0,
|
||||
name: pos.stockName || pos.stockCode,
|
||||
itemStyle: { color: COLORS[index % COLORS.length] }
|
||||
}));
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: (params: { name: string; value: number; percent: number }) => {
|
||||
return `${params.name}<br/>${formatCurrency(params.value)}<br/>占比: ${params.percent.toFixed(1)}%`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
},
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 8,
|
||||
pageButtonItemGap: 5,
|
||||
pageButtonGap: 10,
|
||||
pageIconColor: '#4299E1',
|
||||
pageIconInactiveColor: '#ccc',
|
||||
pageTextStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '持仓分布',
|
||||
type: 'pie',
|
||||
radius: ['0%', '70%'],
|
||||
center: ['35%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: (params: { percent: number }) => {
|
||||
return params.percent >= 10 ? `${params.percent.toFixed(1)}%` : '';
|
||||
},
|
||||
fontSize: 11,
|
||||
color: '#fff',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
data
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (!positions || positions.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无持仓
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PositionDistributionChart;
|
||||
@@ -50,9 +50,9 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { FiTrendingUp, FiTrendingDown, FiMinus, FiBarChart2, FiPieChart } from 'react-icons/fi';
|
||||
|
||||
// 导入现有的高质量组件
|
||||
import BarChart from '../../../components/Charts/BarChart';
|
||||
import PieChart from '../../../components/Charts/PieChart';
|
||||
// 导入图表组件
|
||||
import { PositionDistributionChart } from './PositionDistributionChart';
|
||||
import { ProfitAnalysisChart } from './ProfitAnalysisChart';
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
@@ -271,108 +271,10 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
||||
);
|
||||
}
|
||||
|
||||
// 安全地准备持仓分布图表数据
|
||||
// 安全地准备持仓数据
|
||||
const safePositions = Array.isArray(positions) ? positions : [];
|
||||
const hasPositions = safePositions.length > 0;
|
||||
|
||||
const positionDistributionData = hasPositions ? safePositions.map(pos => pos?.marketValue || 0) : [];
|
||||
const positionDistributionLabels = hasPositions ? safePositions.map(pos => pos?.stockName || pos?.stockCode || '') : [];
|
||||
|
||||
const positionDistributionOptions = {
|
||||
labels: positionDistributionLabels,
|
||||
colors: ['#4299E1', '#48BB78', '#ED8936', '#9F7AEA', '#F56565', '#38B2AC', '#ECC94B'],
|
||||
chart: {
|
||||
width: "100%",
|
||||
height: "300px"
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: '12px'
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: function (val) {
|
||||
return (val || 0).toFixed(1) + "%"
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return formatCurrency(val || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 安全地准备盈亏分布柱状图数据
|
||||
const profitBarData = hasPositions ? [{
|
||||
name: '盈亏分布',
|
||||
data: safePositions.map(pos => pos?.profit || 0)
|
||||
}] : [];
|
||||
|
||||
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
|
||||
|
||||
const profitBarOptions = {
|
||||
chart: {
|
||||
toolbar: { show: false },
|
||||
height: 300
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 8,
|
||||
columnWidth: "60%",
|
||||
colors: {
|
||||
ranges: [{
|
||||
from: -1000000,
|
||||
to: 0,
|
||||
color: '#F56565'
|
||||
}, {
|
||||
from: 0.01,
|
||||
to: 1000000,
|
||||
color: '#48BB78'
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: hasPositions ? safePositions.map(pos => pos?.stockCode || '') : [],
|
||||
labels: {
|
||||
style: {
|
||||
colors: xAxisLabelColor,
|
||||
fontSize: '12px'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: yAxisLabelColor,
|
||||
fontSize: '12px'
|
||||
},
|
||||
formatter: function (val) {
|
||||
return '¥' + ((val || 0) / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return formatCurrency(val || 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 5,
|
||||
borderColor: gridBorderColor
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack spacing={6} align="stretch">
|
||||
@@ -463,12 +365,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="300px">
|
||||
<PieChart
|
||||
chartData={positionDistributionData}
|
||||
chartOptions={positionDistributionOptions}
|
||||
/>
|
||||
</Box>
|
||||
<PositionDistributionChart positions={safePositions} height={300} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
@@ -490,12 +387,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="300px">
|
||||
<BarChart
|
||||
chartData={profitBarData}
|
||||
chartOptions={profitBarOptions}
|
||||
/>
|
||||
</Box>
|
||||
<ProfitAnalysisChart positions={safePositions} height={300} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
161
src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
Normal file
161
src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 盈亏分析柱状图组件
|
||||
* 使用 ECharts 展示各股票盈亏情况
|
||||
*/
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface PositionItem {
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
profit: number;
|
||||
}
|
||||
|
||||
interface ProfitAnalysisChartProps {
|
||||
positions: PositionItem[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 盈亏分析柱状图
|
||||
* @param positions - 持仓列表(含盈亏数据)
|
||||
* @param height - 图表高度,默认 300px
|
||||
*/
|
||||
export const ProfitAnalysisChart = ({
|
||||
positions,
|
||||
height = 300
|
||||
}: ProfitAnalysisChartProps) => {
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// 准备数据
|
||||
const categories = positions.map(pos => pos.stockCode || pos.stockName);
|
||||
const profits = positions.map(pos => pos.profit || 0);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: { name: string; value: number }[]) => {
|
||||
const data = params[0];
|
||||
const pos = positions.find(p => p.stockCode === data.name || p.stockName === data.name);
|
||||
const stockName = pos?.stockName || data.name;
|
||||
return `${stockName}<br/>盈亏: ${formatCurrency(data.value)}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e0e0e0'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#718096',
|
||||
fontSize: 12,
|
||||
rotate: categories.length > 5 ? 45 : 0,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#718096',
|
||||
fontSize: 12,
|
||||
formatter: (value: number) => {
|
||||
if (Math.abs(value) >= 10000) {
|
||||
return `¥${(value / 10000).toFixed(1)}万`;
|
||||
} else if (Math.abs(value) >= 1000) {
|
||||
return `¥${(value / 1000).toFixed(1)}k`;
|
||||
}
|
||||
return `¥${value.toFixed(0)}`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#E2E8F0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '盈亏',
|
||||
type: 'bar',
|
||||
barWidth: '60%',
|
||||
barMaxWidth: 50,
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: (params: { value: number }) => {
|
||||
return params.value >= 0 ? '#48BB78' : '#F56565';
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
data: profits
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (!positions || positions.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无持仓
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfitAnalysisChart;
|
||||
@@ -321,11 +321,13 @@ export function useTradingAccount() {
|
||||
// 调试模式:demo用户返回模拟数据,避免CORS
|
||||
if (!user || user.id === 'demo') {
|
||||
const now = Date.now();
|
||||
const baseAssets = 1000000; // 起始资产100万
|
||||
const data = Array.from({ length: days }, (_, i) => {
|
||||
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
|
||||
// 简单生成一条平滑的收益曲线
|
||||
const value = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||||
return { date: date.toISOString().slice(0, 10), value };
|
||||
// 生成一条平滑的资产曲线(在100万附近波动,整体上涨)
|
||||
const dailyReturn = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||||
const totalAssets = baseAssets * (1 + dailyReturn);
|
||||
return { date: date.toISOString().slice(0, 10), total_assets: totalAssets };
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ import PositionsList from './components/PositionsList';
|
||||
import TradingHistory from './components/TradingHistory';
|
||||
import MarginTrading from './components/MarginTrading';
|
||||
|
||||
// 导入现有的高质量组件
|
||||
import LineChart from '../../components/Charts/LineChart';
|
||||
// 导入图表组件
|
||||
import { AssetTrendChart } from './components/AssetTrendChart';
|
||||
|
||||
// 导航栏已由 MainLayout 提供,无需在此导入
|
||||
|
||||
@@ -89,9 +89,6 @@ export default function TradingSimulation() {
|
||||
// 所有的 useColorModeValue 也必须在顶部
|
||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
|
||||
const contentTextColor = useColorModeValue('gray.700', 'white');
|
||||
|
||||
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
|
||||
@@ -137,93 +134,9 @@ export default function TradingSimulation() {
|
||||
}, [account, getAssetHistory]);
|
||||
|
||||
// ========== 3. 数据处理和计算(不是 Hooks,可以放在这里)==========
|
||||
// 准备资产走势图表数据(使用真实数据,安全处理)
|
||||
// 检查是否有资产历史数据
|
||||
const hasAssetData = Array.isArray(assetHistory) && assetHistory.length > 0;
|
||||
|
||||
const assetTrendData = hasAssetData ? [{
|
||||
name: "总资产",
|
||||
data: assetHistory.map(item => {
|
||||
// 安全地获取数据,避免undefined错误
|
||||
if (!item) return 0;
|
||||
return item.closing_assets || item.total_assets || 0;
|
||||
})
|
||||
}] : [];
|
||||
|
||||
const assetTrendOptions = hasAssetData ? {
|
||||
chart: {
|
||||
toolbar: { show: false },
|
||||
height: 350
|
||||
},
|
||||
tooltip: {
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return '¥' + (val || 0).toLocaleString()
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 3
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
categories: assetHistory.map(item => {
|
||||
// 安全地获取日期
|
||||
if (!item) return '';
|
||||
return item.date || '';
|
||||
}),
|
||||
labels: {
|
||||
style: {
|
||||
colors: xAxisLabelColor,
|
||||
fontSize: "12px",
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: yAxisLabelColor,
|
||||
fontSize: "12px",
|
||||
},
|
||||
formatter: function(val) {
|
||||
return '¥' + ((val || 0) / 10000).toFixed(1) + 'w'
|
||||
}
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 5,
|
||||
borderColor: gridBorderColor
|
||||
},
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shade: "light",
|
||||
type: "vertical",
|
||||
shadeIntensity: 0.5,
|
||||
gradientToColors: undefined,
|
||||
inverseColors: true,
|
||||
opacityFrom: 0.8,
|
||||
opacityTo: 0,
|
||||
stops: [],
|
||||
},
|
||||
colors: [account?.totalProfit >= 0 ? "#48BB78" : "#F56565"],
|
||||
},
|
||||
colors: [account?.totalProfit >= 0 ? "#48BB78" : "#F56565"],
|
||||
} : {};
|
||||
|
||||
// ========== 4. 现在可以安全地进行条件返回了 ==========
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -367,12 +280,7 @@ export default function TradingSimulation() {
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="350px">
|
||||
<LineChart
|
||||
chartData={assetTrendData}
|
||||
chartOptions={assetTrendOptions}
|
||||
/>
|
||||
</Box>
|
||||
<AssetTrendChart data={assetHistory} height={350} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user