feat: 任务 1: 集成 TradingSimulation 追踪事件任务 2: 传递 tradingEvents 到子组件

This commit is contained in:
zdl
2025-10-29 14:24:39 +08:00
parent 8632e40c94
commit e5ab99bae6
5 changed files with 139 additions and 16 deletions

View File

@@ -28,7 +28,9 @@ import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiAct
import DonutChart from '../../../components/Charts/DonutChart'; import DonutChart from '../../../components/Charts/DonutChart';
import IconBox from '../../../components/Icons/IconBox'; import IconBox from '../../../components/Icons/IconBox';
export default function AccountOverview({ account }) { export default function AccountOverview({ account, tradingEvents }) {
// tradingEvents 已传递,可用于将来添加的账户重置等功能
// 例如: tradingEvents.trackAccountReset(beforeResetData)
const textColor = useColorModeValue('gray.700', 'white'); const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'gray.400'); const secondaryColor = useColorModeValue('gray.500', 'gray.400');
const profitColor = account?.totalProfit >= 0 ? 'green.500' : 'red.500'; const profitColor = account?.totalProfit >= 0 ? 'green.500' : 'red.500';

View File

@@ -64,20 +64,38 @@ const calculateChange = (currentPrice, avgPrice) => {
return { change, changePercent }; return { change, changePercent };
}; };
export default function PositionsList({ positions, account, onSellStock }) { export default function PositionsList({ positions, account, onSellStock, tradingEvents }) {
const [selectedPosition, setSelectedPosition] = useState(null); const [selectedPosition, setSelectedPosition] = useState(null);
const [sellQuantity, setSellQuantity] = useState(0); const [sellQuantity, setSellQuantity] = useState(0);
const [orderType, setOrderType] = useState('MARKET'); const [orderType, setOrderType] = useState('MARKET');
const [limitPrice, setLimitPrice] = useState(''); const [limitPrice, setLimitPrice] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [hasTracked, setHasTracked] = React.useState(false);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast(); const toast = useToast();
const cardBg = useColorModeValue('white', 'gray.800'); const cardBg = useColorModeValue('white', 'gray.800');
const textColor = useColorModeValue('gray.700', 'white'); const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'gray.400'); const secondaryColor = useColorModeValue('gray.500', 'gray.400');
// 🎯 追踪持仓查看 - 组件加载时触发一次
React.useEffect(() => {
if (!hasTracked && positions && positions.length > 0 && tradingEvents && tradingEvents.trackSimulationHoldingsViewed) {
const totalMarketValue = positions.reduce((sum, pos) => sum + (pos.marketValue || pos.quantity * pos.currentPrice || 0), 0);
const totalCost = positions.reduce((sum, pos) => sum + (pos.totalCost || pos.quantity * pos.avgPrice || 0), 0);
const totalProfit = positions.reduce((sum, pos) => sum + (pos.profit || 0), 0);
tradingEvents.trackSimulationHoldingsViewed({
count: positions.length,
totalValue: totalMarketValue,
totalCost,
profitLoss: totalProfit,
});
setHasTracked(true);
}
}, [positions, tradingEvents, hasTracked]);
// 格式化货币 // 格式化货币
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return new Intl.NumberFormat('zh-CN', { return new Intl.NumberFormat('zh-CN', {
@@ -102,6 +120,17 @@ export default function PositionsList({ positions, account, onSellStock }) {
setSelectedPosition(position); setSelectedPosition(position);
setSellQuantity(position.availableQuantity); // 默认全部可卖数量 setSellQuantity(position.availableQuantity); // 默认全部可卖数量
setLimitPrice(position.currentPrice?.toString() || position.avgPrice.toString()); setLimitPrice(position.currentPrice?.toString() || position.avgPrice.toString());
// 🎯 追踪卖出按钮点击
if (tradingEvents && tradingEvents.trackSellButtonClicked) {
tradingEvents.trackSellButtonClicked({
stockCode: position.stockCode,
stockName: position.stockName,
quantity: position.quantity,
profitLoss: position.profit || 0,
}, 'holdings');
}
onOpen(); onOpen();
}; };
@@ -110,6 +139,8 @@ export default function PositionsList({ positions, account, onSellStock }) {
if (!selectedPosition || sellQuantity <= 0) return; if (!selectedPosition || sellQuantity <= 0) return;
setIsLoading(true); setIsLoading(true);
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedPosition.currentPrice || selectedPosition.avgPrice;
try { try {
const result = await onSellStock( const result = await onSellStock(
selectedPosition.stockCode, selectedPosition.stockCode,
@@ -126,6 +157,20 @@ export default function PositionsList({ positions, account, onSellStock }) {
orderType, orderType,
orderId: result.orderId orderId: result.orderId
}); });
// 🎯 追踪卖出成功
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedPosition.stockCode,
stockName: selectedPosition.stockName,
direction: 'sell',
quantity: sellQuantity,
price,
orderType,
success: true,
});
}
toast({ toast({
title: '卖出成功', title: '卖出成功',
description: `已卖出 ${selectedPosition.stockName} ${sellQuantity}`, description: `已卖出 ${selectedPosition.stockName} ${sellQuantity}`,
@@ -142,6 +187,21 @@ export default function PositionsList({ positions, account, onSellStock }) {
quantity: sellQuantity, quantity: sellQuantity,
orderType orderType
}); });
// 🎯 追踪卖出失败
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedPosition.stockCode,
stockName: selectedPosition.stockName,
direction: 'sell',
quantity: sellQuantity,
price,
orderType,
success: false,
errorMessage: error.message,
});
}
toast({ toast({
title: '卖出失败', title: '卖出失败',
description: error.message, description: error.message,

View File

@@ -34,18 +34,31 @@ import {
import { FiSearch, FiFilter, FiClock, FiTrendingUp, FiTrendingDown } from 'react-icons/fi'; import { FiSearch, FiFilter, FiClock, FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
export default function TradingHistory({ history, onCancelOrder }) { export default function TradingHistory({ history, onCancelOrder, tradingEvents }) {
const [filterType, setFilterType] = useState('ALL'); // ALL, BUY, SELL const [filterType, setFilterType] = useState('ALL'); // ALL, BUY, SELL
const [filterStatus, setFilterStatus] = useState('ALL'); // ALL, FILLED, PENDING, CANCELLED const [filterStatus, setFilterStatus] = useState('ALL'); // ALL, FILLED, PENDING, CANCELLED
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('createdAt'); // createdAt, stockCode, amount const [sortBy, setSortBy] = useState('createdAt'); // createdAt, stockCode, amount
const [sortOrder, setSortOrder] = useState('desc'); // desc, asc const [sortOrder, setSortOrder] = useState('desc'); // desc, asc
const [hasTracked, setHasTracked] = React.useState(false);
const toast = useToast(); const toast = useToast();
const cardBg = useColorModeValue('white', 'gray.800'); const cardBg = useColorModeValue('white', 'gray.800');
const textColor = useColorModeValue('gray.700', 'white'); const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'gray.400'); const secondaryColor = useColorModeValue('gray.500', 'gray.400');
// 🎯 追踪历史记录查看 - 组件加载时触发一次
React.useEffect(() => {
if (!hasTracked && history && history.length > 0 && tradingEvents && tradingEvents.trackSimulationHistoryViewed) {
tradingEvents.trackSimulationHistoryViewed({
count: history.length,
filterBy: 'all',
dateRange: 'all',
});
setHasTracked(true);
}
}, [history, tradingEvents, hasTracked]);
// 格式化货币 // 格式化货币
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return new Intl.NumberFormat('zh-CN', { return new Intl.NumberFormat('zh-CN', {

View File

@@ -55,7 +55,7 @@ import { FiSearch, FiTrendingUp, FiTrendingDown, FiDollarSign, FiZap, FiTarget }
// 导入现有的高质量组件 // 导入现有的高质量组件
import IconBox from '../../../components/Icons/IconBox'; import IconBox from '../../../components/Icons/IconBox';
export default function TradingPanel({ account, onBuyStock, onSellStock, searchStocks }) { export default function TradingPanel({ account, onBuyStock, onSellStock, searchStocks, tradingEvents }) {
const [activeTab, setActiveTab] = useState(0); // 0: 买入, 1: 卖出 const [activeTab, setActiveTab] = useState(0); // 0: 买入, 1: 卖出
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedStock, setSelectedStock] = useState(null); const [selectedStock, setSelectedStock] = useState(null);
@@ -87,7 +87,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
const results = await searchStocks(searchTerm); const results = await searchStocks(searchTerm);
// 转换为组件需要的格式 // 转换为组件需要的格式
const formattedResults = results.map(stock => [ const formattedResults = results.map(stock => [
stock.stock_code, stock.stock_code,
{ {
name: stock.stock_name, name: stock.stock_name,
price: stock.current_price || 0, // 使用后端返回的真实价格 price: stock.current_price || 0, // 使用后端返回的真实价格
@@ -97,10 +97,20 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
]); ]);
setFilteredStocks(formattedResults); setFilteredStocks(formattedResults);
setShowStockList(true); setShowStockList(true);
// 🎯 追踪股票搜索
if (tradingEvents && tradingEvents.trackSimulationStockSearched) {
tradingEvents.trackSimulationStockSearched(searchTerm, formattedResults.length);
}
} catch (error) { } catch (error) {
logger.error('TradingPanel', 'handleStockSearch', error, { searchTerm }); logger.error('TradingPanel', 'handleStockSearch', error, { searchTerm });
setFilteredStocks([]); setFilteredStocks([]);
setShowStockList(false); setShowStockList(false);
// 🎯 追踪搜索无结果
if (tradingEvents && tradingEvents.trackSimulationStockSearched) {
tradingEvents.trackSimulationStockSearched(searchTerm, 0);
}
} }
} else { } else {
setFilteredStocks([]); setFilteredStocks([]);
@@ -109,7 +119,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
}, 300); // 300ms 防抖 }, 300); // 300ms 防抖
return () => clearTimeout(searchDebounced); return () => clearTimeout(searchDebounced);
}, [searchTerm, searchStocks]); }, [searchTerm, searchStocks, tradingEvents]);
// 选择股票 // 选择股票
const handleSelectStock = (code, stock) => { const handleSelectStock = (code, stock) => {
@@ -169,6 +179,9 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
if (!validateForm()) return; if (!validateForm()) return;
setIsLoading(true); setIsLoading(true);
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedStock.price;
const direction = activeTab === 0 ? 'buy' : 'sell';
try { try {
let result; let result;
if (activeTab === 0) { if (activeTab === 0) {
@@ -197,6 +210,19 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
orderType orderType
}); });
// 🎯 追踪下单成功
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedStock.code,
stockName: selectedStock.name,
direction,
quantity,
price,
orderType,
success: true,
});
}
// ✅ 保留交易成功toast关键用户操作反馈 // ✅ 保留交易成功toast关键用户操作反馈
toast({ toast({
title: activeTab === 0 ? '买入成功' : '卖出成功', title: activeTab === 0 ? '买入成功' : '卖出成功',
@@ -217,6 +243,20 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
orderType orderType
}); });
// 🎯 追踪下单失败
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedStock.code,
stockName: selectedStock.name,
direction,
quantity,
price,
orderType,
success: false,
errorMessage: error.message,
});
}
// ✅ 保留交易失败toast关键用户操作错误反馈 // ✅ 保留交易失败toast关键用户操作错误反馈
toast({ toast({
title: activeTab === 0 ? '买入失败' : '卖出失败', title: activeTab === 0 ? '买入失败' : '卖出失败',

View File

@@ -281,9 +281,14 @@ export default function TradingSimulation() {
</Box> </Box>
{/* 主要功能区域 - 放在上面 */} {/* 主要功能区域 - 放在上面 */}
<Tabs <Tabs
index={activeTab} index={activeTab}
onChange={setActiveTab} onChange={(index) => {
setActiveTab(index);
// 🎯 追踪 Tab 切换
const tabNames = ['trading', 'holdings', 'history', 'margin'];
tradingEvents.trackTabClicked(tabNames[index]);
}}
variant="soft-rounded" variant="soft-rounded"
colorScheme="blue" colorScheme="blue"
size="lg" size="lg"
@@ -298,28 +303,31 @@ export default function TradingSimulation() {
<TabPanels> <TabPanels>
{/* 交易面板 */} {/* 交易面板 */}
<TabPanel px={0}> <TabPanel px={0}>
<TradingPanel <TradingPanel
account={account} account={account}
onBuyStock={buyStock} onBuyStock={buyStock}
onSellStock={sellStock} onSellStock={sellStock}
searchStocks={searchStocks} searchStocks={searchStocks}
tradingEvents={tradingEvents}
/> />
</TabPanel> </TabPanel>
{/* 我的持仓 */} {/* 我的持仓 */}
<TabPanel px={0}> <TabPanel px={0}>
<PositionsList <PositionsList
positions={positions} positions={positions}
account={account} account={account}
onSellStock={sellStock} onSellStock={sellStock}
tradingEvents={tradingEvents}
/> />
</TabPanel> </TabPanel>
{/* 交易历史 */} {/* 交易历史 */}
<TabPanel px={0}> <TabPanel px={0}>
<TradingHistory <TradingHistory
history={tradingHistory} history={tradingHistory}
onCancelOrder={cancelOrder} onCancelOrder={cancelOrder}
tradingEvents={tradingEvents}
/> />
</TabPanel> </TabPanel>
@@ -341,7 +349,7 @@ export default function TradingSimulation() {
<Heading size="lg" mb={4} color={useColorModeValue('gray.700', 'white')}> <Heading size="lg" mb={4} color={useColorModeValue('gray.700', 'white')}>
📊 账户统计分析 📊 账户统计分析
</Heading> </Heading>
<AccountOverview account={account} /> <AccountOverview account={account} tradingEvents={tradingEvents} />
</Box> </Box>
{/* 资产走势图表 - 只在有数据时显示 */} {/* 资产走势图表 - 只在有数据时显示 */}