Compare commits
5 Commits
9156da410d
...
a2c5c8bb47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2c5c8bb47 | ||
|
|
292d3a007a | ||
|
|
a27065e613 | ||
|
|
12fc63bef9 | ||
|
|
ac76db09a2 |
14
package.json
14
package.json
@@ -24,29 +24,20 @@
|
||||
"@visx/visx": "^3.12.0",
|
||||
"@visx/wordcloud": "^3.12.0",
|
||||
"antd": "^5.27.4",
|
||||
"apexcharts": "^3.27.3",
|
||||
"axios": "^1.10.0",
|
||||
"classnames": "^2.5.1",
|
||||
"craco-less": "^3.0.1",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^2.23.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"draft-js": "^0.11.7",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"fullcalendar": "^5.9.0",
|
||||
"globalize": "^1.7.0",
|
||||
"history": "^5.3.0",
|
||||
"klinecharts": "^10.0.0-beta1",
|
||||
"lucide-react": "^0.540.0",
|
||||
"match-sorter": "6.3.0",
|
||||
"nouislider": "15.0.0",
|
||||
"posthog-js": "^1.295.0",
|
||||
"react": "^19.0.0",
|
||||
"react-apexcharts": "^1.3.9",
|
||||
"react-circular-slider-svg": "^0.1.5",
|
||||
"react-custom-scrollbars-2": "^4.4.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-force-graph-3d": "^1.29.0",
|
||||
@@ -58,16 +49,12 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-responsive": "^10.0.1",
|
||||
"react-responsive-masonry": "^2.7.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-scroll": "^1.8.4",
|
||||
"react-scroll-into-view": "^2.1.3",
|
||||
"react-table": "^7.7.0",
|
||||
"react-tagsinput": "3.19.0",
|
||||
"react-to-print": "^3.0.3",
|
||||
"react-tsparticles": "^2.12.2",
|
||||
"reagraph": "^4.27.0",
|
||||
"recharts": "^3.1.2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sass": "^1.49.9",
|
||||
@@ -75,7 +62,6 @@
|
||||
"styled-components": "^5.3.11",
|
||||
"stylis": "^4.0.10",
|
||||
"stylis-plugin-rtl": "^2.1.1",
|
||||
"three": "^0.181.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -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,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 BubbleChart 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="bubble"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BubbleChart;
|
||||
@@ -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,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 LineBarChart 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="line"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LineBarChart;
|
||||
@@ -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;
|
||||
@@ -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 PolarChart 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="polarArea"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PolarChart;
|
||||
@@ -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 RadarChart 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="radar"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RadarChart;
|
||||
@@ -1,594 +0,0 @@
|
||||
// src/components/StockChart/StockChartAntdModal.js - Antd版本的股票图表组件
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Modal, Button, Spin, Typography } from 'antd';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { echarts } from '@lib/echarts';
|
||||
import dayjs from 'dayjs';
|
||||
import { stockService } from '../../services/eventService';
|
||||
import CitedContent from '../Citation/CitedContent';
|
||||
import { logger } from '../../utils/logger';
|
||||
import RiskDisclaimer from '../RiskDisclaimer';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const StockChartAntdModal = ({
|
||||
open = false,
|
||||
onCancel,
|
||||
stock,
|
||||
eventTime,
|
||||
fixed = false,
|
||||
width = 800
|
||||
}) => {
|
||||
const chartRef = useRef(null);
|
||||
const chartInstanceRef = useRef(null);
|
||||
const [activeChartType, setActiveChartType] = useState('timeline');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [chartData, setChartData] = useState(null);
|
||||
const [preloadedData, setPreloadedData] = useState({});
|
||||
|
||||
// 预加载数据
|
||||
const preloadData = async (type) => {
|
||||
if (!stock?.stock_code || preloadedData[type]) return;
|
||||
|
||||
try {
|
||||
// 统一的事件时间处理逻辑:盘后事件推到次日开盘
|
||||
let adjustedEventTime = eventTime;
|
||||
if (eventTime) {
|
||||
try {
|
||||
const eventMoment = dayjs(eventTime);
|
||||
if (eventMoment.isValid()) {
|
||||
// 如果是15:00之后的事件,推到下一个交易日的9:30
|
||||
if (eventMoment.hour() >= 15) {
|
||||
const nextDay = eventMoment.clone().add(1, 'day');
|
||||
nextDay.hour(9).minute(30).second(0).millisecond(0);
|
||||
adjustedEventTime = nextDay.format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('StockChartAntdModal', '事件时间解析失败', {
|
||||
eventTime,
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const response = await stockService.getKlineData(stock.stock_code, type, adjustedEventTime);
|
||||
setPreloadedData(prev => ({...prev, [type]: response}));
|
||||
logger.debug('StockChartAntdModal', '数据预加载成功', {
|
||||
stockCode: stock.stock_code,
|
||||
type,
|
||||
dataLength: response?.data?.length || 0
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('StockChartAntdModal', 'preloadData', err, {
|
||||
stockCode: stock?.stock_code,
|
||||
type
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 预加载数据的effect
|
||||
useEffect(() => {
|
||||
if (open && stock?.stock_code) {
|
||||
// 预加载两种图表类型的数据
|
||||
preloadData('timeline');
|
||||
preloadData('daily');
|
||||
}
|
||||
}, [open, stock?.stock_code, eventTime]);
|
||||
|
||||
// 加载图表数据
|
||||
useEffect(() => {
|
||||
const loadChartData = async () => {
|
||||
if (!stock?.stock_code) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 先尝试使用预加载的数据
|
||||
let data = preloadedData[activeChartType];
|
||||
|
||||
if (!data) {
|
||||
// 如果预加载数据不存在,则立即请求
|
||||
let adjustedEventTime = eventTime;
|
||||
if (eventTime) {
|
||||
try {
|
||||
const eventMoment = dayjs(eventTime);
|
||||
if (eventMoment.isValid()) {
|
||||
// 如果是15:00之后的事件,推到下一个交易日的9:30
|
||||
if (eventMoment.hour() >= 15) {
|
||||
const nextDay = eventMoment.clone().add(1, 'day');
|
||||
nextDay.hour(9).minute(30).second(0).millisecond(0);
|
||||
adjustedEventTime = nextDay.format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('StockChartAntdModal', '事件时间解析失败', {
|
||||
eventTime,
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
data = await stockService.getKlineData(stock.stock_code, activeChartType, adjustedEventTime);
|
||||
}
|
||||
|
||||
setChartData(data);
|
||||
logger.debug('StockChartAntdModal', '图表数据加载成功', {
|
||||
stockCode: stock.stock_code,
|
||||
chartType: activeChartType,
|
||||
dataLength: data?.data?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('StockChartAntdModal', 'loadChartData', error, {
|
||||
stockCode: stock?.stock_code,
|
||||
chartType: activeChartType
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (stock && stock.stock_code) {
|
||||
loadChartData();
|
||||
}
|
||||
}, [stock?.stock_code, activeChartType, eventTime]);
|
||||
|
||||
// 生成图表配置
|
||||
const getChartOption = () => {
|
||||
if (!chartData || !chartData.data) {
|
||||
return {
|
||||
title: { text: '暂无数据', left: 'center' },
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{ data: [], type: 'line' }]
|
||||
};
|
||||
}
|
||||
|
||||
const data = chartData.data;
|
||||
const tradeDate = chartData.trade_date;
|
||||
|
||||
// 处理数据格式
|
||||
let times = [];
|
||||
let prices = [];
|
||||
let opens = [];
|
||||
let highs = [];
|
||||
let lows = [];
|
||||
let closes = [];
|
||||
let volumes = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
times = data.map(item => item.time || item.date || item.timestamp);
|
||||
prices = data.map(item => item.close || item.price || item.value);
|
||||
opens = data.map(item => item.open);
|
||||
highs = data.map(item => item.high);
|
||||
lows = data.map(item => item.low);
|
||||
closes = data.map(item => item.close);
|
||||
volumes = data.map(item => item.volume);
|
||||
} else if (data.times && data.prices) {
|
||||
times = data.times;
|
||||
prices = data.prices;
|
||||
opens = data.opens || [];
|
||||
highs = data.highs || [];
|
||||
lows = data.lows || [];
|
||||
closes = data.closes || [];
|
||||
volumes = data.volumes || [];
|
||||
}
|
||||
|
||||
// 生成K线数据结构
|
||||
const klineData = times.map((t, i) => [opens[i], closes[i], lows[i], highs[i]]);
|
||||
|
||||
// 计算事件标记线位置
|
||||
let markLineData = [];
|
||||
if (eventTime && times.length > 0) {
|
||||
const eventMoment = dayjs(eventTime);
|
||||
const eventDate = eventMoment.format('YYYY-MM-DD');
|
||||
|
||||
if (activeChartType === 'timeline') {
|
||||
// 分时图:在相同交易日内定位具体时间
|
||||
if (eventDate === tradeDate) {
|
||||
const eventTime = eventMoment.format('HH:mm');
|
||||
let nearestIdx = 0;
|
||||
const eventMinutes = eventMoment.hour() * 60 + eventMoment.minute();
|
||||
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
const [h, m] = times[i].split(':').map(Number);
|
||||
const timeMinutes = h * 60 + m;
|
||||
const currentDiff = Math.abs(timeMinutes - eventMinutes);
|
||||
const nearestDiff = Math.abs(
|
||||
(times[nearestIdx].split(':').map(Number)[0] * 60 + times[nearestIdx].split(':').map(Number)[1]) - eventMinutes
|
||||
);
|
||||
if (currentDiff < nearestDiff) {
|
||||
nearestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
markLineData = [{
|
||||
name: '事件发生',
|
||||
xAxis: nearestIdx,
|
||||
label: {
|
||||
formatter: '事件发生',
|
||||
position: 'middle',
|
||||
color: '#FFD700',
|
||||
fontSize: 12
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#FFD700',
|
||||
type: 'solid',
|
||||
width: 2
|
||||
}
|
||||
}];
|
||||
}
|
||||
} else if (activeChartType === 'daily') {
|
||||
// 日K线:定位到交易日
|
||||
let targetIndex = -1;
|
||||
|
||||
// 1. 先尝试找到完全匹配的日期
|
||||
targetIndex = times.findIndex(time => time === eventDate);
|
||||
|
||||
// 2. 如果没有完全匹配,找到第一个大于等于事件日期的交易日
|
||||
if (targetIndex === -1) {
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
if (times[i] >= eventDate) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果事件日期晚于所有交易日,则标记在最后一个交易日
|
||||
if (targetIndex === -1 && eventDate > times[times.length - 1]) {
|
||||
targetIndex = times.length - 1;
|
||||
}
|
||||
|
||||
// 4. 如果事件日期早于所有交易日,则标记在第一个交易日
|
||||
if (targetIndex === -1 && eventDate < times[0]) {
|
||||
targetIndex = 0;
|
||||
}
|
||||
|
||||
if (targetIndex >= 0) {
|
||||
let labelText = '事件发生';
|
||||
let labelPosition = 'middle';
|
||||
|
||||
// 根据事件时间和交易日的关系调整标签
|
||||
if (eventDate === times[targetIndex]) {
|
||||
if (eventMoment.hour() >= 15) {
|
||||
labelText = '事件发生\n(盘后)';
|
||||
} else if (eventMoment.hour() < 9 || (eventMoment.hour() === 9 && eventMoment.minute() < 30)) {
|
||||
labelText = '事件发生\n(盘前)';
|
||||
}
|
||||
} else if (eventDate < times[targetIndex]) {
|
||||
labelText = '事件发生\n(前一日)';
|
||||
labelPosition = 'start';
|
||||
} else {
|
||||
labelText = '事件发生\n(影响日)';
|
||||
labelPosition = 'end';
|
||||
}
|
||||
|
||||
markLineData = [{
|
||||
name: '事件发生',
|
||||
xAxis: targetIndex,
|
||||
label: {
|
||||
formatter: labelText,
|
||||
position: labelPosition,
|
||||
color: '#FFD700',
|
||||
fontSize: 12,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
padding: [4, 8],
|
||||
borderRadius: 4
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#FFD700',
|
||||
type: 'solid',
|
||||
width: 2
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分时图
|
||||
if (activeChartType === 'timeline') {
|
||||
const avgPrices = data.map(item => item.avg_price);
|
||||
|
||||
// 获取昨收盘价作为基准
|
||||
const prevClose = chartData.prev_close || (prices.length > 0 ? prices[0] : 0);
|
||||
|
||||
// 计算涨跌幅数据
|
||||
const changePercentData = prices.map(price => ((price - prevClose) / prevClose * 100));
|
||||
const avgChangePercentData = avgPrices.map(avgPrice => ((avgPrice - prevClose) / prevClose * 100));
|
||||
|
||||
const currentPrice = prices[prices.length - 1];
|
||||
const currentChange = ((currentPrice - prevClose) / prevClose * 100);
|
||||
const isUp = currentChange >= 0;
|
||||
const lineColor = isUp ? '#ef5350' : '#26a69a';
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: `${stock.stock_name || stock.stock_code} - 分时图`,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 16, fontWeight: 'bold' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: function(params) {
|
||||
const d = params[0]?.dataIndex ?? 0;
|
||||
const price = prices[d];
|
||||
const avgPrice = avgPrices[d];
|
||||
const volume = volumes[d];
|
||||
|
||||
// 安全计算涨跌幅,处理 undefined/null/0 的情况
|
||||
const safeCalcPercent = (val, base) => {
|
||||
if (val == null || base == null || base === 0) return 0;
|
||||
return ((val - base) / base * 100);
|
||||
};
|
||||
|
||||
const priceChangePercent = safeCalcPercent(price, prevClose);
|
||||
const avgChangePercent = safeCalcPercent(avgPrice, prevClose);
|
||||
const priceColor = priceChangePercent >= 0 ? '#ef5350' : '#26a69a';
|
||||
const avgColor = avgChangePercent >= 0 ? '#ef5350' : '#26a69a';
|
||||
|
||||
// 安全格式化数字
|
||||
const safeFixed = (val, digits = 2) => (val != null && !isNaN(val)) ? val.toFixed(digits) : '-';
|
||||
const formatPercent = (val) => {
|
||||
if (val == null || isNaN(val)) return '-';
|
||||
return (val >= 0 ? '+' : '') + val.toFixed(2) + '%';
|
||||
};
|
||||
|
||||
return `时间:${times[d] || '-'}<br/>现价:<span style="color: ${priceColor}">¥${safeFixed(price)} (${formatPercent(priceChangePercent)})</span><br/>均价:<span style="color: ${avgColor}">¥${safeFixed(avgPrice)} (${formatPercent(avgChangePercent)})</span><br/>昨收:¥${safeFixed(prevClose)}<br/>成交量:${volume != null ? Math.round(volume/100) + '手' : '-'}`;
|
||||
}
|
||||
},
|
||||
grid: [
|
||||
{ left: '10%', right: '10%', height: '50%', top: '15%' },
|
||||
{ left: '10%', right: '10%', top: '70%', height: '20%' }
|
||||
],
|
||||
xAxis: [
|
||||
{ type: 'category', data: times, gridIndex: 0, boundaryGap: false },
|
||||
{ type: 'category', data: times, gridIndex: 1, axisLabel: { show: false } }
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
gridIndex: 0,
|
||||
scale: false,
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
if (value == null || isNaN(value)) return '-';
|
||||
return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
gridIndex: 0,
|
||||
scale: false,
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
if (value == null || isNaN(value)) return '-';
|
||||
return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'value', gridIndex: 1, scale: true, axisLabel: { formatter: v => (v != null && !isNaN(v)) ? Math.round(v/100) + '手' : '-' } }
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
|
||||
{ show: true, xAxisIndex: [0, 1], type: 'slider', bottom: '0%' }
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '分时价',
|
||||
type: 'line',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
data: changePercentData,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: lineColor, width: 2 },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)' },
|
||||
{ offset: 1, color: isUp ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)' }
|
||||
])
|
||||
},
|
||||
markLine: {
|
||||
symbol: 'none',
|
||||
data: [
|
||||
// 昨收盘价基准线 (0%)
|
||||
{
|
||||
yAxis: 0,
|
||||
lineStyle: {
|
||||
color: '#666',
|
||||
type: 'dashed',
|
||||
width: 1.5,
|
||||
opacity: 0.8
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '昨收盘价',
|
||||
position: 'insideEndTop',
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
...markLineData
|
||||
],
|
||||
animation: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '均价线',
|
||||
type: 'line',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 1,
|
||||
data: avgChangePercentData,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: '#FFA500', width: 1 }
|
||||
},
|
||||
{
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 2,
|
||||
data: volumes,
|
||||
itemStyle: { color: '#b0c4de', opacity: 0.6 }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 日K线图
|
||||
if (activeChartType === 'daily') {
|
||||
return {
|
||||
title: {
|
||||
text: `${stock.stock_name || stock.stock_code} - 日K线`,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 16, fontWeight: 'bold' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: function(params) {
|
||||
const kline = params[0];
|
||||
const volume = params[1];
|
||||
if (!kline || !kline.data) return '';
|
||||
let tooltipHtml = `日期: ${times[kline.dataIndex]}<br/>开盘: ¥${kline.data[0]}<br/>收盘: ¥${kline.data[1]}<br/>最低: ¥${kline.data[2]}<br/>最高: ¥${kline.data[3]}`;
|
||||
if (volume && volume.data) {
|
||||
tooltipHtml += `<br/>成交量: ${Math.round(volume.data/100)}手`;
|
||||
}
|
||||
return tooltipHtml;
|
||||
}
|
||||
},
|
||||
grid: [
|
||||
{ left: '10%', right: '10%', height: '60%' },
|
||||
{ left: '10%', right: '10%', top: '75%', height: '20%' }
|
||||
],
|
||||
xAxis: [
|
||||
{ type: 'category', data: times, scale: true, boundaryGap: true, gridIndex: 0 },
|
||||
{ type: 'category', gridIndex: 1, data: times, axisLabel: { show: false } }
|
||||
],
|
||||
yAxis: [
|
||||
{ scale: true, splitArea: { show: true }, gridIndex: 0 },
|
||||
{ scale: true, gridIndex: 1, axisLabel: { formatter: (value) => Math.round(value/100) + '手' } }
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0, 1], start: 70, end: 100 },
|
||||
{ show: true, xAxisIndex: [0, 1], type: 'slider', bottom: '0%', start: 70, end: 100 }
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'K线',
|
||||
type: 'candlestick',
|
||||
yAxisIndex: 0,
|
||||
data: klineData,
|
||||
markLine: {
|
||||
symbol: 'none',
|
||||
data: markLineData,
|
||||
animation: false
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#ef5350',
|
||||
color0: '#26a69a',
|
||||
borderColor: '#ef5350',
|
||||
borderColor0: '#26a69a'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumes.map((volume, index) => ({
|
||||
value: volume,
|
||||
itemStyle: {
|
||||
color: closes[index] >= opens[index] ? '#ef5350' : '#26a69a'
|
||||
}
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={`${stock?.stock_name || stock?.stock_code} (${stock?.stock_code}) - 股票详情`}
|
||||
footer={null}
|
||||
onCancel={onCancel}
|
||||
width={width}
|
||||
centered
|
||||
zIndex={2500}
|
||||
mask={true}
|
||||
destroyOnHidden={true}
|
||||
bodyStyle={{ maxHeight: 'calc(90vh - 120px)', overflowY: 'auto', padding: '16px' }}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
{/* 图表类型切换按钮 */}
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
type={activeChartType === 'timeline' ? 'primary' : 'default'}
|
||||
onClick={() => setActiveChartType('timeline')}
|
||||
>
|
||||
分时图
|
||||
</Button>
|
||||
<Button
|
||||
type={activeChartType === 'daily' ? 'primary' : 'default'}
|
||||
onClick={() => setActiveChartType('daily')}
|
||||
>
|
||||
日K线
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 图表容器 */}
|
||||
<div style={{ height: '400px', width: '100%' }}>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<ReactECharts
|
||||
option={getChartOption()}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
onChartReady={(chart) => {
|
||||
setTimeout(() => chart.resize(), 50);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 关联描述 */}
|
||||
{stock?.relation_desc?.data ? (
|
||||
// 使用引用组件(带研报来源)
|
||||
<CitedContent
|
||||
data={stock.relation_desc}
|
||||
title="关联描述"
|
||||
containerStyle={{ marginTop: 16 }}
|
||||
/>
|
||||
) : stock?.relation_desc ? (
|
||||
// 降级显示(无引用数据)
|
||||
<div style={{ marginTop: 16, padding: 16, backgroundColor: '#f5f5f5', borderRadius: 6 }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>关联描述:</Text>
|
||||
<Text>{stock.relation_desc}(AI合成)</Text>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 风险提示 */}
|
||||
<RiskDisclaimer variant="default" />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockChartAntdModal;
|
||||
@@ -28,7 +28,7 @@ export const homeRoutes = [
|
||||
// 个人中心 - /home/center
|
||||
{
|
||||
path: 'center',
|
||||
component: lazyComponents.CenterDashboard,
|
||||
component: lazyComponents.Center,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
|
||||
@@ -11,7 +11,7 @@ export const lazyComponents = {
|
||||
// Home 模块
|
||||
// ⚡ 直接引用 HomePage,无需中间层(静态页面不需要骨架屏)
|
||||
HomePage: React.lazy(() => import('@views/Home/HomePage')),
|
||||
CenterDashboard: React.lazy(() => import('@views/Center')),
|
||||
Center: React.lazy(() => import('@views/Center')),
|
||||
ProfilePage: React.lazy(() => import('@views/Profile/ProfilePage')),
|
||||
// 价值论坛 - 我的积分页面
|
||||
ForumMyPoints: React.lazy(() => import('@views/Profile')),
|
||||
@@ -56,7 +56,7 @@ export const lazyComponents = {
|
||||
*/
|
||||
export const {
|
||||
HomePage,
|
||||
CenterDashboard,
|
||||
Center,
|
||||
ProfilePage,
|
||||
ForumMyPoints,
|
||||
SettingsPage,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// src/views/Center/index.js
|
||||
// 入口文件,导出 Center 组件
|
||||
|
||||
export { default } from './Center';
|
||||
@@ -13,12 +13,12 @@ import ForumCenter from '@views/Profile/components/ForumCenter';
|
||||
import { THEME } from '@views/Profile/components/MarketDashboard/constants';
|
||||
|
||||
/**
|
||||
* CenterDashboard 组件
|
||||
* Center 组件
|
||||
* 个人中心仪表板主页面
|
||||
*
|
||||
* 注意:右侧 WatchSidebar 已移至全局 GlobalSidebar(在 MainLayout 中渲染)
|
||||
*/
|
||||
const CenterDashboard: React.FC = () => {
|
||||
const Center: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box bg={THEME.bg.primary} minH="100vh" overflowX="hidden">
|
||||
@@ -42,4 +42,4 @@ const CenterDashboard: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CenterDashboard;
|
||||
export default Center;
|
||||
@@ -16,6 +16,7 @@ import { FeaturedFeatureCard } from './components/FeaturedFeatureCard';
|
||||
import { FeatureCard } from './components/FeatureCard';
|
||||
import MiniProgramLauncher from '@/components/MiniProgramLauncher';
|
||||
import { isMobileDevice } from '@/components/MiniProgramLauncher/hooks/useWechatEnvironment';
|
||||
import Center from '@views/Center';
|
||||
import '@/styles/home-animations.css';
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,7 @@ import '@/styles/home-animations.css';
|
||||
* 展示平台核心功能,引导用户探索各个功能模块
|
||||
*/
|
||||
const HomePage: React.FC = () => {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { user, isAuthenticated, isLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { track } = usePostHogTrack();
|
||||
|
||||
@@ -83,6 +84,17 @@ const HomePage: React.FC = () => {
|
||||
// 移动端判断(用于显示小程序入口)
|
||||
const isMobile = isMobileDevice();
|
||||
|
||||
// 等待认证状态确认(避免闪烁)
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已登录直接渲染个人中心
|
||||
if (isAuthenticated && user) {
|
||||
return <Center />;
|
||||
}
|
||||
|
||||
// 未登录渲染首页内容
|
||||
return (
|
||||
<Box minH="100%">
|
||||
{/* Hero Section - 深色科技风格,自适应容器高度 */}
|
||||
|
||||
@@ -1,221 +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.
|
||||
|
||||
*/
|
||||
|
||||
// Chakra imports
|
||||
import { Box, Grid, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
// Custom components
|
||||
import Card from "components/Card/Card";
|
||||
import CardBody from "components/Card/CardBody";
|
||||
import CardHeader from "components/Card/CardHeader";
|
||||
import BarChart from "components/Charts/BarChart";
|
||||
import BubbleChart from "components/Charts/BubbleChart";
|
||||
import DonutChart from "components/Charts/DonutChart";
|
||||
import LineBarChart from "components/Charts/LineBarChart";
|
||||
import LineChart from "components/Charts/LineChart";
|
||||
import PieChart from "components/Charts/PieChart";
|
||||
import PolarChart from "components/Charts/PolarChart";
|
||||
import RadarChart from "components/Charts/RadarChart";
|
||||
// Assets
|
||||
import React from "react";
|
||||
import {
|
||||
barChartDataCharts1,
|
||||
barChartDataCharts2,
|
||||
barChartOptionsCharts1,
|
||||
barChartOptionsCharts2,
|
||||
bubbleChartData,
|
||||
bubbleChartOptions,
|
||||
donutChartDataCharts1,
|
||||
donutChartOptionsCharts1,
|
||||
lineBarChartData,
|
||||
lineBarChartOptions,
|
||||
lineChartDataCharts1,
|
||||
lineChartDataCharts2,
|
||||
lineChartOptionsCharts1,
|
||||
lineChartOptionsCharts2,
|
||||
pieChartDataCharts1,
|
||||
pieChartOptionsCharts1,
|
||||
polarChartDataCharts,
|
||||
polarChartOptionsCharts,
|
||||
radarChartDataCharts,
|
||||
radarChartOptionsCharts,
|
||||
} from "variables/charts";
|
||||
|
||||
function Charts() {
|
||||
const textColor = useColorModeValue("gray.700", "white");
|
||||
|
||||
return (
|
||||
<Grid
|
||||
templateColumns={{ sm: "1fr", md: "repeat(2, 1fr)" }}
|
||||
templateRows={{ sm: "repeat(8, 1fr)", md: "repeat(4, 1fr)" }}
|
||||
gap="24px"
|
||||
pt={{ sm: "125px", lg: "75px" }}
|
||||
>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Line chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" h="100%">
|
||||
<LineChart
|
||||
chartData={lineChartDataCharts1}
|
||||
chartOptions={lineChartOptionsCharts1}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Line chart with gradient
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" h="100%">
|
||||
<LineChart
|
||||
chartData={lineChartDataCharts2}
|
||||
chartOptions={lineChartOptionsCharts2}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Bar chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" h="100%">
|
||||
<BarChart
|
||||
chartData={barChartDataCharts1}
|
||||
chartOptions={barChartOptionsCharts1}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Bar chart horizontal
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" h="100%">
|
||||
<BarChart
|
||||
chartData={barChartDataCharts2}
|
||||
chartOptions={barChartOptionsCharts2}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Mixed chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" h="100%">
|
||||
<LineBarChart
|
||||
chartData={lineBarChartData}
|
||||
chartOptions={lineBarChartOptions}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Bubble chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" h="100%">
|
||||
<BubbleChart
|
||||
chartData={bubbleChartData}
|
||||
chartOptions={bubbleChartOptions}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Donut chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" minH={{ sm: "200px", lg: "300px" }}>
|
||||
<DonutChart
|
||||
chartData={donutChartDataCharts1}
|
||||
chartOptions={donutChartOptionsCharts1}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Pie chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" minH={{ sm: "200px", lg: "300px" }}>
|
||||
<PieChart
|
||||
chartData={pieChartDataCharts1}
|
||||
chartOptions={pieChartOptionsCharts1}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Radar chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" minH={{ sm: "300px", lg: "500px" }}>
|
||||
<RadarChart
|
||||
chartData={radarChartDataCharts}
|
||||
chartOptions={radarChartOptionsCharts}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card px="0px" pb="0px">
|
||||
<CardHeader mb="34px" px="22px">
|
||||
<Text color={textColor} fontSize="lg" fontWeight="bold">
|
||||
Polar chart
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody h="100%">
|
||||
<Box w="100%" minH={{ sm: "300px", lg: "500px" }}>
|
||||
<PolarChart
|
||||
chartData={polarChartDataCharts}
|
||||
chartOptions={polarChartOptionsCharts}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Charts;
|
||||
@@ -1,740 +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';
|
||||
|
||||
// Chakra imports
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
Icon,
|
||||
Progress,
|
||||
Stack,
|
||||
Stat,
|
||||
StatHelpText,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
Text,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
// Assets
|
||||
import avatar4 from 'assets/img/avatars/avatar4.png';
|
||||
import basicDark from 'assets/img/BgMusicCard.png';
|
||||
import basicBlue from 'assets/img/basic-auth.png';
|
||||
import { BsCircleFill } from 'react-icons/bs';
|
||||
import { IoEllipsisHorizontalSharp } from 'react-icons/io5';
|
||||
import {
|
||||
CartIcon,
|
||||
DocumentIcon,
|
||||
GlobeIcon,
|
||||
HomeIcon,
|
||||
WalletIcon,
|
||||
} from 'components/Icons/Icons';
|
||||
// Custom components
|
||||
import Card from 'components/Card/Card';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import DonutChart from 'components/Charts/DonutChart';
|
||||
import LineChart from 'components/Charts/LineChart';
|
||||
import IconBox from 'components/Icons/IconBox';
|
||||
import { HSeparator } from 'components/Separator/Separator';
|
||||
import {
|
||||
donutChartDataGeneral,
|
||||
donutChartOptionsGeneral,
|
||||
lineChartDataGeneral,
|
||||
lineChartOptionsGeneral,
|
||||
} from 'variables/charts';
|
||||
|
||||
function General() {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const iconBoxInside = useColorModeValue('white', 'white');
|
||||
const iconBlue = useColorModeValue('blue.500', 'blue.500');
|
||||
const bgSalaryCard = useColorModeValue('gray.50', 'navy.900');
|
||||
const bgButton = useColorModeValue('gray.700', 'blue.500');
|
||||
|
||||
return (
|
||||
<Flex direction='column' pt={{ sm: '125px', lg: '75px' }}>
|
||||
<Grid
|
||||
templateColumns={{
|
||||
sm: '1fr',
|
||||
lg: '0.5fr 1.5fr 1fr',
|
||||
xl: '1fr 1.7fr 1.5fr',
|
||||
}}
|
||||
gap='24px'
|
||||
mb='24px'
|
||||
>
|
||||
<Card
|
||||
bgImage={colorMode === 'light' ? basicDark : basicBlue}
|
||||
bgSize='cover'
|
||||
bgPosition='50%'
|
||||
>
|
||||
<Flex
|
||||
p={{ sm: '24px 52px', lg: '12px 26px', xl: '24px 52px' }}
|
||||
direction='column'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
h='100%'
|
||||
>
|
||||
<Text color='#fff' fontSize='2xl' fontWeight='normal'>
|
||||
Earnings
|
||||
</Text>
|
||||
<Text color='#fff' fontSize='5xl' fontWeight='bold' mb='12px'>
|
||||
$15,800
|
||||
</Text>
|
||||
<Badge
|
||||
bg='rgba(255, 255, 255, 0.3)'
|
||||
mb='26px'
|
||||
color='#fff'
|
||||
w='150px'
|
||||
borderRadius='8px'
|
||||
display='flex'
|
||||
justify='center'
|
||||
align='center'
|
||||
>
|
||||
<Text
|
||||
textAlign='center'
|
||||
py='6px'
|
||||
fontSize='11px'
|
||||
w='100%'
|
||||
h='100%'
|
||||
alignSelf='center'
|
||||
>
|
||||
+15% SINCE LAST WEEK
|
||||
</Text>
|
||||
</Badge>
|
||||
<Button
|
||||
variant={colorMode === 'light' ? 'primary' : 'light'}
|
||||
py='16px'
|
||||
w='155px'
|
||||
h='25px'
|
||||
>
|
||||
VIEW MORE
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Grid
|
||||
templateColumns={{
|
||||
sm: '1fr',
|
||||
md: 'repeat(2, 1fr)',
|
||||
}}
|
||||
templateRows='repeat(2, 1fr)'
|
||||
gap='24px'
|
||||
>
|
||||
<Card px={{ lg: '8px', xl: '22px' }}>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
h='100%'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='sm'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
pb='.1rem'
|
||||
>
|
||||
Today's Money
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor}>
|
||||
$53,000
|
||||
</StatNumber>
|
||||
<StatHelpText
|
||||
alignSelf='flex-end'
|
||||
justifySelf='flex-end'
|
||||
m='0px'
|
||||
color='green.400'
|
||||
fontWeight='bold'
|
||||
ps='3px'
|
||||
fontSize='md'
|
||||
>
|
||||
+55%
|
||||
</StatHelpText>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox h={'45px'} w={'45px'} bg={bgButton}>
|
||||
<WalletIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card px={{ lg: '8px', xl: '22px' }}>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
h='100%'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='sm'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
pb='.1rem'
|
||||
>
|
||||
New Clients
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor}>
|
||||
+$3,052
|
||||
</StatNumber>
|
||||
<StatHelpText
|
||||
alignSelf='flex-end'
|
||||
justifySelf='flex-end'
|
||||
m='0px'
|
||||
color='red.500'
|
||||
fontWeight='bold'
|
||||
ps='3px'
|
||||
fontSize='md'
|
||||
>
|
||||
-14%
|
||||
</StatHelpText>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox h={'45px'} w={'45px'} bg={bgButton}>
|
||||
<DocumentIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card px={{ lg: '8px', xl: '22px' }}>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
h='100%'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='sm'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
pb='.1rem'
|
||||
>
|
||||
Today's Users
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor}>
|
||||
$2,300
|
||||
</StatNumber>
|
||||
<StatHelpText
|
||||
alignSelf='flex-end'
|
||||
justifySelf='flex-end'
|
||||
m='0px'
|
||||
color='green.400'
|
||||
fontWeight='bold'
|
||||
ps='3px'
|
||||
fontSize='md'
|
||||
>
|
||||
+5%
|
||||
</StatHelpText>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox h={'45px'} w={'45px'} bg={bgButton}>
|
||||
<GlobeIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card px={{ lg: '8px', xl: '22px' }}>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
h='100%'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='sm'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
pb='.1rem'
|
||||
>
|
||||
Total Sales
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor}>
|
||||
$173,000
|
||||
</StatNumber>
|
||||
<StatHelpText
|
||||
alignSelf='flex-end'
|
||||
justifySelf='flex-end'
|
||||
m='0px'
|
||||
color='green.400'
|
||||
fontWeight='bold'
|
||||
ps='3px'
|
||||
fontSize='md'
|
||||
>
|
||||
+8%
|
||||
</StatHelpText>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox h={'45px'} w={'45px'} bg={bgButton}>
|
||||
<CartIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Card>
|
||||
<CardHeader mb='28px'>
|
||||
<Flex>
|
||||
<Avatar
|
||||
src={avatar4}
|
||||
w='40px'
|
||||
h='40px'
|
||||
mr='15px'
|
||||
borderRadius='12px'
|
||||
/>
|
||||
<Flex direction='column'>
|
||||
<Text color={textColor} fontSize='md' fontWeight='bold'>
|
||||
Esthera Jackson
|
||||
</Text>
|
||||
<Text color='gray.400' fontSize='sm' fontWeight='normal'>
|
||||
2h ago
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Flex direction='column' w='100%'>
|
||||
<Flex direction='column' maxW='400px' mb='32px'>
|
||||
<Text color={textColor} fontSize='lg' fontWeight='bold' mb='4px'>
|
||||
I need a Ruby developer for my new website.
|
||||
</Text>
|
||||
<Text color='gray.400' fontSize='sm' fontWeight='normal'>
|
||||
The website was initially built in PHP, I need a professional
|
||||
ruby programmer to shift it.
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
justify='space-between'
|
||||
w='100%'
|
||||
bg={bgSalaryCard}
|
||||
borderRadius='12px'
|
||||
p='20px 25px'
|
||||
direction={{ sm: 'row' }}
|
||||
>
|
||||
<Text color='gray.400' fontSize='sm' fontWeight='normal'>
|
||||
${' '}
|
||||
<Text
|
||||
as='span'
|
||||
fontSize='2xl'
|
||||
color={textColor}
|
||||
fontWeight='bold'
|
||||
>
|
||||
3,000
|
||||
</Text>{' '}
|
||||
/ month
|
||||
</Text>
|
||||
<Button
|
||||
variant='outlined'
|
||||
w={{ sm: '75px', md: '125px', lg: '75px', xl: '125px' }}
|
||||
h='35px'
|
||||
>
|
||||
APPLY
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid
|
||||
templateColumns={{ sm: '1fr', lg: '1.85fr 1fr', xl: '70% 30%' }}
|
||||
gap='24px'
|
||||
>
|
||||
<Card p={{ sm: '16px', md: '22px' }} maxW={{ sm: '330px', md: '100%' }}>
|
||||
<CardHeader>
|
||||
<Flex justify='space-between' w='100%'>
|
||||
<Text color={textColor} fontSize='lg' fontWeight='bold'>
|
||||
To do List
|
||||
</Text>
|
||||
<Text color='gray.400' fontSize='md' fontWeight='normal'>
|
||||
23 - 30 March 2020
|
||||
</Text>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Flex
|
||||
direction='column'
|
||||
w='100%'
|
||||
overflowX={{ sm: 'scroll', md: 'hidden' }}
|
||||
>
|
||||
<HSeparator my='14px' />
|
||||
<Flex>
|
||||
<Flex
|
||||
bgColor='blue.500'
|
||||
borderRadius='12px'
|
||||
minWidth='3px'
|
||||
h='100%'
|
||||
/>
|
||||
<Flex direction='column' p='12px 22px' w='100%'>
|
||||
<Flex justify='space-between' mb='18px'>
|
||||
<Flex align='center'>
|
||||
<Checkbox
|
||||
me='16px'
|
||||
colorScheme='blue'
|
||||
size='lg'
|
||||
defaultIsChecked
|
||||
/>
|
||||
<Text color={textColor} fontSize='md' fontWeight='bold'>
|
||||
Check status
|
||||
</Text>
|
||||
</Flex>
|
||||
<Icon
|
||||
as={IoEllipsisHorizontalSharp}
|
||||
color='gray.400'
|
||||
w='20px'
|
||||
h='20px'
|
||||
cursor='pointer'
|
||||
/>
|
||||
</Flex>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={{
|
||||
sm: '20px',
|
||||
md: '100px',
|
||||
lg: '80px',
|
||||
xl: '180px',
|
||||
}}
|
||||
ms={{ sm: '0px', md: '36px', lg: '0px', xl: '36px' }}
|
||||
>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Date
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
22 July 2022
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Project
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
2414_VR4sf3#
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Company
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
Creative Tim
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<HSeparator my='14px' />
|
||||
<Flex>
|
||||
<Flex
|
||||
bgColor='orange.300'
|
||||
borderRadius='12px'
|
||||
minWidth='3px'
|
||||
h='100%'
|
||||
/>
|
||||
<Flex direction='column' p='12px 22px' w='100%'>
|
||||
<Flex justify='space-between' mb='18px'>
|
||||
<Flex align='center'>
|
||||
<Checkbox me='16px' colorScheme='blue' size='lg' />
|
||||
<Text color={textColor} fontSize='md' fontWeight='bold'>
|
||||
Management discussion
|
||||
</Text>
|
||||
</Flex>
|
||||
<Icon
|
||||
as={IoEllipsisHorizontalSharp}
|
||||
color='gray.400'
|
||||
w='20px'
|
||||
h='20px'
|
||||
cursor='pointer'
|
||||
/>
|
||||
</Flex>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={{
|
||||
sm: '20px',
|
||||
md: '100px',
|
||||
lg: '80px',
|
||||
xl: '180px',
|
||||
}}
|
||||
ms={{ sm: '0px', md: '36px', lg: '0px', xl: '36px' }}
|
||||
>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Date
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
22 July 2022
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Project
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
4411_8sIsdd23
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Company
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
Apple
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<HSeparator my='14px' />
|
||||
<Flex>
|
||||
<Flex
|
||||
bgColor='pink.300'
|
||||
borderRadius='12px'
|
||||
minWidth='3px'
|
||||
h='100%'
|
||||
/>
|
||||
<Flex direction='column' p='12px 22px' w='100%'>
|
||||
<Flex justify='space-between' mb='18px'>
|
||||
<Flex align='center'>
|
||||
<Checkbox me='16px' colorScheme='blue' size='lg' />
|
||||
<Text color={textColor} fontSize='md' fontWeight='bold'>
|
||||
New channel distribution
|
||||
</Text>
|
||||
</Flex>
|
||||
<Icon
|
||||
as={IoEllipsisHorizontalSharp}
|
||||
color='gray.400'
|
||||
w='20px'
|
||||
h='20px'
|
||||
cursor='pointer'
|
||||
/>
|
||||
</Flex>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={{
|
||||
sm: '20px',
|
||||
md: '100px',
|
||||
lg: '80px',
|
||||
xl: '180px',
|
||||
}}
|
||||
ms={{ sm: '0px', md: '36px', lg: '0px', xl: '36px' }}
|
||||
>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Date
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
22 July 2022
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Project
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
827d_kdl33D1s
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Company
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
Microsoft
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<HSeparator my='14px' />
|
||||
<Flex>
|
||||
<Flex
|
||||
bgColor='purple.500'
|
||||
borderRadius='12px'
|
||||
minWidth='3px'
|
||||
h='100%'
|
||||
/>{' '}
|
||||
<Flex direction='column' p='12px 22px' w='100%'>
|
||||
<Flex justify='space-between' mb='18px'>
|
||||
<Flex align='center'>
|
||||
<Checkbox
|
||||
me='16px'
|
||||
colorScheme='blue'
|
||||
size='lg'
|
||||
defaultIsChecked
|
||||
/>
|
||||
<Text color={textColor} fontSize='md' fontWeight='bold'>
|
||||
IOS App development
|
||||
</Text>
|
||||
</Flex>
|
||||
<Icon
|
||||
as={IoEllipsisHorizontalSharp}
|
||||
color='gray.400'
|
||||
w='20px'
|
||||
h='20px'
|
||||
cursor='pointer'
|
||||
/>
|
||||
</Flex>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={{
|
||||
sm: '20px',
|
||||
md: '100px',
|
||||
lg: '80px',
|
||||
xl: '180px',
|
||||
}}
|
||||
ms={{ sm: '0px', md: '36px', lg: '0px', xl: '36px' }}
|
||||
>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Date
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
22 July 2022
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Project
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
88s1_349DA2sa
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Company
|
||||
</Text>
|
||||
<Text color='gray.500' fontWeight='bold' fontSize='md'>
|
||||
Facebook
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Flex direction='column'>
|
||||
<Card
|
||||
mb='24px'
|
||||
maxW={{ sm: '330px', md: '100%' }}
|
||||
px={{ sm: '0px', lg: '22px' }}
|
||||
>
|
||||
<CardHeader>
|
||||
<Flex direction='column' w='100%'>
|
||||
<Flex
|
||||
justify='space-between'
|
||||
w='100%'
|
||||
px={{ sm: '22px', lg: '0px' }}
|
||||
>
|
||||
<Flex align='center'>
|
||||
<IconBox h={'45px'} w={'45px'} bg={iconBlue} me='16px'>
|
||||
<HomeIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Tasks
|
||||
</Text>
|
||||
<Text color={textColor} fontWeight='bold' fontSize='xl'>
|
||||
480
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction='column' alignSelf='flex-end' minW='125px'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
60%
|
||||
</Text>
|
||||
<Progress
|
||||
colorScheme='blue'
|
||||
size='sm'
|
||||
borderRadius='15px'
|
||||
value={60}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<LineChart
|
||||
chartData={lineChartDataGeneral}
|
||||
chartOptions={lineChartOptionsGeneral}
|
||||
/>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card maxW={{ sm: '330px', md: '100%' }}>
|
||||
<Flex justify='space-between' w='100%' minH='190px'>
|
||||
<Flex direction='column'>
|
||||
<Flex align='center'>
|
||||
<IconBox h={'45px'} w={'45px'} bg={iconBlue} me='16px'>
|
||||
<DocumentIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
<Flex direction='column'>
|
||||
<Text color='gray.400' fontWeight='normal' fontSize='md'>
|
||||
Projects
|
||||
</Text>
|
||||
<Text color={textColor} fontWeight='bold' fontSize='xl'>
|
||||
115
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction='column' mt='36px'>
|
||||
<Flex align='center'>
|
||||
<Icon
|
||||
as={BsCircleFill}
|
||||
color='blue.500'
|
||||
w='10px'
|
||||
h='10px'
|
||||
me='8px'
|
||||
/>
|
||||
<Text color={textColor} fontWeight='normal' fontSize='md'>
|
||||
Done
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex align='center'>
|
||||
<Icon
|
||||
as={BsCircleFill}
|
||||
color='gray.400'
|
||||
w='10px'
|
||||
h='10px'
|
||||
me='8px'
|
||||
/>
|
||||
<Text color={textColor} fontWeight='normal' fontSize='md'>
|
||||
In progress
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<DonutChart
|
||||
chartData={donutChartDataGeneral}
|
||||
chartOptions={donutChartOptionsGeneral}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default General;
|
||||
@@ -1,400 +0,0 @@
|
||||
// Chakra imports
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
Progress,
|
||||
SimpleGrid,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
// Custom components
|
||||
import Card from 'components/Card/Card.js';
|
||||
import BarChart from 'components/Charts/BarChart';
|
||||
import LineChart from 'components/Charts/LineChart';
|
||||
import IconBox from 'components/Icons/IconBox';
|
||||
// Custom icons
|
||||
import {
|
||||
CartIcon,
|
||||
DocumentIcon,
|
||||
GlobeIcon,
|
||||
WalletIcon,
|
||||
} from 'components/Icons/Icons.js';
|
||||
import React from 'react';
|
||||
// Variables
|
||||
import {
|
||||
barChartDataRTL,
|
||||
barChartOptionsRTL,
|
||||
lineChartDataRTL,
|
||||
lineChartOptionsRTL,
|
||||
} from 'variables/charts';
|
||||
import { pageVisits, socialTraffic } from 'variables/general';
|
||||
|
||||
export default function Dashboard() {
|
||||
// Chakra Color Mode
|
||||
const iconBlue = useColorModeValue('blue.500', 'blue.500');
|
||||
const iconBoxInside = useColorModeValue('white', 'white');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const tableRowColor = useColorModeValue('#F7FAFC', 'navy.900');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
const textTableColor = useColorModeValue('gray.500', 'white');
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<Flex flexDirection='column' pt={{ base: '120px', md: '75px' }}>
|
||||
<SimpleGrid columns={{ sm: 1, md: 2, xl: 4 }} spacing='24px' mb='20px'>
|
||||
<Card minH='125px'>
|
||||
<Flex direction='column'>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
mb='25px'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
textTransform='uppercase'
|
||||
>
|
||||
Today's Money
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
$53,897
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox borderRadius='50%' h={'45px'} w={'45px'} bg={iconBlue}>
|
||||
<WalletIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
<Text color='gray.400' fontSize='sm'>
|
||||
<Text as='span' color='green.400' fontWeight='bold'>
|
||||
+3.48%{' '}
|
||||
</Text>
|
||||
Since last month
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card minH='125px'>
|
||||
<Flex direction='column'>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
mb='25px'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
textTransform='uppercase'
|
||||
>
|
||||
Today's Users
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
$3,200
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox borderRadius='50%' h={'45px'} w={'45px'} bg={iconBlue}>
|
||||
<GlobeIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
<Text color='gray.400' fontSize='sm'>
|
||||
<Text as='span' color='green.400' fontWeight='bold'>
|
||||
+5.2%{' '}
|
||||
</Text>
|
||||
Since last month
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card minH='125px'>
|
||||
<Flex direction='column'>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
mb='25px'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
textTransform='uppercase'
|
||||
>
|
||||
New Clients
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
+2,503
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox borderRadius='50%' h={'45px'} w={'45px'} bg={iconBlue}>
|
||||
<DocumentIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
<Text color='gray.400' fontSize='sm'>
|
||||
<Text as='span' color='red.500' fontWeight='bold'>
|
||||
-2.82%{' '}
|
||||
</Text>
|
||||
Since last month
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card minH='125px'>
|
||||
<Flex direction='column'>
|
||||
<Flex
|
||||
flexDirection='row'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
mb='25px'
|
||||
>
|
||||
<Stat me='auto'>
|
||||
<StatLabel
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='bold'
|
||||
textTransform='uppercase'
|
||||
>
|
||||
Total Sales
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
$173,000
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox borderRadius='50%' h={'45px'} w={'45px'} bg={iconBlue}>
|
||||
<CartIcon h={'24px'} w={'24px'} color={iconBoxInside} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
<Text color='gray.400' fontSize='sm'>
|
||||
<Text as='span' color='green.400' fontWeight='bold'>
|
||||
+8.12%{' '}
|
||||
</Text>
|
||||
Since last month
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
<Grid
|
||||
templateColumns={{ sm: '1fr', lg: '2fr 1fr' }}
|
||||
templateRows={{ lg: 'repeat(2, auto)' }}
|
||||
gap='20px'
|
||||
>
|
||||
<Card
|
||||
bg={
|
||||
colorMode === 'dark'
|
||||
? 'navy.800'
|
||||
: 'linear-gradient(81.62deg, #313860 2.25%, #151928 79.87%)'
|
||||
}
|
||||
p='0px'
|
||||
maxW={{ sm: '320px', md: '100%' }}
|
||||
>
|
||||
<Flex direction='column' mb='40px' p='28px 22px 0px 0px'>
|
||||
<Text color='#fff' fontSize='lg' fontWeight='bold' mb='6px'>
|
||||
Sales Overview
|
||||
</Text>
|
||||
<Text color='#fff' fontSize='sm'>
|
||||
<Text as='span' color='green.400' fontWeight='bold'>
|
||||
(+5) more{' '}
|
||||
</Text>
|
||||
in 2022
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box minH='300px'>
|
||||
<LineChart
|
||||
chartData={lineChartDataRTL}
|
||||
chartOptions={lineChartOptionsRTL}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
<Card p='0px' maxW={{ sm: '320px', md: '100%' }}>
|
||||
<Flex direction='column' mb='40px' p='28px 22px 0px 0px'>
|
||||
<Text color='gray.400' fontSize='sm' fontWeight='bold' mb='6px'>
|
||||
PERFORMANCE
|
||||
</Text>
|
||||
<Text color={textColor} fontSize='lg' fontWeight='bold'>
|
||||
Total orders
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box minH='300px'>
|
||||
<BarChart
|
||||
chartData={barChartDataRTL}
|
||||
chartOptions={barChartOptionsRTL}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
<Card p='0px' maxW={{ sm: '320px', md: '100%' }}>
|
||||
<Flex direction='column'>
|
||||
<Flex align='center' justify='space-between' p='22px'>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
Page visits
|
||||
</Text>
|
||||
<Button variant='primary' maxH='30px'>
|
||||
SEE ALL
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box overflow={{ sm: 'scroll', lg: 'hidden' }}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr bg={tableRowColor}>
|
||||
<Th color='gray.400' borderColor={borderColor}>
|
||||
Page name
|
||||
</Th>
|
||||
<Th color='gray.400' borderColor={borderColor}>
|
||||
Visitors
|
||||
</Th>
|
||||
<Th color='gray.400' borderColor={borderColor}>
|
||||
Unique users
|
||||
</Th>
|
||||
<Th color='gray.400' borderColor={borderColor}>
|
||||
Bounce rate
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{pageVisits.map((el, index, arr) => {
|
||||
return (
|
||||
<Tr key={index}>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
fontWeight='bold'
|
||||
borderColor={borderColor}
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
>
|
||||
{el.pageName}
|
||||
</Td>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
borderColor={borderColor}
|
||||
>
|
||||
{el.visitors}
|
||||
</Td>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
borderColor={borderColor}
|
||||
>
|
||||
{el.uniqueUsers}
|
||||
</Td>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
borderColor={borderColor}
|
||||
>
|
||||
{el.bounceRate}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card p='0px' maxW={{ sm: '320px', md: '100%' }}>
|
||||
<Flex direction='column'>
|
||||
<Flex align='center' justify='space-between' p='22px'>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
Social traffic
|
||||
</Text>
|
||||
<Button variant='primary' maxH='30px'>
|
||||
SEE ALL
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box overflow={{ sm: 'scroll', lg: 'hidden' }}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr bg={tableRowColor}>
|
||||
<Th color='gray.400' borderColor={borderColor}>
|
||||
Referral
|
||||
</Th>
|
||||
<Th color='gray.400' borderColor={borderColor}>
|
||||
Visitors
|
||||
</Th>
|
||||
<Th color='gray.400' borderColor={borderColor}></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{socialTraffic.map((el, index, arr) => {
|
||||
return (
|
||||
<Tr key={index}>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
fontWeight='bold'
|
||||
borderColor={borderColor}
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
>
|
||||
{el.referral}
|
||||
</Td>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
borderColor={borderColor}
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
>
|
||||
{el.visitors}
|
||||
</Td>
|
||||
<Td
|
||||
color={textTableColor}
|
||||
fontSize='sm'
|
||||
borderColor={borderColor}
|
||||
border={index === arr.length - 1 ? 'none' : null}
|
||||
>
|
||||
<Flex align='center'>
|
||||
<Text
|
||||
color={textTableColor}
|
||||
fontWeight='bold'
|
||||
fontSize='sm'
|
||||
me='12px'
|
||||
>{`${el.percentage}%`}</Text>
|
||||
<Progress
|
||||
size='xs'
|
||||
colorScheme={el.color}
|
||||
value={el.percentage}
|
||||
minW='120px'
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiActivity } from 'react-icons/fi';
|
||||
|
||||
// 导入现有的图表组件
|
||||
import DonutChart from '../../../components/Charts/DonutChart';
|
||||
// 导入图表组件
|
||||
import { AssetAllocationChart } from './AssetAllocationChart';
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
|
||||
export default function AccountOverview({ account, tradingEvents }) {
|
||||
@@ -65,68 +65,6 @@ export default function AccountOverview({ account, tradingEvents }) {
|
||||
return `${(percent || 0) >= 0 ? '+' : ''}${(percent || 0).toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// 安全地准备资产配置饼图数据
|
||||
const assetAllocationData = account?.totalAssets > 0 ? [
|
||||
(account.availableCash / account.totalAssets) * 100,
|
||||
(account.marketValue / account.totalAssets) * 100
|
||||
] : [100, 0];
|
||||
|
||||
const assetAllocationOptions = {
|
||||
labels: ['现金资产', '股票资产'],
|
||||
colors: ['#4299E1', '#48BB78'],
|
||||
chart: {
|
||||
width: "100%",
|
||||
height: "280px"
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
fontSize: '12px',
|
||||
labels: {
|
||||
colors: textColor
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
colors: ['#fff']
|
||||
},
|
||||
formatter: function (val) {
|
||||
return (val || 0).toFixed(1) + "%"
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
expandOnClick: false,
|
||||
donut: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
colors: ['#4299E1', '#48BB78'],
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return formatCurrency(val / 100 * (account?.totalAssets || 0))
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||
@@ -330,12 +268,11 @@ export default function AccountOverview({ account, tradingEvents }) {
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="280px">
|
||||
<DonutChart
|
||||
chartData={assetAllocationData}
|
||||
chartOptions={assetAllocationOptions}
|
||||
/>
|
||||
</Box>
|
||||
<AssetAllocationChart
|
||||
cashAmount={account?.availableCash || 0}
|
||||
stockAmount={account?.marketValue || 0}
|
||||
height={280}
|
||||
/>
|
||||
|
||||
{/* 详细配置信息 */}
|
||||
<VStack spacing={3} mt={4}>
|
||||
|
||||
143
src/views/TradingSimulation/components/AssetAllocationChart.tsx
Normal file
143
src/views/TradingSimulation/components/AssetAllocationChart.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 资产配置环形图组件
|
||||
* 使用 ECharts 展示现金/股票资产占比
|
||||
*/
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface AssetAllocationChartProps {
|
||||
cashAmount: number;
|
||||
stockAmount: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产配置环形图
|
||||
* @param cashAmount - 现金资产金额
|
||||
* @param stockAmount - 股票资产金额
|
||||
* @param height - 图表高度,默认 280px
|
||||
*/
|
||||
export const AssetAllocationChart = ({
|
||||
cashAmount,
|
||||
stockAmount,
|
||||
height = 280
|
||||
}: AssetAllocationChartProps) => {
|
||||
const total = cashAmount + stockAmount;
|
||||
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: (params: { name: string; value: number; percent: number }) => {
|
||||
return `${params.name}<br/>${formatCurrency(params.value)}<br/>占比: ${params.percent.toFixed(1)}%`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
},
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
itemGap: 20
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '资产配置',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
center: ['50%', '45%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: (params: { name: string; percent: number }) => {
|
||||
return `${params.name}\n${params.percent.toFixed(1)}%`;
|
||||
},
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
lineHeight: 18
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 15,
|
||||
length2: 10,
|
||||
smooth: true
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: cashAmount,
|
||||
name: '现金资产',
|
||||
itemStyle: { color: '#4299E1' }
|
||||
},
|
||||
{
|
||||
value: stockAmount,
|
||||
name: '股票资产',
|
||||
itemStyle: { color: '#48BB78' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无数据
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetAllocationChart;
|
||||
180
src/views/TradingSimulation/components/AssetTrendChart.tsx
Normal file
180
src/views/TradingSimulation/components/AssetTrendChart.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 资产走势图表组件
|
||||
* 使用 ECharts 的面积图模式展示资产走势
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface AssetHistoryItem {
|
||||
date: string;
|
||||
closing_assets?: number;
|
||||
total_assets?: number;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
interface AssetTrendChartProps {
|
||||
data: AssetHistoryItem[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产走势图表
|
||||
* @param data - 资产历史数据数组
|
||||
* @param height - 图表高度,默认 350px
|
||||
*/
|
||||
export const AssetTrendChart = ({ data, height = 350 }: AssetTrendChartProps) => {
|
||||
const option = useMemo(() => {
|
||||
if (!data || data.length === 0) {
|
||||
return {
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#999', fontSize: 14 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 提取数据
|
||||
const dates = data.map(item => item.date || '');
|
||||
const values = data.map(item => item.closing_assets || item.total_assets || item.value || 0);
|
||||
|
||||
// 判断涨跌(首尾对比)
|
||||
const isUp = values[values.length - 1] >= values[0];
|
||||
const lineColor = isUp ? '#48BB78' : '#F56565';
|
||||
const areaColorStart = isUp ? 'rgba(72, 187, 120, 0.4)' : 'rgba(245, 101, 101, 0.4)';
|
||||
const areaColorEnd = isUp ? 'rgba(72, 187, 120, 0.05)' : 'rgba(245, 101, 101, 0.05)';
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: { color: '#fff' },
|
||||
formatter: (params: { name: string; value: number }[]) => {
|
||||
const item = params[0];
|
||||
const value = item.value || 0;
|
||||
const formatted = new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY'
|
||||
}).format(value);
|
||||
return `${item.name}<br/>总资产: ${formatted}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: { color: '#e0e0e0' }
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: '#888888',
|
||||
fontSize: 12,
|
||||
formatter: (value: string) => {
|
||||
// 简化日期显示:MM-DD
|
||||
if (value && value.length >= 10) {
|
||||
return value.substring(5, 10);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true, // 关键:启用 scale 让 Y 轴自适应数据范围,不从 0 开始
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: '#888888',
|
||||
fontSize: 12,
|
||||
formatter: (value: number) => {
|
||||
if (value >= 10000) {
|
||||
return `¥${(value / 10000).toFixed(1)}万`;
|
||||
}
|
||||
return `¥${value.toFixed(0)}`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#e0e0e0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总资产',
|
||||
type: 'line',
|
||||
data: values,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
color: lineColor,
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: lineColor
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: areaColorStart },
|
||||
{ offset: 1, color: areaColorEnd }
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无数据
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetTrendChart;
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 持仓分布饼图组件
|
||||
* 使用 ECharts 展示各股票持仓占比
|
||||
*/
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface PositionItem {
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
marketValue: number;
|
||||
}
|
||||
|
||||
interface PositionDistributionChartProps {
|
||||
positions: PositionItem[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// 预设颜色调色板
|
||||
const COLORS = [
|
||||
'#4299E1', // 蓝色
|
||||
'#48BB78', // 绿色
|
||||
'#ED8936', // 橙色
|
||||
'#9F7AEA', // 紫色
|
||||
'#F56565', // 红色
|
||||
'#38B2AC', // 青色
|
||||
'#ECC94B', // 黄色
|
||||
'#667EEA', // 靛蓝色
|
||||
'#FC8181', // 浅红色
|
||||
'#68D391' // 浅绿色
|
||||
];
|
||||
|
||||
/**
|
||||
* 持仓分布饼图
|
||||
* @param positions - 持仓列表
|
||||
* @param height - 图表高度,默认 300px
|
||||
*/
|
||||
export const PositionDistributionChart = ({
|
||||
positions,
|
||||
height = 300
|
||||
}: PositionDistributionChartProps) => {
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// 准备数据
|
||||
const data = positions.map((pos, index) => ({
|
||||
value: pos.marketValue || 0,
|
||||
name: pos.stockName || pos.stockCode,
|
||||
itemStyle: { color: COLORS[index % COLORS.length] }
|
||||
}));
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: (params: { name: string; value: number; percent: number }) => {
|
||||
return `${params.name}<br/>${formatCurrency(params.value)}<br/>占比: ${params.percent.toFixed(1)}%`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
},
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 8,
|
||||
pageButtonItemGap: 5,
|
||||
pageButtonGap: 10,
|
||||
pageIconColor: '#4299E1',
|
||||
pageIconInactiveColor: '#ccc',
|
||||
pageTextStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '持仓分布',
|
||||
type: 'pie',
|
||||
radius: ['0%', '70%'],
|
||||
center: ['35%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: (params: { percent: number }) => {
|
||||
return params.percent >= 10 ? `${params.percent.toFixed(1)}%` : '';
|
||||
},
|
||||
fontSize: 11,
|
||||
color: '#fff',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
data
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (!positions || positions.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无持仓
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PositionDistributionChart;
|
||||
@@ -50,9 +50,9 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { FiTrendingUp, FiTrendingDown, FiMinus, FiBarChart2, FiPieChart } from 'react-icons/fi';
|
||||
|
||||
// 导入现有的高质量组件
|
||||
import BarChart from '../../../components/Charts/BarChart';
|
||||
import PieChart from '../../../components/Charts/PieChart';
|
||||
// 导入图表组件
|
||||
import { PositionDistributionChart } from './PositionDistributionChart';
|
||||
import { ProfitAnalysisChart } from './ProfitAnalysisChart';
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
@@ -271,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
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="300px">
|
||||
<PieChart
|
||||
chartData={positionDistributionData}
|
||||
chartOptions={positionDistributionOptions}
|
||||
/>
|
||||
</Box>
|
||||
<PositionDistributionChart positions={safePositions} height={300} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
@@ -490,12 +387,7 @@ export default function PositionsList({ positions, account, onSellStock, trading
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="300px">
|
||||
<BarChart
|
||||
chartData={profitBarData}
|
||||
chartOptions={profitBarOptions}
|
||||
/>
|
||||
</Box>
|
||||
<ProfitAnalysisChart positions={safePositions} height={300} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
161
src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
Normal file
161
src/views/TradingSimulation/components/ProfitAnalysisChart.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 盈亏分析柱状图组件
|
||||
* 使用 ECharts 展示各股票盈亏情况
|
||||
*/
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
interface PositionItem {
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
profit: number;
|
||||
}
|
||||
|
||||
interface ProfitAnalysisChartProps {
|
||||
positions: PositionItem[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 盈亏分析柱状图
|
||||
* @param positions - 持仓列表(含盈亏数据)
|
||||
* @param height - 图表高度,默认 300px
|
||||
*/
|
||||
export const ProfitAnalysisChart = ({
|
||||
positions,
|
||||
height = 300
|
||||
}: ProfitAnalysisChartProps) => {
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// 准备数据
|
||||
const categories = positions.map(pos => pos.stockCode || pos.stockName);
|
||||
const profits = positions.map(pos => pos.profit || 0);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: { name: string; value: number }[]) => {
|
||||
const data = params[0];
|
||||
const pos = positions.find(p => p.stockCode === data.name || p.stockName === data.name);
|
||||
const stockName = pos?.stockName || data.name;
|
||||
return `${stockName}<br/>盈亏: ${formatCurrency(data.value)}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e0e0e0'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#718096',
|
||||
fontSize: 12,
|
||||
rotate: categories.length > 5 ? 45 : 0,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#718096',
|
||||
fontSize: 12,
|
||||
formatter: (value: number) => {
|
||||
if (Math.abs(value) >= 10000) {
|
||||
return `¥${(value / 10000).toFixed(1)}万`;
|
||||
} else if (Math.abs(value) >= 1000) {
|
||||
return `¥${(value / 1000).toFixed(1)}k`;
|
||||
}
|
||||
return `¥${value.toFixed(0)}`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#E2E8F0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '盈亏',
|
||||
type: 'bar',
|
||||
barWidth: '60%',
|
||||
barMaxWidth: 50,
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: (params: { value: number }) => {
|
||||
return params.value >= 0 ? '#48BB78' : '#F56565';
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
data: profits
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (!positions || positions.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
h={`${height}px`}
|
||||
w="100%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color="gray.500"
|
||||
fontSize="sm"
|
||||
>
|
||||
暂无持仓
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ width: '100%', height: `${height}px` }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfitAnalysisChart;
|
||||
@@ -321,11 +321,13 @@ export function useTradingAccount() {
|
||||
// 调试模式:demo用户返回模拟数据,避免CORS
|
||||
if (!user || user.id === 'demo') {
|
||||
const now = Date.now();
|
||||
const baseAssets = 1000000; // 起始资产100万
|
||||
const data = Array.from({ length: days }, (_, i) => {
|
||||
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
|
||||
// 简单生成一条平滑的收益曲线
|
||||
const value = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||||
return { date: date.toISOString().slice(0, 10), value };
|
||||
// 生成一条平滑的资产曲线(在100万附近波动,整体上涨)
|
||||
const dailyReturn = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||||
const totalAssets = baseAssets * (1 + dailyReturn);
|
||||
return { date: date.toISOString().slice(0, 10), total_assets: totalAssets };
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ import PositionsList from './components/PositionsList';
|
||||
import TradingHistory from './components/TradingHistory';
|
||||
import MarginTrading from './components/MarginTrading';
|
||||
|
||||
// 导入现有的高质量组件
|
||||
import LineChart from '../../components/Charts/LineChart';
|
||||
// 导入图表组件
|
||||
import { AssetTrendChart } from './components/AssetTrendChart';
|
||||
|
||||
// 导航栏已由 MainLayout 提供,无需在此导入
|
||||
|
||||
@@ -89,9 +89,6 @@ export default function TradingSimulation() {
|
||||
// 所有的 useColorModeValue 也必须在顶部
|
||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
|
||||
const contentTextColor = useColorModeValue('gray.700', 'white');
|
||||
|
||||
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
|
||||
@@ -137,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() {
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="350px">
|
||||
<LineChart
|
||||
chartData={assetTrendData}
|
||||
chartOptions={assetTrendOptions}
|
||||
/>
|
||||
</Box>
|
||||
<AssetTrendChart data={assetHistory} height={350} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user