feat: 任务 1: 集成 TradingSimulation 追踪事件任务 2: 传递 tradingEvents 到子组件
This commit is contained in:
@@ -28,7 +28,9 @@ import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiAct
|
||||
import DonutChart from '../../../components/Charts/DonutChart';
|
||||
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 secondaryColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const profitColor = account?.totalProfit >= 0 ? 'green.500' : 'red.500';
|
||||
|
||||
@@ -64,20 +64,38 @@ const calculateChange = (currentPrice, avgPrice) => {
|
||||
return { change, changePercent };
|
||||
};
|
||||
|
||||
export default function PositionsList({ positions, account, onSellStock }) {
|
||||
export default function PositionsList({ positions, account, onSellStock, tradingEvents }) {
|
||||
const [selectedPosition, setSelectedPosition] = useState(null);
|
||||
const [sellQuantity, setSellQuantity] = useState(0);
|
||||
const [orderType, setOrderType] = useState('MARKET');
|
||||
const [limitPrice, setLimitPrice] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [hasTracked, setHasTracked] = React.useState(false);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const toast = useToast();
|
||||
|
||||
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
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) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
@@ -102,6 +120,17 @@ export default function PositionsList({ positions, account, onSellStock }) {
|
||||
setSelectedPosition(position);
|
||||
setSellQuantity(position.availableQuantity); // 默认全部可卖数量
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -110,6 +139,8 @@ export default function PositionsList({ positions, account, onSellStock }) {
|
||||
if (!selectedPosition || sellQuantity <= 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedPosition.currentPrice || selectedPosition.avgPrice;
|
||||
|
||||
try {
|
||||
const result = await onSellStock(
|
||||
selectedPosition.stockCode,
|
||||
@@ -126,6 +157,20 @@ export default function PositionsList({ positions, account, onSellStock }) {
|
||||
orderType,
|
||||
orderId: result.orderId
|
||||
});
|
||||
|
||||
// 🎯 追踪卖出成功
|
||||
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
|
||||
tradingEvents.trackSimulationOrderPlaced({
|
||||
stockCode: selectedPosition.stockCode,
|
||||
stockName: selectedPosition.stockName,
|
||||
direction: 'sell',
|
||||
quantity: sellQuantity,
|
||||
price,
|
||||
orderType,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '卖出成功',
|
||||
description: `已卖出 ${selectedPosition.stockName} ${sellQuantity} 股`,
|
||||
@@ -142,6 +187,21 @@ export default function PositionsList({ positions, account, onSellStock }) {
|
||||
quantity: sellQuantity,
|
||||
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({
|
||||
title: '卖出失败',
|
||||
description: error.message,
|
||||
|
||||
@@ -34,18 +34,31 @@ import {
|
||||
import { FiSearch, FiFilter, FiClock, FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
|
||||
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 [filterStatus, setFilterStatus] = useState('ALL'); // ALL, FILLED, PENDING, CANCELLED
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortBy, setSortBy] = useState('createdAt'); // createdAt, stockCode, amount
|
||||
const [sortOrder, setSortOrder] = useState('desc'); // desc, asc
|
||||
|
||||
const [hasTracked, setHasTracked] = React.useState(false);
|
||||
|
||||
const toast = useToast();
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
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) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
|
||||
@@ -55,7 +55,7 @@ import { FiSearch, FiTrendingUp, FiTrendingDown, FiDollarSign, FiZap, FiTarget }
|
||||
// 导入现有的高质量组件
|
||||
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 [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedStock, setSelectedStock] = useState(null);
|
||||
@@ -87,7 +87,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
const results = await searchStocks(searchTerm);
|
||||
// 转换为组件需要的格式
|
||||
const formattedResults = results.map(stock => [
|
||||
stock.stock_code,
|
||||
stock.stock_code,
|
||||
{
|
||||
name: stock.stock_name,
|
||||
price: stock.current_price || 0, // 使用后端返回的真实价格
|
||||
@@ -97,10 +97,20 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
]);
|
||||
setFilteredStocks(formattedResults);
|
||||
setShowStockList(true);
|
||||
|
||||
// 🎯 追踪股票搜索
|
||||
if (tradingEvents && tradingEvents.trackSimulationStockSearched) {
|
||||
tradingEvents.trackSimulationStockSearched(searchTerm, formattedResults.length);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('TradingPanel', 'handleStockSearch', error, { searchTerm });
|
||||
setFilteredStocks([]);
|
||||
setShowStockList(false);
|
||||
|
||||
// 🎯 追踪搜索无结果
|
||||
if (tradingEvents && tradingEvents.trackSimulationStockSearched) {
|
||||
tradingEvents.trackSimulationStockSearched(searchTerm, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setFilteredStocks([]);
|
||||
@@ -109,7 +119,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
}, 300); // 300ms 防抖
|
||||
|
||||
return () => clearTimeout(searchDebounced);
|
||||
}, [searchTerm, searchStocks]);
|
||||
}, [searchTerm, searchStocks, tradingEvents]);
|
||||
|
||||
// 选择股票
|
||||
const handleSelectStock = (code, stock) => {
|
||||
@@ -169,6 +179,9 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedStock.price;
|
||||
const direction = activeTab === 0 ? 'buy' : 'sell';
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (activeTab === 0) {
|
||||
@@ -197,6 +210,19 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
orderType
|
||||
});
|
||||
|
||||
// 🎯 追踪下单成功
|
||||
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
|
||||
tradingEvents.trackSimulationOrderPlaced({
|
||||
stockCode: selectedStock.code,
|
||||
stockName: selectedStock.name,
|
||||
direction,
|
||||
quantity,
|
||||
price,
|
||||
orderType,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 保留交易成功toast(关键用户操作反馈)
|
||||
toast({
|
||||
title: activeTab === 0 ? '买入成功' : '卖出成功',
|
||||
@@ -217,6 +243,20 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
|
||||
orderType
|
||||
});
|
||||
|
||||
// 🎯 追踪下单失败
|
||||
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
|
||||
tradingEvents.trackSimulationOrderPlaced({
|
||||
stockCode: selectedStock.code,
|
||||
stockName: selectedStock.name,
|
||||
direction,
|
||||
quantity,
|
||||
price,
|
||||
orderType,
|
||||
success: false,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 保留交易失败toast(关键用户操作错误反馈)
|
||||
toast({
|
||||
title: activeTab === 0 ? '买入失败' : '卖出失败',
|
||||
|
||||
@@ -281,9 +281,14 @@ export default function TradingSimulation() {
|
||||
</Box>
|
||||
|
||||
{/* 主要功能区域 - 放在上面 */}
|
||||
<Tabs
|
||||
index={activeTab}
|
||||
onChange={setActiveTab}
|
||||
<Tabs
|
||||
index={activeTab}
|
||||
onChange={(index) => {
|
||||
setActiveTab(index);
|
||||
// 🎯 追踪 Tab 切换
|
||||
const tabNames = ['trading', 'holdings', 'history', 'margin'];
|
||||
tradingEvents.trackTabClicked(tabNames[index]);
|
||||
}}
|
||||
variant="soft-rounded"
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
@@ -298,28 +303,31 @@ export default function TradingSimulation() {
|
||||
<TabPanels>
|
||||
{/* 交易面板 */}
|
||||
<TabPanel px={0}>
|
||||
<TradingPanel
|
||||
<TradingPanel
|
||||
account={account}
|
||||
onBuyStock={buyStock}
|
||||
onSellStock={sellStock}
|
||||
searchStocks={searchStocks}
|
||||
tradingEvents={tradingEvents}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* 我的持仓 */}
|
||||
<TabPanel px={0}>
|
||||
<PositionsList
|
||||
<PositionsList
|
||||
positions={positions}
|
||||
account={account}
|
||||
onSellStock={sellStock}
|
||||
tradingEvents={tradingEvents}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* 交易历史 */}
|
||||
<TabPanel px={0}>
|
||||
<TradingHistory
|
||||
<TradingHistory
|
||||
history={tradingHistory}
|
||||
onCancelOrder={cancelOrder}
|
||||
tradingEvents={tradingEvents}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -341,7 +349,7 @@ export default function TradingSimulation() {
|
||||
<Heading size="lg" mb={4} color={useColorModeValue('gray.700', 'white')}>
|
||||
📊 账户统计分析
|
||||
</Heading>
|
||||
<AccountOverview account={account} />
|
||||
<AccountOverview account={account} tradingEvents={tradingEvents} />
|
||||
</Box>
|
||||
|
||||
{/* 资产走势图表 - 只在有数据时显示 */}
|
||||
|
||||
Reference in New Issue
Block a user