From 292d3a007a2be6203665b265c4881b9204d713bd Mon Sep 17 00:00:00 2001
From: zdl <3489966805@qq.com>
Date: Wed, 24 Dec 2025 12:06:26 +0800
Subject: [PATCH] =?UTF-8?q?refactor(TradingSimulation):=20=E8=BF=81?=
=?UTF-8?q?=E7=A7=BB=20ApexCharts=20=E5=9B=BE=E8=A1=A8=E5=88=B0=20ECharts?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
图表组件迁移:
- 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 依赖做准备
---
src/components/Charts/BarChart.js | 50 -----
src/components/Charts/DonutChart.js | 51 -----
src/components/Charts/LineChart.js | 51 -----
src/components/Charts/PieChart.js | 51 -----
.../components/AccountOverview.js | 77 +-------
.../components/AssetAllocationChart.tsx | 143 ++++++++++++++
.../components/AssetTrendChart.tsx | 180 ++++++++++++++++++
.../components/PositionDistributionChart.tsx | 160 ++++++++++++++++
.../components/PositionsList.js | 120 +-----------
.../components/ProfitAnalysisChart.tsx | 161 ++++++++++++++++
.../hooks/useTradingAccount.js | 8 +-
src/views/TradingSimulation/index.js | 100 +---------
12 files changed, 666 insertions(+), 486 deletions(-)
delete mode 100755 src/components/Charts/BarChart.js
delete mode 100755 src/components/Charts/DonutChart.js
delete mode 100755 src/components/Charts/LineChart.js
delete mode 100755 src/components/Charts/PieChart.js
create mode 100644 src/views/TradingSimulation/components/AssetAllocationChart.tsx
create mode 100644 src/views/TradingSimulation/components/AssetTrendChart.tsx
create mode 100644 src/views/TradingSimulation/components/PositionDistributionChart.tsx
create mode 100644 src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
diff --git a/src/components/Charts/BarChart.js b/src/components/Charts/BarChart.js
deleted file mode 100755
index 7cdd8fbe..00000000
--- a/src/components/Charts/BarChart.js
+++ /dev/null
@@ -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 (
-
- );
- }
-}
-
-export default BarChart;
diff --git a/src/components/Charts/DonutChart.js b/src/components/Charts/DonutChart.js
deleted file mode 100755
index 78a18498..00000000
--- a/src/components/Charts/DonutChart.js
+++ /dev/null
@@ -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 (
-
- );
- }
-}
-
-export default DonutChart;
diff --git a/src/components/Charts/LineChart.js b/src/components/Charts/LineChart.js
deleted file mode 100755
index 86ea7178..00000000
--- a/src/components/Charts/LineChart.js
+++ /dev/null
@@ -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 (
-
- );
- }
-}
-
-export default LineChart;
diff --git a/src/components/Charts/PieChart.js b/src/components/Charts/PieChart.js
deleted file mode 100755
index 27845219..00000000
--- a/src/components/Charts/PieChart.js
+++ /dev/null
@@ -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 (
-
- );
- }
-}
-
-export default PieChart;
diff --git a/src/views/TradingSimulation/components/AccountOverview.js b/src/views/TradingSimulation/components/AccountOverview.js
index c677dc44..00b39307 100644
--- a/src/views/TradingSimulation/components/AccountOverview.js
+++ b/src/views/TradingSimulation/components/AccountOverview.js
@@ -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 (
@@ -330,12 +268,11 @@ export default function AccountOverview({ account, tradingEvents }) {
-
-
-
+
{/* 详细配置信息 */}
diff --git a/src/views/TradingSimulation/components/AssetAllocationChart.tsx b/src/views/TradingSimulation/components/AssetAllocationChart.tsx
new file mode 100644
index 00000000..9c32c1fa
--- /dev/null
+++ b/src/views/TradingSimulation/components/AssetAllocationChart.tsx
@@ -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}
${formatCurrency(params.value)}
占比: ${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 (
+
+ 暂无数据
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default AssetAllocationChart;
diff --git a/src/views/TradingSimulation/components/AssetTrendChart.tsx b/src/views/TradingSimulation/components/AssetTrendChart.tsx
new file mode 100644
index 00000000..7ca866bf
--- /dev/null
+++ b/src/views/TradingSimulation/components/AssetTrendChart.tsx
@@ -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}
总资产: ${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 (
+
+ 暂无数据
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default AssetTrendChart;
diff --git a/src/views/TradingSimulation/components/PositionDistributionChart.tsx b/src/views/TradingSimulation/components/PositionDistributionChart.tsx
new file mode 100644
index 00000000..486642b5
--- /dev/null
+++ b/src/views/TradingSimulation/components/PositionDistributionChart.tsx
@@ -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}
${formatCurrency(params.value)}
占比: ${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 (
+
+ 暂无持仓
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default PositionDistributionChart;
diff --git a/src/views/TradingSimulation/components/PositionsList.js b/src/views/TradingSimulation/components/PositionsList.js
index cca36095..f12d1bcb 100644
--- a/src/views/TradingSimulation/components/PositionsList.js
+++ b/src/views/TradingSimulation/components/PositionsList.js
@@ -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,107 +271,9 @@ 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 (
<>
@@ -463,12 +365,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
-
-
-
+
)}
@@ -490,12 +387,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
-
-
-
+
)}
diff --git a/src/views/TradingSimulation/components/ProfitAnalysisChart.tsx b/src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
new file mode 100644
index 00000000..6271c2eb
--- /dev/null
+++ b/src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
@@ -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}
盈亏: ${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 (
+
+ 暂无持仓
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default ProfitAnalysisChart;
diff --git a/src/views/TradingSimulation/hooks/useTradingAccount.js b/src/views/TradingSimulation/hooks/useTradingAccount.js
index b010e7b2..6534a720 100644
--- a/src/views/TradingSimulation/hooks/useTradingAccount.js
+++ b/src/views/TradingSimulation/hooks/useTradingAccount.js
@@ -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;
}
diff --git a/src/views/TradingSimulation/index.js b/src/views/TradingSimulation/index.js
index 9fd0e5c2..8695ba1e 100644
--- a/src/views/TradingSimulation/index.js
+++ b/src/views/TradingSimulation/index.js
@@ -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,92 +134,8 @@ 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) {
@@ -367,12 +280,7 @@ export default function TradingSimulation() {
-
-
-
+
)}