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:
zdl
2025-12-24 12:06:26 +08:00
parent a27065e613
commit 292d3a007a
12 changed files with 666 additions and 486 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}>

View 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;

View 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;

View File

@@ -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;

View File

@@ -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>
)}

View 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;

View File

@@ -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;
}

View File

@@ -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>
)}