feat: 重构 TradingSimulation 和 Dashboard 组件
This commit is contained in:
@@ -54,6 +54,7 @@ import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/zh-cn';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import './InvestmentCalendar.css';
|
||||
|
||||
moment.locale('zh-cn');
|
||||
@@ -86,10 +87,10 @@ export default function InvestmentCalendarChakra() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
|
||||
// 直接加载用户相关的事件(投资计划 + 关注的未来事件)
|
||||
const userResponse = await fetch(base + '/api/account/calendar/events', {
|
||||
credentials: 'include'
|
||||
const userResponse = await fetch(base + '/api/account/calendar/events', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (userResponse.ok) {
|
||||
@@ -110,20 +111,18 @@ export default function InvestmentCalendarChakra() {
|
||||
}));
|
||||
|
||||
setEvents(allEvents);
|
||||
logger.debug('InvestmentCalendar', '日历事件加载成功', {
|
||||
count: allEvents.length
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日历事件失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '无法加载日历事件',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
logger.error('InvestmentCalendar', 'loadEvents', error);
|
||||
// ❌ 移除数据加载失败 toast(非关键操作)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [toast]);
|
||||
}, []); // ✅ 移除 toast 依赖
|
||||
|
||||
useEffect(() => {
|
||||
loadEvents();
|
||||
@@ -170,7 +169,7 @@ export default function InvestmentCalendarChakra() {
|
||||
const handleAddEvent = async () => {
|
||||
try {
|
||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
|
||||
const eventData = {
|
||||
...newEvent,
|
||||
event_date: (selectedDate ? selectedDate.format('YYYY-MM-DD') : moment().format('YYYY-MM-DD')),
|
||||
@@ -189,6 +188,10 @@ export default function InvestmentCalendarChakra() {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
logger.info('InvestmentCalendar', '添加事件成功', {
|
||||
eventTitle: eventData.title,
|
||||
eventDate: eventData.event_date
|
||||
});
|
||||
toast({
|
||||
title: '添加成功',
|
||||
description: '投资计划已添加',
|
||||
@@ -207,7 +210,9 @@ export default function InvestmentCalendarChakra() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加事件失败:', error);
|
||||
logger.error('InvestmentCalendar', 'handleAddEvent', error, {
|
||||
eventTitle: newEvent?.title
|
||||
});
|
||||
toast({
|
||||
title: '添加失败',
|
||||
description: '无法添加投资计划',
|
||||
@@ -220,6 +225,7 @@ export default function InvestmentCalendarChakra() {
|
||||
// 删除用户事件
|
||||
const handleDeleteEvent = async (eventId) => {
|
||||
if (!eventId) {
|
||||
logger.warn('InvestmentCalendar', '删除事件失败', '缺少事件 ID', { eventId });
|
||||
toast({
|
||||
title: '无法删除',
|
||||
description: '缺少事件 ID',
|
||||
@@ -230,13 +236,14 @@ export default function InvestmentCalendarChakra() {
|
||||
}
|
||||
try {
|
||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
|
||||
const response = await fetch(base + `/api/account/calendar/events/${eventId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logger.info('InvestmentCalendar', '删除事件成功', { eventId });
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success',
|
||||
@@ -245,7 +252,7 @@ export default function InvestmentCalendarChakra() {
|
||||
loadEvents();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除事件失败:', error);
|
||||
logger.error('InvestmentCalendar', 'handleDeleteEvent', error, { eventId });
|
||||
toast({
|
||||
title: '删除失败',
|
||||
status: 'error',
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
} from 'react-icons/fi';
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/zh-cn';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
moment.locale('zh-cn');
|
||||
|
||||
@@ -97,31 +98,30 @@ export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
const response = await fetch(base + '/api/account/investment-plans', {
|
||||
credentials: 'include'
|
||||
|
||||
const response = await fetch(base + '/api/account/investment-plans', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const allItems = data.data || [];
|
||||
setPlans(allItems.filter(item => item.type === 'plan'));
|
||||
setReviews(allItems.filter(item => item.type === 'review'));
|
||||
logger.debug('InvestmentPlansAndReviews', '数据加载成功', {
|
||||
plansCount: allItems.filter(item => item.type === 'plan').length,
|
||||
reviewsCount: allItems.filter(item => item.type === 'review').length
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '无法加载投资计划和复盘记录',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
logger.error('InvestmentPlansAndReviews', 'loadData', error);
|
||||
// ❌ 移除数据加载失败 toast(非关键操作)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [toast]);
|
||||
}, []); // ✅ 移除 toast 依赖
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -154,13 +154,13 @@ export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
const url = editingItem
|
||||
|
||||
const url = editingItem
|
||||
? base + `/api/account/investment-plans/${editingItem.id}`
|
||||
: base + '/api/account/investment-plans';
|
||||
|
||||
|
||||
const method = editingItem ? 'PUT' : 'POST';
|
||||
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
@@ -171,6 +171,11 @@ export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logger.info('InvestmentPlansAndReviews', `${editingItem ? '更新' : '创建'}成功`, {
|
||||
itemId: editingItem?.id,
|
||||
title: formData.title,
|
||||
type: formData.type
|
||||
});
|
||||
toast({
|
||||
title: editingItem ? '更新成功' : '创建成功',
|
||||
status: 'success',
|
||||
@@ -182,7 +187,10 @@ export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
||||
throw new Error('保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
logger.error('InvestmentPlansAndReviews', 'handleSave', error, {
|
||||
itemId: editingItem?.id,
|
||||
title: formData?.title
|
||||
});
|
||||
toast({
|
||||
title: '保存失败',
|
||||
description: '无法保存数据',
|
||||
@@ -195,16 +203,17 @@ export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
||||
// 删除数据
|
||||
const handleDelete = async (id) => {
|
||||
if (!window.confirm('确定要删除吗?')) return;
|
||||
|
||||
|
||||
try {
|
||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
|
||||
const response = await fetch(base + `/api/account/investment-plans/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logger.info('InvestmentPlansAndReviews', '删除成功', { itemId: id });
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success',
|
||||
@@ -213,7 +222,7 @@ export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
||||
loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
logger.error('InvestmentPlansAndReviews', 'handleDelete', error, { itemId: id });
|
||||
toast({
|
||||
title: '删除失败',
|
||||
status: 'error',
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
FiAlertCircle,
|
||||
} from 'react-icons/fi';
|
||||
import { eventService } from '../../../services/eventService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import moment from 'moment';
|
||||
|
||||
export default function MyFutureEvents({ limit = 5 }) {
|
||||
@@ -49,24 +50,21 @@ export default function MyFutureEvents({ limit = 5 }) {
|
||||
const response = await eventService.calendar.getFollowingEvents();
|
||||
if (response.success) {
|
||||
// 按时间排序,最近的在前
|
||||
const sortedEvents = (response.data || []).sort((a, b) =>
|
||||
const sortedEvents = (response.data || []).sort((a, b) =>
|
||||
moment(a.calendar_time).valueOf() - moment(b.calendar_time).valueOf()
|
||||
);
|
||||
setFutureEvents(sortedEvents);
|
||||
logger.debug('MyFutureEvents', '未来事件加载成功', {
|
||||
count: sortedEvents.length
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载未来事件失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '无法加载关注的未来事件',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
logger.error('MyFutureEvents', 'loadFutureEvents', error);
|
||||
// ❌ 移除数据加载失败 toast(非关键操作)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [toast]);
|
||||
}, []); // ✅ 移除 toast 依赖
|
||||
|
||||
useEffect(() => {
|
||||
loadFutureEvents();
|
||||
@@ -78,6 +76,7 @@ export default function MyFutureEvents({ limit = 5 }) {
|
||||
const response = await eventService.calendar.toggleFollow(eventId);
|
||||
if (response.success) {
|
||||
setFutureEvents(prev => prev.filter(event => event.id !== eventId));
|
||||
logger.info('MyFutureEvents', '取消关注成功', { eventId });
|
||||
toast({
|
||||
title: '取消关注成功',
|
||||
status: 'success',
|
||||
@@ -86,7 +85,7 @@ export default function MyFutureEvents({ limit = 5 }) {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消关注失败:', error);
|
||||
logger.error('MyFutureEvents', 'handleUnfollow', error, { eventId });
|
||||
toast({
|
||||
title: '操作失败',
|
||||
description: '取消关注失败,请重试',
|
||||
|
||||
@@ -54,6 +54,7 @@ import { FiTrendingUp, FiTrendingDown, FiMinus, FiBarChart2, FiPieChart } from '
|
||||
import BarChart from '../../../components/Charts/BarChart';
|
||||
import PieChart from '../../../components/Charts/PieChart';
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
// 计算涨跌幅的辅助函数
|
||||
const calculateChange = (currentPrice, avgPrice) => {
|
||||
@@ -118,6 +119,13 @@ export default function PositionsList({ positions, account, onSellStock }) {
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
logger.info('PositionsList', '卖出成功', {
|
||||
stockCode: selectedPosition.stockCode,
|
||||
stockName: selectedPosition.stockName,
|
||||
quantity: sellQuantity,
|
||||
orderType,
|
||||
orderId: result.orderId
|
||||
});
|
||||
toast({
|
||||
title: '卖出成功',
|
||||
description: `已卖出 ${selectedPosition.stockName} ${sellQuantity} 股`,
|
||||
@@ -128,6 +136,12 @@ export default function PositionsList({ positions, account, onSellStock }) {
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('PositionsList', 'handleSellConfirm', error, {
|
||||
stockCode: selectedPosition?.stockCode,
|
||||
stockName: selectedPosition?.stockName,
|
||||
quantity: sellQuantity,
|
||||
orderType
|
||||
});
|
||||
toast({
|
||||
title: '卖出失败',
|
||||
description: error.message,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
useToast
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSearch, FiFilter, FiClock, FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
export default function TradingHistory({ history, onCancelOrder }) {
|
||||
const [filterType, setFilterType] = useState('ALL'); // ALL, BUY, SELL
|
||||
@@ -147,6 +148,7 @@ export default function TradingHistory({ history, onCancelOrder }) {
|
||||
const handleCancelOrder = async (orderId) => {
|
||||
try {
|
||||
await onCancelOrder(orderId);
|
||||
logger.info('TradingHistory', '撤单成功', { orderId });
|
||||
toast({
|
||||
title: '撤单成功',
|
||||
description: `订单 ${orderId} 已撤销`,
|
||||
@@ -155,6 +157,7 @@ export default function TradingHistory({ history, onCancelOrder }) {
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('TradingHistory', 'handleCancelOrder', error, { orderId });
|
||||
toast({
|
||||
title: '撤单失败',
|
||||
description: error.message,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/views/TradingSimulation/components/TradingPanel.js - 交易面板组件(现代化版本)
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -97,7 +98,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
setFilteredStocks(formattedResults);
|
||||
setShowStockList(true);
|
||||
} catch (error) {
|
||||
console.error('搜索股票失败:', error);
|
||||
logger.error('TradingPanel', 'handleStockSearch', error, { searchTerm });
|
||||
setFilteredStocks([]);
|
||||
setShowStockList(false);
|
||||
}
|
||||
@@ -189,6 +190,14 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
logger.info('TradingPanel', `${activeTab === 0 ? '买入' : '卖出'}成功`, {
|
||||
orderId: result.orderId,
|
||||
stockCode: selectedStock.code,
|
||||
quantity,
|
||||
orderType
|
||||
});
|
||||
|
||||
// ✅ 保留交易成功toast(关键用户操作反馈)
|
||||
toast({
|
||||
title: activeTab === 0 ? '买入成功' : '卖出成功',
|
||||
description: `订单号: ${result.orderId}`,
|
||||
@@ -202,6 +211,13 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
clearSelection();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('TradingPanel', `${activeTab === 0 ? '买入' : '卖出'}失败`, error, {
|
||||
stockCode: selectedStock?.code,
|
||||
quantity,
|
||||
orderType
|
||||
});
|
||||
|
||||
// ✅ 保留交易失败toast(关键用户操作错误反馈)
|
||||
toast({
|
||||
title: activeTab === 0 ? '买入失败' : '卖出失败',
|
||||
description: error.message,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// src/views/TradingSimulation/hooks/useTradingAccount.js - 模拟盘账户管理 Hook
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useAuth } from '../../../contexts/AuthContext';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
// API 基础URL - 修复HTTPS混合内容问题
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||
@@ -108,7 +109,7 @@ export function useTradingAccount() {
|
||||
const searchStocks = useCallback(async (keyword) => {
|
||||
// 调试模式:返回模拟数据
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:模拟股票搜索', keyword);
|
||||
logger.debug('useTradingAccount', '调试模式:模拟股票搜索', { keyword });
|
||||
const mockStocks = [
|
||||
{
|
||||
stock_code: '000001',
|
||||
@@ -147,7 +148,7 @@ export function useTradingAccount() {
|
||||
const response = await apiRequest(`/api/stocks/search?q=${encodeURIComponent(keyword)}&limit=10`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('搜索股票失败:', error);
|
||||
logger.error('useTradingAccount', 'searchStocks', error, { keyword });
|
||||
return [];
|
||||
}
|
||||
}, [user]);
|
||||
@@ -156,7 +157,7 @@ export function useTradingAccount() {
|
||||
const refreshAccount = useCallback(async () => {
|
||||
// 调试模式:使用模拟数据(因为后端API可能有CORS问题)
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:使用模拟账户数据');
|
||||
logger.debug('useTradingAccount', '调试模式:使用模拟账户数据', { userId: user?.id });
|
||||
setAccount({
|
||||
id: 'demo',
|
||||
accountName: '演示账户',
|
||||
@@ -198,7 +199,7 @@ export function useTradingAccount() {
|
||||
setTradingHistory(mapOrderData(ordersResponse.data || []));
|
||||
|
||||
} catch (err) {
|
||||
console.error('刷新账户数据失败:', err);
|
||||
logger.error('useTradingAccount', 'refreshAccount', err, { userId: user?.id });
|
||||
setError('加载账户数据失败: ' + err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -213,7 +214,7 @@ export function useTradingAccount() {
|
||||
|
||||
// 调试模式:模拟买入成功
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:模拟买入', { stockCode, quantity, orderType });
|
||||
logger.debug('useTradingAccount', '调试模式:模拟买入', { stockCode, quantity, orderType });
|
||||
return { success: true, orderId: 'demo_' + Date.now() };
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ export function useTradingAccount() {
|
||||
|
||||
// 调试模式:模拟卖出成功
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:模拟卖出', { stockCode, quantity, orderType });
|
||||
logger.debug('useTradingAccount', '调试模式:模拟卖出', { stockCode, quantity, orderType });
|
||||
return { success: true, orderId: 'demo_' + Date.now() };
|
||||
}
|
||||
|
||||
@@ -311,7 +312,7 @@ export function useTradingAccount() {
|
||||
const response = await apiRequest(`/api/simulation/transactions?${params.toString()}`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取交易记录失败:', error);
|
||||
logger.error('useTradingAccount', 'getTransactions', error, options);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
@@ -334,7 +335,7 @@ export function useTradingAccount() {
|
||||
const response = await apiRequest(`/api/simulation/statistics?days=${days}`);
|
||||
return response.data?.daily_returns || [];
|
||||
} catch (error) {
|
||||
console.error('获取资产历史失败:', error);
|
||||
logger.error('useTradingAccount', 'getAssetHistory', error, { days, userId: user?.id });
|
||||
return [];
|
||||
}
|
||||
}, [user]);
|
||||
@@ -359,7 +360,7 @@ export function useTradingAccount() {
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error('获取股票行情失败:', error);
|
||||
logger.error('useTradingAccount', 'getStockQuotes', error, { stockCodes });
|
||||
return {};
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Link,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
// 导入子组件
|
||||
import AccountOverview from './components/AccountOverview';
|
||||
@@ -86,9 +87,11 @@ export default function TradingSimulation() {
|
||||
|
||||
// 调试:观察认证状态变化
|
||||
useEffect(() => {
|
||||
try {
|
||||
console.log('🔍 TradingSimulation mounted. isAuthenticated=', isAuthenticated, 'user=', user);
|
||||
} catch {}
|
||||
logger.debug('TradingSimulation', '组件挂载,认证状态检查', {
|
||||
isAuthenticated,
|
||||
userId: user?.id,
|
||||
userName: user?.name
|
||||
});
|
||||
}, [isAuthenticated, user]);
|
||||
|
||||
// 获取资产历史数据的 useEffect
|
||||
@@ -96,8 +99,15 @@ export default function TradingSimulation() {
|
||||
if (account) {
|
||||
getAssetHistory(30).then(data => {
|
||||
setAssetHistory(data || []);
|
||||
logger.debug('TradingSimulation', '资产历史数据加载成功', {
|
||||
accountId: account.id,
|
||||
dataPoints: data?.length || 0
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('获取资产历史失败:', err);
|
||||
logger.error('TradingSimulation', 'getAssetHistory', err, {
|
||||
accountId: account?.id,
|
||||
days: 30
|
||||
});
|
||||
setAssetHistory([]);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user