702 lines
24 KiB
JavaScript
702 lines
24 KiB
JavaScript
// 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 (
|
||
<Card bg={cardBg} shadow="lg">
|
||
<CardBody>
|
||
<VStack spacing={4} py={8}>
|
||
<Icon as={FiMinus} size="48px" color={secondaryColor} />
|
||
<Text color={secondaryColor} fontSize="lg">暂无持仓</Text>
|
||
<Text color={secondaryColor} fontSize="sm">
|
||
请前往交易面板买入股票
|
||
</Text>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// 安全地准备持仓分布图表数据
|
||
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 (
|
||
<>
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 现代化持仓概览 */}
|
||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||
{/* 左侧:核心统计 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="lg" fontWeight="bold" color={textColor}>持仓概览</Text>
|
||
<Badge colorScheme="blue" variant="solid" borderRadius="full">
|
||
{positions.length} 只股票
|
||
</Badge>
|
||
</HStack>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<SimpleGrid columns={1} spacing={4}>
|
||
{/* 持仓市值 */}
|
||
<Card variant="outline">
|
||
<CardBody>
|
||
<Flex direction="row" align="center" justify="center" w="100%">
|
||
<Stat me="auto">
|
||
<StatLabel fontSize="sm" color={secondaryColor} fontWeight="bold">
|
||
持仓市值
|
||
</StatLabel>
|
||
<StatNumber fontSize="xl" color={textColor} fontWeight="bold">
|
||
{formatCurrency(totalMarketValue)}
|
||
</StatNumber>
|
||
</Stat>
|
||
<IconBox
|
||
as="div"
|
||
h={"40px"}
|
||
w={"40px"}
|
||
bg="linear-gradient(90deg, #4481EB 0%, #04BEFE 100%)"
|
||
icon={<Icon as={FiBarChart2} color="white" w="20px" h="20px" />}
|
||
/>
|
||
</Flex>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 总盈亏 */}
|
||
<Card variant="outline">
|
||
<CardBody>
|
||
<Flex direction="row" align="center" justify="center" w="100%">
|
||
<Stat me="auto">
|
||
<StatLabel fontSize="sm" color={secondaryColor} fontWeight="bold">
|
||
总盈亏
|
||
</StatLabel>
|
||
<StatNumber fontSize="xl" color={totalProfit >= 0 ? 'green.500' : 'red.500'} fontWeight="bold">
|
||
{formatCurrency(totalProfit)}
|
||
</StatNumber>
|
||
<StatHelpText color={totalProfit >= 0 ? 'green.500' : 'red.500'} fontSize="sm" fontWeight="bold">
|
||
<StatArrow type={totalProfit >= 0 ? 'increase' : 'decrease'} />
|
||
{(totalProfitPercent || 0).toFixed(2)}%
|
||
</StatHelpText>
|
||
</Stat>
|
||
<IconBox
|
||
as="div"
|
||
h={"40px"}
|
||
w={"40px"}
|
||
bg={totalProfit >= 0
|
||
? "linear-gradient(90deg, #4FD1C7 0%, #81E6D9 100%)"
|
||
: "linear-gradient(90deg, #FEB2B2 0%, #F56565 100%)"
|
||
}
|
||
icon={
|
||
<Icon
|
||
as={totalProfit >= 0 ? FiTrendingUp : FiTrendingDown}
|
||
color="white"
|
||
w="20px"
|
||
h="20px"
|
||
/>
|
||
}
|
||
/>
|
||
</Flex>
|
||
</CardBody>
|
||
</Card>
|
||
</SimpleGrid>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 右侧:持仓分布图 */}
|
||
{hasPositions && (
|
||
<Card>
|
||
<CardHeader>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="lg" fontWeight="bold" color={textColor}>持仓分布</Text>
|
||
<Icon as={FiPieChart} color="blue.500" />
|
||
</HStack>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<Box h="300px">
|
||
<PieChart
|
||
chartData={positionDistributionData}
|
||
chartOptions={positionDistributionOptions}
|
||
/>
|
||
</Box>
|
||
</CardBody>
|
||
</Card>
|
||
)}
|
||
</SimpleGrid>
|
||
|
||
{/* 盈亏分析图表 */}
|
||
{hasPositions && (
|
||
<Card>
|
||
<CardHeader>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="lg" fontWeight="bold" color={textColor}>盈亏分析</Text>
|
||
<Badge
|
||
colorScheme={totalProfit >= 0 ? 'green' : 'red'}
|
||
variant="solid"
|
||
borderRadius="full"
|
||
>
|
||
{totalProfit >= 0 ? '整体盈利' : '整体亏损'}
|
||
</Badge>
|
||
</HStack>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<Box h="300px">
|
||
<BarChart
|
||
chartData={profitBarData}
|
||
chartOptions={profitBarOptions}
|
||
/>
|
||
</Box>
|
||
</CardBody>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 持仓列表 */}
|
||
<Card bg={cardBg} shadow="lg">
|
||
<CardHeader>
|
||
<HStack justify="space-between">
|
||
<Heading size="md">我的持仓</Heading>
|
||
<Badge colorScheme="blue" variant="solid">
|
||
{positions.length} 只股票
|
||
</Badge>
|
||
</HStack>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<Box overflowX="auto">
|
||
<Table variant="simple">
|
||
<Thead>
|
||
<Tr>
|
||
<Th>股票代码</Th>
|
||
<Th>股票名称</Th>
|
||
<Th isNumeric>持仓数量</Th>
|
||
<Th isNumeric>成本价</Th>
|
||
<Th isNumeric>现价</Th>
|
||
<Th isNumeric>市值</Th>
|
||
<Th isNumeric>盈亏</Th>
|
||
<Th isNumeric>盈亏比例</Th>
|
||
<Th>操作</Th>
|
||
</Tr>
|
||
</Thead>
|
||
<Tbody>
|
||
{safePositions.map((position) => {
|
||
if (!position) return null;
|
||
|
||
const { profit, profitPercent, currentPrice, marketValue } = calculatePositionProfit(position);
|
||
const { change, changePercent } = calculateChange(currentPrice, position.avgPrice);
|
||
|
||
return (
|
||
<Tr key={position.stockCode || position.id}>
|
||
<Td fontWeight="medium">{position.stockCode || '-'}</Td>
|
||
<Td>{position.stockName || '-'}</Td>
|
||
<Td isNumeric>{(position.quantity || 0).toLocaleString()}</Td>
|
||
<Td isNumeric>¥{(position.avgPrice || 0).toFixed(2)}</Td>
|
||
<Td isNumeric>
|
||
<VStack spacing={0} align="end">
|
||
<Text>¥{(currentPrice || 0).toFixed(2)}</Text>
|
||
<Text
|
||
fontSize="xs"
|
||
color={(change || 0) >= 0 ? 'green.500' : 'red.500'}
|
||
>
|
||
{(change || 0) >= 0 ? '+' : ''}{(changePercent || 0).toFixed(2)}%
|
||
</Text>
|
||
</VStack>
|
||
</Td>
|
||
<Td isNumeric>{formatCurrency(marketValue)}</Td>
|
||
<Td isNumeric>
|
||
<Text color={profit >= 0 ? 'green.500' : 'red.500'}>
|
||
{formatCurrency(profit)}
|
||
</Text>
|
||
</Td>
|
||
<Td isNumeric>
|
||
<HStack justify="end" spacing={1}>
|
||
<Icon
|
||
as={profit >= 0 ? FiTrendingUp : FiTrendingDown}
|
||
color={profit >= 0 ? 'green.500' : 'red.500'}
|
||
/>
|
||
<Text color={(profit || 0) >= 0 ? 'green.500' : 'red.500'}>
|
||
{(profitPercent || 0).toFixed(2)}%
|
||
</Text>
|
||
</HStack>
|
||
</Td>
|
||
<Td>
|
||
<Button
|
||
colorScheme="red"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => handleSellClick(position)}
|
||
isDisabled={(position.availableQuantity || 0) <= 0}
|
||
>
|
||
卖出
|
||
</Button>
|
||
</Td>
|
||
</Tr>
|
||
);
|
||
}).filter(Boolean)}
|
||
</Tbody>
|
||
</Table>
|
||
</Box>
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
|
||
{/* 卖出对话框 */}
|
||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader>
|
||
卖出 {selectedPosition?.stockName}
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack spacing={4} align="stretch">
|
||
{selectedPosition && (
|
||
<Alert status="info">
|
||
<AlertIcon />
|
||
<AlertDescription>
|
||
当前持仓: {selectedPosition.quantity} 股,可卖: {selectedPosition.availableQuantity} 股,成本价: ¥{(selectedPosition.avgPrice || 0).toFixed(2)}
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
<FormControl>
|
||
<FormLabel>卖出数量(股)</FormLabel>
|
||
<NumberInput
|
||
value={sellQuantity}
|
||
onChange={(value) => setSellQuantity(parseInt(value) || 0)}
|
||
min={1}
|
||
max={selectedPosition?.availableQuantity || 0}
|
||
step={100}
|
||
>
|
||
<NumberInputField />
|
||
<NumberInputStepper>
|
||
<NumberIncrementStepper />
|
||
<NumberDecrementStepper />
|
||
</NumberInputStepper>
|
||
</NumberInput>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>订单类型</FormLabel>
|
||
<Select value={orderType} onChange={(e) => setOrderType(e.target.value)}>
|
||
<option value="MARKET">市价单</option>
|
||
<option value="LIMIT">限价单</option>
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{orderType === 'LIMIT' && (
|
||
<FormControl>
|
||
<FormLabel>限价价格(元)</FormLabel>
|
||
<NumberInput
|
||
value={limitPrice}
|
||
onChange={(value) => setLimitPrice(value)}
|
||
precision={2}
|
||
min={0}
|
||
>
|
||
<NumberInputField />
|
||
</NumberInput>
|
||
</FormControl>
|
||
)}
|
||
|
||
{/* 卖出金额计算 */}
|
||
{sellQuantity > 0 && (
|
||
<Card bg={useColorModeValue('gray.50', 'gray.700')}>
|
||
<CardBody py={3}>
|
||
<VStack spacing={2} align="stretch">
|
||
<HStack justify="space-between">
|
||
<Text fontSize="sm" color={secondaryColor}>交易金额</Text>
|
||
<Text fontSize="sm">{formatCurrency(totalAmount)}</Text>
|
||
</HStack>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="sm" color={secondaryColor}>手续费</Text>
|
||
<Text fontSize="sm">{formatCurrency(commission)}</Text>
|
||
</HStack>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="sm" color={secondaryColor}>印花税</Text>
|
||
<Text fontSize="sm">{formatCurrency(stampTax)}</Text>
|
||
</HStack>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="sm" color={secondaryColor}>过户费</Text>
|
||
<Text fontSize="sm">{formatCurrency(transferFee)}</Text>
|
||
</HStack>
|
||
<HStack justify="space-between">
|
||
<Text fontSize="md" fontWeight="bold">实收金额</Text>
|
||
<Text fontSize="md" fontWeight="bold" color="green.500">
|
||
{formatCurrency(netAmount)}
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
)}
|
||
</VStack>
|
||
</ModalBody>
|
||
|
||
<ModalFooter>
|
||
<Button variant="ghost" mr={3} onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
colorScheme="red"
|
||
onClick={handleSellConfirm}
|
||
isLoading={isLoading}
|
||
loadingText="卖出中..."
|
||
isDisabled={sellQuantity <= 0 || sellQuantity > (selectedPosition?.availableQuantity || 0)}
|
||
>
|
||
确认卖出
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
</>
|
||
);
|
||
}
|