Compare commits

..

5 Commits

Author SHA1 Message Date
zdl
a2c5c8bb47 chore: 移除 ApexCharts 依赖
- 删除 apexcharts 和 react-apexcharts 包
- 所有图表已迁移到 ECharts,不再需要 ApexCharts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 12:07:38 +08:00
zdl
292d3a007a refactor(TradingSimulation): 迁移 ApexCharts 图表到 ECharts
图表组件迁移:
  - AssetTrendChart: 资产走势折线图 → ECharts 面积图
  - AssetAllocationChart: 资产配置环形图 → ECharts 饼图
  - PositionDistributionChart: 持仓分布饼图 → ECharts 饼图
  - ProfitAnalysisChart: 盈亏分析柱状图 → ECharts 柱状图

  删除的 ApexCharts 组件:
  - src/components/Charts/LineChart.js
  - src/components/Charts/BarChart.js
  - src/components/Charts/PieChart.js
  - src/components/Charts/DonutChart.js

  技术改进:
  - 统一使用 ECharts 作为通用图表库
  - 新组件使用 TypeScript,类型安全
  - 为后续移除 apexcharts 依赖做准备
2025-12-24 12:06:26 +08:00
zdl
a27065e613 feat: 删除旧文件 2025-12-24 11:49:24 +08:00
zdl
12fc63bef9 chore: 删除未使用的 ApexCharts 模板组件和演示页面
删除的组件:
- src/components/Charts/BubbleChart.js
- src/components/Charts/LineBarChart.js
- src/components/Charts/PolarChart.js
- src/components/Charts/RadarChart.js

删除的模板演示页面:
- src/views/Pages/Charts.js
- src/views/Pages/Projects/General.js
- src/views/Pages/RTLPage.js

这些文件来自 Argon Dashboard Chakra PRO 模板,从未被路由引用或使用

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 11:28:11 +08:00
zdl
ac76db09a2 feat(HomePage): 已登录用户访问首页展示个人中心内容
- HomePage: 添加条件渲染,已登录时展示 Center 组件
- 重构 Center 目录结构,合并 Center.tsx 到 index.tsx
- 重命名 CenterDashboard 为 Center(lazy-components, homeRoutes)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 11:06:30 +08:00
26 changed files with 685 additions and 2668 deletions

View File

@@ -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": {

View File

@@ -1,50 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import React, { Component } from "react";
import Chart from "react-apexcharts";
class BarChart extends Component {
constructor(props) {
super(props);
this.state = {
chartData: [],
chartOptions: {},
};
}
componentDidMount() {
this.setState({
chartData: this.props.chartData,
chartOptions: this.props.chartOptions,
});
}
render() {
return (
<Chart
options={this.state.chartOptions}
series={this.state.chartData}
type="bar"
width="100%"
height="100%"
/>
);
}
}
export default BarChart;

View File

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

View File

@@ -1,51 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import React from "react";
import ReactApexChart from "react-apexcharts";
class DonutChart extends React.Component {
constructor(props) {
super(props);
this.state = {
chartData: [],
chartOptions: {},
};
}
componentDidMount() {
this.setState({
chartData: this.props.chartData,
chartOptions: this.props.chartOptions,
});
}
render() {
return (
<ReactApexChart
options={this.state.chartOptions}
series={this.state.chartData}
type="donut"
width="100%"
height="100%"
/>
);
}
}
export default DonutChart;

View File

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

View File

@@ -1,51 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import React from "react";
import ReactApexChart from "react-apexcharts";
class LineChart extends React.Component {
constructor(props) {
super(props);
this.state = {
chartData: [],
chartOptions: {},
};
}
componentDidMount() {
this.setState({
chartData: this.props.chartData,
chartOptions: this.props.chartOptions,
});
}
render() {
return (
<ReactApexChart
options={this.state.chartOptions}
series={this.state.chartData}
type="area"
width="100%"
height="100%"
/>
);
}
}
export default LineChart;

View File

@@ -1,51 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import React from "react";
import ReactApexChart from "react-apexcharts";
class PieChart extends React.Component {
constructor(props) {
super(props);
this.state = {
chartData: [],
chartOptions: {},
};
}
componentDidMount() {
this.setState({
chartData: this.props.chartData,
chartOptions: this.props.chartOptions,
});
}
render() {
return (
<ReactApexChart
options={this.state.chartOptions}
series={this.state.chartData}
type="pie"
width="100%"
height="100%"
/>
);
}
}
export default PieChart;

View File

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

View File

@@ -1,51 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import React from "react";
import ReactApexChart from "react-apexcharts";
class 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;

View File

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

View File

@@ -28,7 +28,7 @@ export const homeRoutes = [
// 个人中心 - /home/center
{
path: 'center',
component: lazyComponents.CenterDashboard,
component: lazyComponents.Center,
protection: PROTECTION_MODES.MODAL,
meta: {
title: '个人中心',

View File

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

View File

@@ -1,4 +0,0 @@
// src/views/Center/index.js
// 入口文件,导出 Center 组件
export { default } from './Center';

View File

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

View File

@@ -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 - 深色科技风格,自适应容器高度 */}

View File

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

View File

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

View File

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

View File

@@ -24,8 +24,8 @@ import {
} from '@chakra-ui/react';
import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiActivity } from 'react-icons/fi';
// 导入现有的图表组件
import DonutChart from '../../../components/Charts/DonutChart';
// 导入图表组件
import { AssetAllocationChart } from './AssetAllocationChart';
import IconBox from '../../../components/Icons/IconBox';
export default function AccountOverview({ account, tradingEvents }) {
@@ -65,68 +65,6 @@ export default function AccountOverview({ account, tradingEvents }) {
return `${(percent || 0) >= 0 ? '+' : ''}${(percent || 0).toFixed(2)}%`;
};
// 安全地准备资产配置饼图数据
const assetAllocationData = account?.totalAssets > 0 ? [
(account.availableCash / account.totalAssets) * 100,
(account.marketValue / account.totalAssets) * 100
] : [100, 0];
const assetAllocationOptions = {
labels: ['现金资产', '股票资产'],
colors: ['#4299E1', '#48BB78'],
chart: {
width: "100%",
height: "280px"
},
states: {
hover: {
filter: {
type: "none",
},
},
},
legend: {
show: true,
position: 'bottom',
fontSize: '12px',
labels: {
colors: textColor
}
},
dataLabels: {
enabled: true,
style: {
fontSize: '12px',
fontWeight: 'bold',
colors: ['#fff']
},
formatter: function (val) {
return (val || 0).toFixed(1) + "%"
}
},
plotOptions: {
pie: {
expandOnClick: false,
donut: {
labels: {
show: false,
},
},
},
},
fill: {
colors: ['#4299E1', '#48BB78'],
},
tooltip: {
enabled: true,
theme: "dark",
y: {
formatter: function(val) {
return formatCurrency(val / 100 * (account?.totalAssets || 0))
}
}
},
};
return (
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
@@ -330,12 +268,11 @@ export default function AccountOverview({ account, tradingEvents }) {
</HStack>
</CardHeader>
<CardBody>
<Box h="280px">
<DonutChart
chartData={assetAllocationData}
chartOptions={assetAllocationOptions}
/>
</Box>
<AssetAllocationChart
cashAmount={account?.availableCash || 0}
stockAmount={account?.marketValue || 0}
height={280}
/>
{/* 详细配置信息 */}
<VStack spacing={3} mt={4}>

View File

@@ -0,0 +1,143 @@
/**
* 资产配置环形图组件
* 使用 ECharts 展示现金/股票资产占比
*/
import ReactECharts from 'echarts-for-react';
import { Box } from '@chakra-ui/react';
interface AssetAllocationChartProps {
cashAmount: number;
stockAmount: number;
height?: number;
}
/**
* 资产配置环形图
* @param cashAmount - 现金资产金额
* @param stockAmount - 股票资产金额
* @param height - 图表高度,默认 280px
*/
export const AssetAllocationChart = ({
cashAmount,
stockAmount,
height = 280
}: AssetAllocationChartProps) => {
const total = cashAmount + stockAmount;
// 格式化金额
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
};
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
borderColor: 'transparent',
textStyle: {
color: '#fff'
},
formatter: (params: { name: string; value: number; percent: number }) => {
return `${params.name}<br/>${formatCurrency(params.value)}<br/>占比: ${params.percent.toFixed(1)}%`;
}
},
legend: {
orient: 'horizontal',
bottom: 10,
left: 'center',
textStyle: {
color: '#666',
fontSize: 12
},
itemWidth: 12,
itemHeight: 12,
itemGap: 20
},
series: [
{
name: '资产配置',
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '45%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'outside',
formatter: (params: { name: string; percent: number }) => {
return `${params.name}\n${params.percent.toFixed(1)}%`;
},
fontSize: 12,
color: '#333',
lineHeight: 18
},
labelLine: {
show: true,
length: 15,
length2: 10,
smooth: true
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
data: [
{
value: cashAmount,
name: '现金资产',
itemStyle: { color: '#4299E1' }
},
{
value: stockAmount,
name: '股票资产',
itemStyle: { color: '#48BB78' }
}
]
}
]
};
if (total === 0) {
return (
<Box
h={`${height}px`}
w="100%"
display="flex"
alignItems="center"
justifyContent="center"
color="gray.500"
fontSize="sm"
>
</Box>
);
}
return (
<ReactECharts
option={option}
style={{ width: '100%', height: `${height}px` }}
notMerge={true}
lazyUpdate={true}
/>
);
};
export default AssetAllocationChart;

View File

@@ -0,0 +1,180 @@
/**
* 资产走势图表组件
* 使用 ECharts 的面积图模式展示资产走势
*/
import { useMemo } from 'react';
import ReactECharts from 'echarts-for-react';
import { Box } from '@chakra-ui/react';
interface AssetHistoryItem {
date: string;
closing_assets?: number;
total_assets?: number;
value?: number;
}
interface AssetTrendChartProps {
data: AssetHistoryItem[];
height?: number;
}
/**
* 资产走势图表
* @param data - 资产历史数据数组
* @param height - 图表高度,默认 350px
*/
export const AssetTrendChart = ({ data, height = 350 }: AssetTrendChartProps) => {
const option = useMemo(() => {
if (!data || data.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'middle',
textStyle: { color: '#999', fontSize: 14 }
}
};
}
// 提取数据
const dates = data.map(item => item.date || '');
const values = data.map(item => item.closing_assets || item.total_assets || item.value || 0);
// 判断涨跌(首尾对比)
const isUp = values[values.length - 1] >= values[0];
const lineColor = isUp ? '#48BB78' : '#F56565';
const areaColorStart = isUp ? 'rgba(72, 187, 120, 0.4)' : 'rgba(245, 101, 101, 0.4)';
const areaColorEnd = isUp ? 'rgba(72, 187, 120, 0.05)' : 'rgba(245, 101, 101, 0.05)';
return {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
borderColor: 'transparent',
textStyle: { color: '#fff' },
formatter: (params: { name: string; value: number }[]) => {
const item = params[0];
const value = item.value || 0;
const formatted = new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(value);
return `${item.name}<br/>总资产: ${formatted}`;
}
},
grid: {
left: 60,
right: 20,
top: 20,
bottom: 40
},
xAxis: {
type: 'category',
data: dates,
boundaryGap: false,
axisLine: {
lineStyle: { color: '#e0e0e0' }
},
axisTick: { show: false },
axisLabel: {
color: '#888888',
fontSize: 12,
formatter: (value: string) => {
// 简化日期显示MM-DD
if (value && value.length >= 10) {
return value.substring(5, 10);
}
return value;
}
}
},
yAxis: {
type: 'value',
scale: true, // 关键:启用 scale 让 Y 轴自适应数据范围,不从 0 开始
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: '#888888',
fontSize: 12,
formatter: (value: number) => {
if (value >= 10000) {
return `¥${(value / 10000).toFixed(1)}`;
}
return `¥${value.toFixed(0)}`;
}
},
splitLine: {
lineStyle: {
color: '#e0e0e0',
type: 'dashed'
}
}
},
series: [
{
name: '总资产',
type: 'line',
data: values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
showSymbol: false,
lineStyle: {
color: lineColor,
width: 2
},
itemStyle: {
color: lineColor
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: areaColorStart },
{ offset: 1, color: areaColorEnd }
]
}
},
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#fff'
}
}
}
]
};
}, [data]);
if (!data || data.length === 0) {
return (
<Box
h={`${height}px`}
w="100%"
display="flex"
alignItems="center"
justifyContent="center"
color="gray.500"
fontSize="sm"
>
</Box>
);
}
return (
<ReactECharts
option={option}
style={{ width: '100%', height: `${height}px` }}
notMerge={true}
lazyUpdate={true}
/>
);
};
export default AssetTrendChart;

View File

@@ -0,0 +1,160 @@
/**
* 持仓分布饼图组件
* 使用 ECharts 展示各股票持仓占比
*/
import ReactECharts from 'echarts-for-react';
import { Box } from '@chakra-ui/react';
interface PositionItem {
stockCode: string;
stockName: string;
marketValue: number;
}
interface PositionDistributionChartProps {
positions: PositionItem[];
height?: number;
}
// 预设颜色调色板
const COLORS = [
'#4299E1', // 蓝色
'#48BB78', // 绿色
'#ED8936', // 橙色
'#9F7AEA', // 紫色
'#F56565', // 红色
'#38B2AC', // 青色
'#ECC94B', // 黄色
'#667EEA', // 靛蓝色
'#FC8181', // 浅红色
'#68D391' // 浅绿色
];
/**
* 持仓分布饼图
* @param positions - 持仓列表
* @param height - 图表高度,默认 300px
*/
export const PositionDistributionChart = ({
positions,
height = 300
}: PositionDistributionChartProps) => {
// 格式化金额
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
};
// 准备数据
const data = positions.map((pos, index) => ({
value: pos.marketValue || 0,
name: pos.stockName || pos.stockCode,
itemStyle: { color: COLORS[index % COLORS.length] }
}));
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
borderColor: 'transparent',
textStyle: {
color: '#fff'
},
formatter: (params: { name: string; value: number; percent: number }) => {
return `${params.name}<br/>${formatCurrency(params.value)}<br/>占比: ${params.percent.toFixed(1)}%`;
}
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
textStyle: {
color: '#666',
fontSize: 12
},
itemWidth: 10,
itemHeight: 10,
itemGap: 8,
pageButtonItemGap: 5,
pageButtonGap: 10,
pageIconColor: '#4299E1',
pageIconInactiveColor: '#ccc',
pageTextStyle: {
color: '#666'
}
},
series: [
{
name: '持仓分布',
type: 'pie',
radius: ['0%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'inside',
formatter: (params: { percent: number }) => {
return params.percent >= 10 ? `${params.percent.toFixed(1)}%` : '';
},
fontSize: 11,
color: '#fff',
fontWeight: 'bold'
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
data
}
]
};
if (!positions || positions.length === 0) {
return (
<Box
h={`${height}px`}
w="100%"
display="flex"
alignItems="center"
justifyContent="center"
color="gray.500"
fontSize="sm"
>
</Box>
);
}
return (
<ReactECharts
option={option}
style={{ width: '100%', height: `${height}px` }}
notMerge={true}
lazyUpdate={true}
/>
);
};
export default PositionDistributionChart;

View File

@@ -50,9 +50,9 @@ import {
} from '@chakra-ui/react';
import { FiTrendingUp, FiTrendingDown, FiMinus, FiBarChart2, FiPieChart } from 'react-icons/fi';
// 导入现有的高质量组件
import BarChart from '../../../components/Charts/BarChart';
import PieChart from '../../../components/Charts/PieChart';
// 导入图表组件
import { PositionDistributionChart } from './PositionDistributionChart';
import { ProfitAnalysisChart } from './ProfitAnalysisChart';
import IconBox from '../../../components/Icons/IconBox';
import { logger } from '../../../utils/logger';
@@ -271,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>
)}

View File

@@ -0,0 +1,161 @@
/**
* 盈亏分析柱状图组件
* 使用 ECharts 展示各股票盈亏情况
*/
import ReactECharts from 'echarts-for-react';
import { Box } from '@chakra-ui/react';
interface PositionItem {
stockCode: string;
stockName: string;
profit: number;
}
interface ProfitAnalysisChartProps {
positions: PositionItem[];
height?: number;
}
/**
* 盈亏分析柱状图
* @param positions - 持仓列表(含盈亏数据)
* @param height - 图表高度,默认 300px
*/
export const ProfitAnalysisChart = ({
positions,
height = 300
}: ProfitAnalysisChartProps) => {
// 格式化金额
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
};
// 准备数据
const categories = positions.map(pos => pos.stockCode || pos.stockName);
const profits = positions.map(pos => pos.profit || 0);
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
borderColor: 'transparent',
textStyle: {
color: '#fff'
},
axisPointer: {
type: 'shadow'
},
formatter: (params: { name: string; value: number }[]) => {
const data = params[0];
const pos = positions.find(p => p.stockCode === data.name || p.stockName === data.name);
const stockName = pos?.stockName || data.name;
return `${stockName}<br/>盈亏: ${formatCurrency(data.value)}`;
}
},
grid: {
left: 60,
right: 20,
top: 20,
bottom: 40
},
xAxis: {
type: 'category',
data: categories,
axisLine: {
lineStyle: {
color: '#e0e0e0'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#718096',
fontSize: 12,
rotate: categories.length > 5 ? 45 : 0,
interval: 0
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#718096',
fontSize: 12,
formatter: (value: number) => {
if (Math.abs(value) >= 10000) {
return `¥${(value / 10000).toFixed(1)}`;
} else if (Math.abs(value) >= 1000) {
return `¥${(value / 1000).toFixed(1)}k`;
}
return `¥${value.toFixed(0)}`;
}
},
splitLine: {
lineStyle: {
color: '#E2E8F0',
type: 'dashed'
}
}
},
series: [
{
name: '盈亏',
type: 'bar',
barWidth: '60%',
barMaxWidth: 50,
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: (params: { value: number }) => {
return params.value >= 0 ? '#48BB78' : '#F56565';
}
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
data: profits
}
]
};
if (!positions || positions.length === 0) {
return (
<Box
h={`${height}px`}
w="100%"
display="flex"
alignItems="center"
justifyContent="center"
color="gray.500"
fontSize="sm"
>
</Box>
);
}
return (
<ReactECharts
option={option}
style={{ width: '100%', height: `${height}px` }}
notMerge={true}
lazyUpdate={true}
/>
);
};
export default ProfitAnalysisChart;

View File

@@ -321,11 +321,13 @@ export function useTradingAccount() {
// 调试模式demo用户返回模拟数据避免CORS
if (!user || user.id === 'demo') {
const now = Date.now();
const baseAssets = 1000000; // 起始资产100万
const data = Array.from({ length: days }, (_, i) => {
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
// 简单生成一条平滑的收益曲线
const value = Math.sin(i / 5) * 0.01 + 0.001 * i;
return { date: date.toISOString().slice(0, 10), value };
// 生成一条平滑的资产曲线在100万附近波动整体上涨
const dailyReturn = Math.sin(i / 5) * 0.01 + 0.001 * i;
const totalAssets = baseAssets * (1 + dailyReturn);
return { date: date.toISOString().slice(0, 10), total_assets: totalAssets };
});
return data;
}

View File

@@ -42,8 +42,8 @@ import PositionsList from './components/PositionsList';
import TradingHistory from './components/TradingHistory';
import MarginTrading from './components/MarginTrading';
// 导入现有的高质量组件
import LineChart from '../../components/Charts/LineChart';
// 导入图表组件
import { AssetTrendChart } from './components/AssetTrendChart';
// 导航栏已由 MainLayout 提供,无需在此导入
@@ -89,9 +89,6 @@ export default function TradingSimulation() {
// 所有的 useColorModeValue 也必须在顶部
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
const contentTextColor = useColorModeValue('gray.700', 'white');
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
@@ -137,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>
)}