// src/views/TradingSimulation/components/PositionsList.js - 持仓列表组件(现代化版本) import React, { useState } from 'react'; import { Box, Card, CardHeader, CardBody, Heading, Text, Button, Table, Thead, Tbody, Tr, Th, Td, Badge, HStack, VStack, NumberInput, NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, FormControl, FormLabel, Select, Alert, AlertIcon, AlertDescription, useDisclosure, useToast, useColorModeValue, useColorMode, Icon, Stat, StatLabel, StatNumber, StatHelpText, StatArrow, SimpleGrid, Flex } 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 IconBox from '../../../components/Icons/IconBox'; import { logger } from '../../../utils/logger'; // 计算涨跌幅的辅助函数 const calculateChange = (currentPrice, avgPrice) => { if (!avgPrice || avgPrice === 0) return { change: 0, changePercent: 0 }; const change = currentPrice - avgPrice; const changePercent = (change / avgPrice) * 100; return { change, changePercent }; }; 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', { style: 'currency', currency: 'CNY', minimumFractionDigits: 2 }).format(amount); }; // 计算持仓盈亏(使用后端返回的数据) const calculatePositionProfit = (position) => { return { profit: position.profit || 0, profitPercent: position.profitRate || 0, currentPrice: position.currentPrice || position.avgPrice, marketValue: position.marketValue || (position.currentPrice * position.quantity) }; }; // 打开卖出对话框 const handleSellClick = (position) => { 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(); }; // 执行卖出 const handleSellConfirm = async () => { if (!selectedPosition || sellQuantity <= 0) return; setIsLoading(true); const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedPosition.currentPrice || selectedPosition.avgPrice; try { const result = await onSellStock( selectedPosition.stockCode, sellQuantity, orderType, orderType === 'LIMIT' ? parseFloat(limitPrice) : null ); if (result.success) { logger.info('PositionsList', '卖出成功', { stockCode: selectedPosition.stockCode, stockName: selectedPosition.stockName, quantity: sellQuantity, 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} 股`, status: 'success', duration: 3000, isClosable: true, }); onClose(); } } catch (error) { logger.error('PositionsList', 'handleSellConfirm', error, { stockCode: selectedPosition?.stockCode, stockName: selectedPosition?.stockName, 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, status: 'error', duration: 5000, isClosable: true, }); } finally { setIsLoading(false); } }; // 计算卖出金额 const calculateSellAmount = () => { if (!selectedPosition || !sellQuantity) return { totalAmount: 0, commission: 0, stampTax: 0, netAmount: 0 }; const price = orderType === 'LIMIT' ? parseFloat(limitPrice) || 0 : selectedPosition.currentPrice || selectedPosition.avgPrice; const totalAmount = price * sellQuantity; const commission = Math.max(totalAmount * 0.00025, 5); // 万分之2.5 const stampTax = totalAmount * 0.001; // 千分之1 const transferFee = totalAmount * 0.00002; // 万分之0.2 const netAmount = totalAmount - commission - stampTax - transferFee; return { totalAmount, commission, stampTax, transferFee, netAmount }; }; // 安全地计算总持仓统计 const calculateTotalStats = () => { if (!Array.isArray(positions) || positions.length === 0) { return { totalMarketValue: 0, totalCost: 0, totalProfit: 0, totalProfitPercent: 0 }; } let totalMarketValue = 0; let totalCost = 0; let totalProfit = 0; positions.forEach(position => { if (position) { const { marketValue, profit } = calculatePositionProfit(position); totalMarketValue += marketValue || 0; totalCost += position.totalCost || (position.quantity * position.avgPrice) || 0; totalProfit += profit || 0; } }); const totalProfitPercent = totalCost > 0 ? (totalProfit / totalCost) * 100 : 0; return { totalMarketValue, totalCost, totalProfit, totalProfitPercent }; }; const { totalMarketValue, totalCost, totalProfit, totalProfitPercent } = calculateTotalStats(); const { totalAmount, commission, stampTax, transferFee, netAmount } = calculateSellAmount(); if (!positions || positions.length === 0) { return ( 暂无持仓 请前往交易面板买入股票 ); } // 安全地准备持仓分布图表数据 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 ( <> {/* 现代化持仓概览 */} {/* 左侧:核心统计 */} 持仓概览 {positions.length} 只股票 {/* 持仓市值 */} 持仓市值 {formatCurrency(totalMarketValue)} } /> {/* 总盈亏 */} 总盈亏 = 0 ? 'green.500' : 'red.500'} fontWeight="bold"> {formatCurrency(totalProfit)} = 0 ? 'green.500' : 'red.500'} fontSize="sm" fontWeight="bold"> = 0 ? 'increase' : 'decrease'} /> {(totalProfitPercent || 0).toFixed(2)}% = 0 ? "linear-gradient(90deg, #4FD1C7 0%, #81E6D9 100%)" : "linear-gradient(90deg, #FEB2B2 0%, #F56565 100%)" } icon={ = 0 ? FiTrendingUp : FiTrendingDown} color="white" w="20px" h="20px" /> } /> {/* 右侧:持仓分布图 */} {hasPositions && ( 持仓分布 )} {/* 盈亏分析图表 */} {hasPositions && ( 盈亏分析 = 0 ? 'green' : 'red'} variant="solid" borderRadius="full" > {totalProfit >= 0 ? '整体盈利' : '整体亏损'} )} {/* 持仓列表 */} 我的持仓 {positions.length} 只股票 {safePositions.map((position) => { if (!position) return null; const { profit, profitPercent, currentPrice, marketValue } = calculatePositionProfit(position); const { change, changePercent } = calculateChange(currentPrice, position.avgPrice); return ( ); }).filter(Boolean)}
股票代码 股票名称 持仓数量 成本价 现价 市值 盈亏 盈亏比例 操作
{position.stockCode || '-'} {position.stockName || '-'} {(position.quantity || 0).toLocaleString()} ¥{(position.avgPrice || 0).toFixed(2)} ¥{(currentPrice || 0).toFixed(2)} = 0 ? 'green.500' : 'red.500'} > {(change || 0) >= 0 ? '+' : ''}{(changePercent || 0).toFixed(2)}% {formatCurrency(marketValue)} = 0 ? 'green.500' : 'red.500'}> {formatCurrency(profit)} = 0 ? FiTrendingUp : FiTrendingDown} color={profit >= 0 ? 'green.500' : 'red.500'} /> = 0 ? 'green.500' : 'red.500'}> {(profitPercent || 0).toFixed(2)}%
{/* 卖出对话框 */} 卖出 {selectedPosition?.stockName} {selectedPosition && ( 当前持仓: {selectedPosition.quantity} 股,可卖: {selectedPosition.availableQuantity} 股,成本价: ¥{(selectedPosition.avgPrice || 0).toFixed(2)} )} 卖出数量(股) setSellQuantity(parseInt(value) || 0)} min={1} max={selectedPosition?.availableQuantity || 0} step={100} > 订单类型 {orderType === 'LIMIT' && ( 限价价格(元) setLimitPrice(value)} precision={2} min={0} > )} {/* 卖出金额计算 */} {sellQuantity > 0 && ( 交易金额 {formatCurrency(totalAmount)} 手续费 {formatCurrency(commission)} 印花税 {formatCurrency(stampTax)} 过户费 {formatCurrency(transferFee)} 实收金额 {formatCurrency(netAmount)} )} ); }