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';
|
} from '@chakra-ui/react';
|
||||||
import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiActivity } from 'react-icons/fi';
|
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';
|
import IconBox from '../../../components/Icons/IconBox';
|
||||||
|
|
||||||
export default function AccountOverview({ account, tradingEvents }) {
|
export default function AccountOverview({ account, tradingEvents }) {
|
||||||
@@ -65,68 +65,6 @@ export default function AccountOverview({ account, tradingEvents }) {
|
|||||||
return `${(percent || 0) >= 0 ? '+' : ''}${(percent || 0).toFixed(2)}%`;
|
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 (
|
return (
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||||
@@ -330,12 +268,11 @@ export default function AccountOverview({ account, tradingEvents }) {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Box h="280px">
|
<AssetAllocationChart
|
||||||
<DonutChart
|
cashAmount={account?.availableCash || 0}
|
||||||
chartData={assetAllocationData}
|
stockAmount={account?.marketValue || 0}
|
||||||
chartOptions={assetAllocationOptions}
|
height={280}
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 详细配置信息 */}
|
{/* 详细配置信息 */}
|
||||||
<VStack spacing={3} mt={4}>
|
<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';
|
} from '@chakra-ui/react';
|
||||||
import { FiTrendingUp, FiTrendingDown, FiMinus, FiBarChart2, FiPieChart } from 'react-icons/fi';
|
import { FiTrendingUp, FiTrendingDown, FiMinus, FiBarChart2, FiPieChart } from 'react-icons/fi';
|
||||||
|
|
||||||
// 导入现有的高质量组件
|
// 导入图表组件
|
||||||
import BarChart from '../../../components/Charts/BarChart';
|
import { PositionDistributionChart } from './PositionDistributionChart';
|
||||||
import PieChart from '../../../components/Charts/PieChart';
|
import { ProfitAnalysisChart } from './ProfitAnalysisChart';
|
||||||
import IconBox from '../../../components/Icons/IconBox';
|
import IconBox from '../../../components/Icons/IconBox';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
@@ -271,108 +271,10 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全地准备持仓分布图表数据
|
// 安全地准备持仓数据
|
||||||
const safePositions = Array.isArray(positions) ? positions : [];
|
const safePositions = Array.isArray(positions) ? positions : [];
|
||||||
const hasPositions = safePositions.length > 0;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<VStack spacing={6} align="stretch">
|
<VStack spacing={6} align="stretch">
|
||||||
@@ -463,12 +365,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
|||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Box h="300px">
|
<PositionDistributionChart positions={safePositions} height={300} />
|
||||||
<PieChart
|
|
||||||
chartData={positionDistributionData}
|
|
||||||
chartOptions={positionDistributionOptions}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -490,12 +387,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
|||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Box h="300px">
|
<ProfitAnalysisChart positions={safePositions} height={300} />
|
||||||
<BarChart
|
|
||||||
chartData={profitBarData}
|
|
||||||
chartOptions={profitBarOptions}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</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
|
// 调试模式:demo用户返回模拟数据,避免CORS
|
||||||
if (!user || user.id === 'demo') {
|
if (!user || user.id === 'demo') {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const baseAssets = 1000000; // 起始资产100万
|
||||||
const data = Array.from({ length: days }, (_, i) => {
|
const data = Array.from({ length: days }, (_, i) => {
|
||||||
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
|
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
|
||||||
// 简单生成一条平滑的收益曲线
|
// 生成一条平滑的资产曲线(在100万附近波动,整体上涨)
|
||||||
const value = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
const dailyReturn = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||||||
return { date: date.toISOString().slice(0, 10), value };
|
const totalAssets = baseAssets * (1 + dailyReturn);
|
||||||
|
return { date: date.toISOString().slice(0, 10), total_assets: totalAssets };
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ import PositionsList from './components/PositionsList';
|
|||||||
import TradingHistory from './components/TradingHistory';
|
import TradingHistory from './components/TradingHistory';
|
||||||
import MarginTrading from './components/MarginTrading';
|
import MarginTrading from './components/MarginTrading';
|
||||||
|
|
||||||
// 导入现有的高质量组件
|
// 导入图表组件
|
||||||
import LineChart from '../../components/Charts/LineChart';
|
import { AssetTrendChart } from './components/AssetTrendChart';
|
||||||
|
|
||||||
// 导航栏已由 MainLayout 提供,无需在此导入
|
// 导航栏已由 MainLayout 提供,无需在此导入
|
||||||
|
|
||||||
@@ -89,9 +89,6 @@ export default function TradingSimulation() {
|
|||||||
// 所有的 useColorModeValue 也必须在顶部
|
// 所有的 useColorModeValue 也必须在顶部
|
||||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||||
const cardBg = useColorModeValue('white', 'gray.800');
|
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');
|
const contentTextColor = useColorModeValue('gray.700', 'white');
|
||||||
|
|
||||||
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
|
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
|
||||||
@@ -137,93 +134,9 @@ export default function TradingSimulation() {
|
|||||||
}, [account, getAssetHistory]);
|
}, [account, getAssetHistory]);
|
||||||
|
|
||||||
// ========== 3. 数据处理和计算(不是 Hooks,可以放在这里)==========
|
// ========== 3. 数据处理和计算(不是 Hooks,可以放在这里)==========
|
||||||
// 准备资产走势图表数据(使用真实数据,安全处理)
|
// 检查是否有资产历史数据
|
||||||
const hasAssetData = Array.isArray(assetHistory) && assetHistory.length > 0;
|
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. 现在可以安全地进行条件返回了 ==========
|
// ========== 4. 现在可以安全地进行条件返回了 ==========
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -367,12 +280,7 @@ export default function TradingSimulation() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Box h="350px">
|
<AssetTrendChart data={assetHistory} height={350} />
|
||||||
<LineChart
|
|
||||||
chartData={assetTrendData}
|
|
||||||
chartOptions={assetTrendOptions}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user