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 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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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 ? '买入失败' : '卖出失败',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{/* 资产走势图表 - 只在有数据时显示 */}
|
{/* 资产走势图表 - 只在有数据时显示 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user