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() { - - - + )}