Initial commit
This commit is contained in:
364
src/views/TradingSimulation/components/AccountOverview.js
Normal file
364
src/views/TradingSimulation/components/AccountOverview.js
Normal file
@@ -0,0 +1,364 @@
|
||||
// src/views/TradingSimulation/components/AccountOverview.js - 账户概览组件(现代化版本)
|
||||
import React from 'react';
|
||||
import {
|
||||
Grid,
|
||||
GridItem,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
StatArrow,
|
||||
Box,
|
||||
Text,
|
||||
Badge,
|
||||
VStack,
|
||||
HStack,
|
||||
Progress,
|
||||
useColorModeValue,
|
||||
Icon,
|
||||
Flex,
|
||||
SimpleGrid,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader
|
||||
} from '@chakra-ui/react';
|
||||
import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiActivity } from 'react-icons/fi';
|
||||
|
||||
// 导入现有的图表组件
|
||||
import DonutChart from '../../../components/Charts/DonutChart';
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
|
||||
export default function AccountOverview({ account }) {
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const profitColor = account?.totalProfit >= 0 ? 'green.500' : 'red.500';
|
||||
const profitBg = useColorModeValue(
|
||||
account?.totalProfit >= 0 ? 'green.50' : 'red.50',
|
||||
account?.totalProfit >= 0 ? 'green.900' : 'red.900'
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<Box p={4}>
|
||||
<Text color={secondaryColor}>暂无账户数据</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 安全地计算资产配置比例
|
||||
const cashRatio = account?.totalAssets > 0 ? (account.availableCash / account.totalAssets) * 100 : 0;
|
||||
const stockRatio = account?.totalAssets > 0 ? (account.marketValue / account.totalAssets) * 100 : 0;
|
||||
|
||||
// 格式化数字
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount || 0);
|
||||
};
|
||||
|
||||
const formatPercent = (percent) => {
|
||||
return `${(percent || 0) >= 0 ? '+' : ''}${(percent || 0).toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// 安全地准备资产配置饼图数据
|
||||
const assetAllocationData = account?.totalAssets > 0 ? [
|
||||
(account.availableCash / account.totalAssets) * 100,
|
||||
(account.marketValue / account.totalAssets) * 100
|
||||
] : [100, 0];
|
||||
|
||||
const assetAllocationOptions = {
|
||||
labels: ['现金资产', '股票资产'],
|
||||
colors: ['#4299E1', '#48BB78'],
|
||||
chart: {
|
||||
width: "100%",
|
||||
height: "280px"
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
fontSize: '12px',
|
||||
labels: {
|
||||
colors: textColor
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
colors: ['#fff']
|
||||
},
|
||||
formatter: function (val) {
|
||||
return (val || 0).toFixed(1) + "%"
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
expandOnClick: false,
|
||||
donut: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
colors: ['#4299E1', '#48BB78'],
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return formatCurrency(val / 100 * (account?.totalAssets || 0))
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||
{/* 左侧:主要资产数据 */}
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 核心资产指标 */}
|
||||
<SimpleGrid columns={{ base: 2, md: 2 }} spacing={4}>
|
||||
{/* 总资产卡片 */}
|
||||
<Card minH="120px">
|
||||
<CardBody>
|
||||
<Flex direction="row" align="center" justify="center" w="100%">
|
||||
<Stat me="auto">
|
||||
<StatLabel
|
||||
fontSize="sm"
|
||||
color={secondaryColor}
|
||||
fontWeight="bold"
|
||||
pb=".1rem"
|
||||
>
|
||||
总资产
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize="lg" color={textColor} fontWeight="bold">
|
||||
{formatCurrency(account?.totalAssets)}
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
</Stat>
|
||||
<IconBox
|
||||
as="div"
|
||||
h={"45px"}
|
||||
w={"45px"}
|
||||
bg="linear-gradient(90deg, #4481EB 0%, #04BEFE 100%)"
|
||||
icon={<Icon as={FiDollarSign} color="white" w="24px" h="24px" />}
|
||||
/>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 总收益卡片 */}
|
||||
<Card minH="120px">
|
||||
<CardBody>
|
||||
<Flex direction="row" align="center" justify="center" w="100%">
|
||||
<Stat me="auto">
|
||||
<StatLabel
|
||||
fontSize="sm"
|
||||
color={secondaryColor}
|
||||
fontWeight="bold"
|
||||
pb=".1rem"
|
||||
>
|
||||
总收益
|
||||
</StatLabel>
|
||||
<Flex align="center">
|
||||
<StatNumber fontSize="lg" color={profitColor} fontWeight="bold">
|
||||
{formatCurrency(account?.totalProfit)}
|
||||
</StatNumber>
|
||||
<StatArrow
|
||||
type={(account?.totalProfit || 0) >= 0 ? 'increase' : 'decrease'}
|
||||
color={profitColor}
|
||||
ml={2}
|
||||
/>
|
||||
</Flex>
|
||||
<StatHelpText color={profitColor} fontSize="sm" fontWeight="bold">
|
||||
{formatPercent(account?.totalProfitPercent)}
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
<IconBox
|
||||
as="div"
|
||||
h={"45px"}
|
||||
w={"45px"}
|
||||
bg={(account?.totalProfit || 0) >= 0
|
||||
? "linear-gradient(90deg, #4FD1C7 0%, #81E6D9 100%)"
|
||||
: "linear-gradient(90deg, #FEB2B2 0%, #F56565 100%)"
|
||||
}
|
||||
icon={
|
||||
<Icon
|
||||
as={(account?.totalProfit || 0) >= 0 ? FiTrendingUp : FiTrendingDown}
|
||||
color="white"
|
||||
w="24px"
|
||||
h="24px"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 可用资金卡片 */}
|
||||
<Card minH="120px">
|
||||
<CardBody>
|
||||
<Flex direction="row" align="center" justify="center" w="100%">
|
||||
<Stat me="auto">
|
||||
<StatLabel
|
||||
fontSize="sm"
|
||||
color={secondaryColor}
|
||||
fontWeight="bold"
|
||||
pb=".1rem"
|
||||
>
|
||||
可用资金
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize="lg" color={textColor} fontWeight="bold">
|
||||
{formatCurrency(account?.availableCash)}
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
<StatHelpText color={secondaryColor} fontSize="xs">
|
||||
现金占比: {(cashRatio || 0).toFixed(1)}%
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
<IconBox
|
||||
as="div"
|
||||
h={"45px"}
|
||||
w={"45px"}
|
||||
bg="linear-gradient(90deg, #FFE57F 0%, #FFB74D 100%)"
|
||||
icon={<Icon as={FiTarget} color="white" w="24px" h="24px" />}
|
||||
/>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 持仓市值卡片 */}
|
||||
<Card minH="120px">
|
||||
<CardBody>
|
||||
<Flex direction="row" align="center" justify="center" w="100%">
|
||||
<Stat me="auto">
|
||||
<StatLabel
|
||||
fontSize="sm"
|
||||
color={secondaryColor}
|
||||
fontWeight="bold"
|
||||
pb=".1rem"
|
||||
>
|
||||
持仓市值
|
||||
</StatLabel>
|
||||
<Flex>
|
||||
<StatNumber fontSize="lg" color={textColor} fontWeight="bold">
|
||||
{formatCurrency(account?.marketValue)}
|
||||
</StatNumber>
|
||||
</Flex>
|
||||
<StatHelpText color={secondaryColor} fontSize="xs">
|
||||
持仓占比: {(stockRatio || 0).toFixed(1)}%
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
<IconBox
|
||||
as="div"
|
||||
h={"45px"}
|
||||
w={"45px"}
|
||||
bg="linear-gradient(90deg, #9F7AEA 0%, #805AD5 100%)"
|
||||
icon={<Icon as={FiPieChart} color="white" w="24px" h="24px" />}
|
||||
/>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 风险等级和账户状态 */}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<VStack spacing={4}>
|
||||
<HStack justify="space-between" w="full">
|
||||
<HStack spacing={3}>
|
||||
<IconBox
|
||||
as="div"
|
||||
h={"35px"}
|
||||
w={"35px"}
|
||||
bg="linear-gradient(90deg, #F093FB 0%, #F5576C 100%)"
|
||||
icon={<Icon as={FiActivity} color="white" w="16px" h="16px" />}
|
||||
/>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight="bold" color={textColor}>账户状态</Text>
|
||||
<Text fontSize="xs" color={secondaryColor}>风险等级: 中等</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Badge
|
||||
colorScheme={(account?.totalProfit || 0) >= 0 ? 'green' : 'red'}
|
||||
variant="solid"
|
||||
borderRadius="full"
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{(account?.totalProfit || 0) >= 0 ? '盈利中' : '亏损中'}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<HStack justify="space-between" w="full" fontSize="xs" color={secondaryColor}>
|
||||
<Text>创建时间: {account?.createdAt ? new Date(account.createdAt).toLocaleDateString('zh-CN') : '-'}</Text>
|
||||
<Text>最后更新: {account?.lastUpdated ? new Date(account.lastUpdated).toLocaleString('zh-CN') : '-'}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
|
||||
{/* 右侧:资产配置图表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HStack justify="space-between" w="full">
|
||||
<Text fontSize="lg" fontWeight="bold" color={textColor}>
|
||||
资产配置
|
||||
</Text>
|
||||
<Badge colorScheme="blue" variant="outline">
|
||||
实时更新
|
||||
</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="280px">
|
||||
<DonutChart
|
||||
chartData={assetAllocationData}
|
||||
chartOptions={assetAllocationOptions}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 详细配置信息 */}
|
||||
<VStack spacing={3} mt={4}>
|
||||
<HStack justify="space-between" w="full">
|
||||
<HStack spacing={2}>
|
||||
<Box w={3} h={3} bg="blue.400" borderRadius="sm" />
|
||||
<Text fontSize="sm" color={textColor}>现金资产</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" fontWeight="bold" color={textColor}>
|
||||
{formatCurrency(account?.availableCash)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack justify="space-between" w="full">
|
||||
<HStack spacing={2}>
|
||||
<Box w={3} h={3} bg="green.400" borderRadius="sm" />
|
||||
<Text fontSize="sm" color={textColor}>股票资产</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" fontWeight="bold" color={textColor}>
|
||||
{formatCurrency(account?.marketValue)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
470
src/views/TradingSimulation/components/MarginTrading.js
Normal file
470
src/views/TradingSimulation/components/MarginTrading.js
Normal file
@@ -0,0 +1,470 @@
|
||||
// src/views/TradingSimulation/components/MarginTrading.js - 融资融券组件
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Badge,
|
||||
VStack,
|
||||
HStack,
|
||||
Grid,
|
||||
GridItem,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
Progress,
|
||||
Divider,
|
||||
useColorModeValue,
|
||||
Icon
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FiTrendingUp,
|
||||
FiTrendingDown,
|
||||
FiDollarSign,
|
||||
FiAlertTriangle,
|
||||
FiInfo,
|
||||
FiShield
|
||||
} from 'react-icons/fi';
|
||||
|
||||
export default function MarginTrading({ account, onMarginBuy, onShortSell }) {
|
||||
const [activeTab, setActiveTab] = useState(0); // 0: 融资买入, 1: 融券卖出
|
||||
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const warningBg = useColorModeValue('orange.50', 'orange.900');
|
||||
const errorBg = useColorModeValue('red.50', 'red.900');
|
||||
|
||||
// 格式化货币
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// 计算融资融券相关数据
|
||||
const calculateMarginData = () => {
|
||||
if (!account) return {};
|
||||
|
||||
// 模拟融资融券额度(通常为账户资产的一定比例)
|
||||
const marginCreditLimit = account.totalAssets * 1.0; // 1倍杠杆
|
||||
const shortCreditLimit = account.totalAssets * 0.5; // 0.5倍融券额度
|
||||
|
||||
// 已使用额度
|
||||
const usedMarginCredit = account.marginBalance || 0;
|
||||
const usedShortCredit = account.shortBalance || 0;
|
||||
|
||||
// 可用额度
|
||||
const availableMarginCredit = marginCreditLimit - usedMarginCredit;
|
||||
const availableShortCredit = shortCreditLimit - usedShortCredit;
|
||||
|
||||
// 使用率
|
||||
const marginUsageRate = marginCreditLimit > 0 ? (usedMarginCredit / marginCreditLimit) * 100 : 0;
|
||||
const shortUsageRate = shortCreditLimit > 0 ? (usedShortCredit / shortCreditLimit) * 100 : 0;
|
||||
|
||||
// 风险等级
|
||||
const riskLevel = marginUsageRate > 80 ? 'HIGH' : marginUsageRate > 50 ? 'MEDIUM' : 'LOW';
|
||||
|
||||
return {
|
||||
marginCreditLimit,
|
||||
shortCreditLimit,
|
||||
usedMarginCredit,
|
||||
usedShortCredit,
|
||||
availableMarginCredit,
|
||||
availableShortCredit,
|
||||
marginUsageRate,
|
||||
shortUsageRate,
|
||||
riskLevel
|
||||
};
|
||||
};
|
||||
|
||||
const marginData = calculateMarginData();
|
||||
|
||||
// 获取风险等级颜色
|
||||
const getRiskColor = (level) => {
|
||||
switch (level) {
|
||||
case 'HIGH': return 'red';
|
||||
case 'MEDIUM': return 'orange';
|
||||
case 'LOW': return 'green';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取风险等级文本
|
||||
const getRiskText = (level) => {
|
||||
switch (level) {
|
||||
case 'HIGH': return '高风险';
|
||||
case 'MEDIUM': return '中风险';
|
||||
case 'LOW': return '低风险';
|
||||
default: return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<Alert status="warning">
|
||||
<AlertIcon />
|
||||
<AlertTitle>账户未初始化</AlertTitle>
|
||||
<AlertDescription>请先初始化交易账户</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 重要风险提示 */}
|
||||
<Alert status="warning" variant="left-accent">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>风险提示</AlertTitle>
|
||||
<AlertDescription>
|
||||
融资融券交易具有杠杆效应,可能放大收益和亏损。请充分了解相关风险,谨慎操作。
|
||||
本模拟系统仅供学习使用,实际交易请咨询专业投资顾问。
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
||||
{/* 账户信用状况 */}
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardHeader>
|
||||
<HStack justify="space-between">
|
||||
<Heading size="md">信用账户状况</Heading>
|
||||
<Badge
|
||||
colorScheme={getRiskColor(marginData.riskLevel)}
|
||||
variant="solid"
|
||||
size="lg"
|
||||
>
|
||||
{getRiskText(marginData.riskLevel)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
|
||||
{/* 融资信息 */}
|
||||
<GridItem colSpan={{ base: 12, md: 6 }}>
|
||||
<Card bg={warningBg} border="1px solid" borderColor="orange.200">
|
||||
<CardHeader pb={2}>
|
||||
<HStack>
|
||||
<Icon as={FiTrendingUp} color="orange.500" />
|
||||
<Heading size="sm">融资信息</Heading>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody pt={2}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>融资授信额度</Text>
|
||||
<Text fontSize="sm" fontWeight="medium">
|
||||
{formatCurrency(marginData.marginCreditLimit)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>已使用融资</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" color="orange.600">
|
||||
{formatCurrency(marginData.usedMarginCredit)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>可用融资</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" color="green.600">
|
||||
{formatCurrency(marginData.availableMarginCredit)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<Text fontSize="xs" color={secondaryColor}>使用率</Text>
|
||||
<Text fontSize="xs" fontWeight="medium">
|
||||
{(marginData.marginUsageRate || 0).toFixed(1)}%
|
||||
</Text>
|
||||
</HStack>
|
||||
<Progress
|
||||
value={marginData.marginUsageRate}
|
||||
colorScheme={getRiskColor(marginData.riskLevel)}
|
||||
size="sm"
|
||||
borderRadius="full"
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</GridItem>
|
||||
|
||||
{/* 融券信息 */}
|
||||
<GridItem colSpan={{ base: 12, md: 6 }}>
|
||||
<Card bg={errorBg} border="1px solid" borderColor="red.200">
|
||||
<CardHeader pb={2}>
|
||||
<HStack>
|
||||
<Icon as={FiTrendingDown} color="red.500" />
|
||||
<Heading size="sm">融券信息</Heading>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody pt={2}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>融券授信额度</Text>
|
||||
<Text fontSize="sm" fontWeight="medium">
|
||||
{formatCurrency(marginData.shortCreditLimit)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>已使用融券</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" color="red.600">
|
||||
{formatCurrency(marginData.usedShortCredit)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>可用融券</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" color="green.600">
|
||||
{formatCurrency(marginData.availableShortCredit)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<Text fontSize="xs" color={secondaryColor}>使用率</Text>
|
||||
<Text fontSize="xs" fontWeight="medium">
|
||||
{(marginData.shortUsageRate || 0).toFixed(1)}%
|
||||
</Text>
|
||||
</HStack>
|
||||
<Progress
|
||||
value={marginData.shortUsageRate}
|
||||
colorScheme={getRiskColor(marginData.riskLevel)}
|
||||
size="sm"
|
||||
borderRadius="full"
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 融资融券交易区域 */}
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardHeader>
|
||||
<Heading size="md">融资融券交易</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Tabs index={activeTab} onChange={setActiveTab} colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab color="orange.500" _selected={{ color: 'orange.600', borderColor: 'orange.500' }}>
|
||||
<HStack>
|
||||
<Icon as={FiTrendingUp} />
|
||||
<Text>融资买入</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
<Tab color="red.500" _selected={{ color: 'red.600', borderColor: 'red.500' }}>
|
||||
<HStack>
|
||||
<Icon as={FiTrendingDown} />
|
||||
<Text>融券卖出</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* 融资买入面板 */}
|
||||
<TabPanel px={0}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle fontSize="md">融资买入</AlertTitle>
|
||||
<AlertDescription>
|
||||
使用券商资金买入股票,需要支付融资利息。当股价上涨时可以获得放大收益,
|
||||
但股价下跌时亏损也会被放大。
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
||||
<Card bg={useColorModeValue('orange.50', 'orange.900')}>
|
||||
<CardBody>
|
||||
<Grid templateColumns="repeat(3, 1fr)" gap={4}>
|
||||
<Stat textAlign="center">
|
||||
<StatLabel>可用融资额度</StatLabel>
|
||||
<StatNumber color="orange.600">
|
||||
{formatCurrency(marginData.availableMarginCredit)}
|
||||
</StatNumber>
|
||||
<StatHelpText>
|
||||
<Icon as={FiDollarSign} mr={1} />
|
||||
杠杆比例: 1:1
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
<Stat textAlign="center">
|
||||
<StatLabel>融资利率</StatLabel>
|
||||
<StatNumber color="orange.600">8.35%</StatNumber>
|
||||
<StatHelpText>年化利率</StatHelpText>
|
||||
</Stat>
|
||||
<Stat textAlign="center">
|
||||
<StatLabel>最低维持担保比例</StatLabel>
|
||||
<StatNumber color="orange.600">130%</StatNumber>
|
||||
<StatHelpText>低于此比例将被强制平仓</StatHelpText>
|
||||
</Stat>
|
||||
</Grid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Alert status="warning" variant="subtle">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
<strong>注意:</strong>融资买入功能当前仅为演示。实际操作中需要满足适当性要求,
|
||||
并签署相关协议。请在"交易面板"中进行普通股票交易。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
colorScheme="orange"
|
||||
size="lg"
|
||||
isDisabled={true}
|
||||
leftIcon={<Icon as={FiAlertTriangle} />}
|
||||
>
|
||||
融资买入功能(演示版本暂不可用)
|
||||
</Button>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 融券卖出面板 */}
|
||||
<TabPanel px={0}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle fontSize="md">融券卖出</AlertTitle>
|
||||
<AlertDescription>
|
||||
借入股票后卖出,当股价下跌时买入还券获利。这是一种做空策略,
|
||||
适合在预期股价下跌时使用,但风险较高。
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
||||
<Card bg={useColorModeValue('red.50', 'red.900')}>
|
||||
<CardBody>
|
||||
<Grid templateColumns="repeat(3, 1fr)" gap={4}>
|
||||
<Stat textAlign="center">
|
||||
<StatLabel>可用融券额度</StatLabel>
|
||||
<StatNumber color="red.600">
|
||||
{formatCurrency(marginData.availableShortCredit)}
|
||||
</StatNumber>
|
||||
<StatHelpText>
|
||||
<Icon as={FiDollarSign} mr={1} />
|
||||
杠杆比例: 1:0.5
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
<Stat textAlign="center">
|
||||
<StatLabel>融券费率</StatLabel>
|
||||
<StatNumber color="red.600">10.35%</StatNumber>
|
||||
<StatHelpText>年化费率</StatHelpText>
|
||||
</Stat>
|
||||
<Stat textAlign="center">
|
||||
<StatLabel>最低维持担保比例</StatLabel>
|
||||
<StatNumber color="red.600">130%</StatNumber>
|
||||
<StatHelpText>低于此比例将被强制平仓</StatHelpText>
|
||||
</Stat>
|
||||
</Grid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Alert status="error" variant="subtle">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
<strong>高风险警告:</strong>融券卖出理论上存在无限亏损风险。
|
||||
当股价持续上涨时,亏损可能超过本金。请谨慎操作!
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert status="warning" variant="subtle">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
<strong>注意:</strong>融券卖出功能当前仅为演示。实际操作需要满足更严格的条件,
|
||||
包括但不限于资产规模、投资经验等要求。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
colorScheme="red"
|
||||
size="lg"
|
||||
isDisabled={true}
|
||||
leftIcon={<Icon as={FiAlertTriangle} />}
|
||||
>
|
||||
融券卖出功能(演示版本暂不可用)
|
||||
</Button>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 相关说明和风险提示 */}
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FiInfo} color="blue.500" />
|
||||
<Heading size="md">融资融券知识</Heading>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Box>
|
||||
<Heading size="sm" mb={2} color="blue.600">什么是融资融券?</Heading>
|
||||
<Text fontSize="sm" color={secondaryColor} lineHeight="tall">
|
||||
融资融券是指投资者向证券公司借入资金买入股票(融资)或借入股票卖出(融券)的交易行为。
|
||||
它为投资者提供了杠杆工具,可以放大投资收益,但同时也放大了投资风险。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Heading size="sm" mb={2} color="orange.600">融资交易</Heading>
|
||||
<Text fontSize="sm" color={secondaryColor} lineHeight="tall">
|
||||
• <strong>原理:</strong>向券商借钱买股票,看涨时使用<br/>
|
||||
• <strong>收益:</strong>股价上涨时获得放大收益<br/>
|
||||
• <strong>风险:</strong>股价下跌时亏损放大,需支付融资利息<br/>
|
||||
• <strong>强平:</strong>维持担保比例低于130%时可能被强制平仓
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Heading size="sm" mb={2} color="red.600">融券交易</Heading>
|
||||
<Text fontSize="sm" color={secondaryColor} lineHeight="tall">
|
||||
• <strong>原理:</strong>向券商借股票来卖,看跌时使用<br/>
|
||||
• <strong>收益:</strong>股价下跌时低价买入还券获利<br/>
|
||||
• <strong>风险:</strong>股价上涨时亏损可能无限大,需支付融券费用<br/>
|
||||
• <strong>强平:</strong>维持担保比例低于130%时可能被强制平仓
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Alert status="error" variant="left-accent">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle fontSize="sm">重要提醒</AlertTitle>
|
||||
<AlertDescription fontSize="sm">
|
||||
融资融券交易风险极高,可能导致超出本金的亏损。投资者应当充分了解相关风险,
|
||||
具备相应的风险承受能力,并在专业人士指导下进行操作。
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
627
src/views/TradingSimulation/components/PositionsList.js
Normal file
627
src/views/TradingSimulation/components/PositionsList.js
Normal file
@@ -0,0 +1,627 @@
|
||||
// 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';
|
||||
|
||||
// 计算涨跌幅的辅助函数
|
||||
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 }) {
|
||||
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 { 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');
|
||||
|
||||
// 格式化货币
|
||||
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());
|
||||
onOpen();
|
||||
};
|
||||
|
||||
// 执行卖出
|
||||
const handleSellConfirm = async () => {
|
||||
if (!selectedPosition || sellQuantity <= 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await onSellStock(
|
||||
selectedPosition.stockCode,
|
||||
sellQuantity,
|
||||
orderType,
|
||||
orderType === 'LIMIT' ? parseFloat(limitPrice) : null
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: '卖出成功',
|
||||
description: `已卖出 ${selectedPosition.stockName} ${sellQuantity} 股`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
393
src/views/TradingSimulation/components/TradingHistory.js
Normal file
393
src/views/TradingSimulation/components/TradingHistory.js
Normal file
@@ -0,0 +1,393 @@
|
||||
// src/views/TradingSimulation/components/TradingHistory.js - 交易历史组件
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Badge,
|
||||
HStack,
|
||||
VStack,
|
||||
Select,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Flex,
|
||||
Spacer,
|
||||
Icon,
|
||||
useColorModeValue,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertDescription,
|
||||
Tooltip,
|
||||
useToast
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSearch, FiFilter, FiClock, FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
|
||||
|
||||
export default function TradingHistory({ history, onCancelOrder }) {
|
||||
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 toast = useToast();
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
|
||||
|
||||
// 格式化货币
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤和排序历史记录
|
||||
const filteredAndSortedHistory = useMemo(() => {
|
||||
let filtered = [...(history || [])];
|
||||
|
||||
// 按类型过滤
|
||||
if (filterType !== 'ALL') {
|
||||
filtered = filtered.filter(trade => trade.type === filterType);
|
||||
}
|
||||
|
||||
// 按状态过滤
|
||||
if (filterStatus !== 'ALL') {
|
||||
filtered = filtered.filter(trade => trade.status === filterStatus);
|
||||
}
|
||||
|
||||
// 按搜索词过滤
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(trade =>
|
||||
trade.stockCode.includes(searchTerm) ||
|
||||
trade.stockName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
trade.orderId.includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// 排序
|
||||
filtered.sort((a, b) => {
|
||||
let aValue, bValue;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'stockCode':
|
||||
aValue = a.stockCode;
|
||||
bValue = b.stockCode;
|
||||
break;
|
||||
case 'amount':
|
||||
aValue = a.totalAmount;
|
||||
bValue = b.totalAmount;
|
||||
break;
|
||||
default:
|
||||
aValue = new Date(a.createdAt);
|
||||
bValue = new Date(b.createdAt);
|
||||
}
|
||||
|
||||
if (sortOrder === 'desc') {
|
||||
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
||||
} else {
|
||||
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [history, filterType, filterStatus, searchTerm, sortBy, sortOrder]);
|
||||
|
||||
// 获取状态样式
|
||||
const getStatusBadge = (status) => {
|
||||
switch (status) {
|
||||
case 'FILLED':
|
||||
return <Badge colorScheme="green" variant="solid">已成交</Badge>;
|
||||
case 'PENDING':
|
||||
return <Badge colorScheme="yellow" variant="solid">待成交</Badge>;
|
||||
case 'CANCELLED':
|
||||
return <Badge colorScheme="gray" variant="solid">已撤销</Badge>;
|
||||
default:
|
||||
return <Badge colorScheme="gray">未知</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取交易类型样式
|
||||
const getTypeBadge = (type) => {
|
||||
switch (type) {
|
||||
case 'BUY':
|
||||
return <Badge colorScheme="green" variant="outline">买入</Badge>;
|
||||
case 'SELL':
|
||||
return <Badge colorScheme="red" variant="outline">卖出</Badge>;
|
||||
default:
|
||||
return <Badge colorScheme="gray" variant="outline">未知</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
// 撤销订单
|
||||
const handleCancelOrder = async (orderId) => {
|
||||
try {
|
||||
await onCancelOrder(orderId);
|
||||
toast({
|
||||
title: '撤单成功',
|
||||
description: `订单 ${orderId} 已撤销`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '撤单失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 计算统计数据
|
||||
const stats = useMemo(() => {
|
||||
const filled = history?.filter(t => t.status === 'FILLED') || [];
|
||||
const totalTrades = filled.length;
|
||||
const buyTrades = filled.filter(t => t.type === 'BUY').length;
|
||||
const sellTrades = filled.filter(t => t.type === 'SELL').length;
|
||||
const totalVolume = filled.reduce((sum, t) => sum + t.totalAmount, 0);
|
||||
|
||||
return { totalTrades, buyTrades, sellTrades, totalVolume };
|
||||
}, [history]);
|
||||
|
||||
if (!history || history.length === 0) {
|
||||
return (
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardBody>
|
||||
<VStack spacing={4} py={8}>
|
||||
<Icon as={FiClock} size="48px" color={secondaryColor} />
|
||||
<Text color={secondaryColor} fontSize="lg">暂无交易记录</Text>
|
||||
<Text color={secondaryColor} fontSize="sm">
|
||||
完成首笔交易后,历史记录将在这里显示
|
||||
</Text>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 交易统计 */}
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardHeader>
|
||||
<Heading size="md">交易统计</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<HStack spacing={8} justify="space-around">
|
||||
<VStack>
|
||||
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
|
||||
{stats.totalTrades}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={secondaryColor}>总交易次数</Text>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<Text fontSize="2xl" fontWeight="bold" color="green.500">
|
||||
{stats.buyTrades}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={secondaryColor}>买入次数</Text>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<Text fontSize="2xl" fontWeight="bold" color="red.500">
|
||||
{stats.sellTrades}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={secondaryColor}>卖出次数</Text>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<Text fontSize="2xl" fontWeight="bold" color="purple.500">
|
||||
{formatCurrency(stats.totalVolume)}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={secondaryColor}>累计成交额</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 过滤和搜索 */}
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardBody>
|
||||
<Flex direction={{ base: 'column', md: 'row' }} gap={4} align="end">
|
||||
<Box flex={1}>
|
||||
<Text fontSize="sm" color={secondaryColor} mb={2}>搜索</Text>
|
||||
<InputGroup>
|
||||
<InputLeftElement>
|
||||
<Icon as={FiSearch} color={secondaryColor} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="搜索股票代码、名称或订单号..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Box>
|
||||
|
||||
<Box minW="120px">
|
||||
<Text fontSize="sm" color={secondaryColor} mb={2}>交易类型</Text>
|
||||
<Select value={filterType} onChange={(e) => setFilterType(e.target.value)}>
|
||||
<option value="ALL">全部</option>
|
||||
<option value="BUY">买入</option>
|
||||
<option value="SELL">卖出</option>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box minW="120px">
|
||||
<Text fontSize="sm" color={secondaryColor} mb={2}>订单状态</Text>
|
||||
<Select value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
|
||||
<option value="ALL">全部</option>
|
||||
<option value="FILLED">已成交</option>
|
||||
<option value="PENDING">待成交</option>
|
||||
<option value="CANCELLED">已撤销</option>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box minW="120px">
|
||||
<Text fontSize="sm" color={secondaryColor} mb={2}>排序方式</Text>
|
||||
<Select
|
||||
value={`${sortBy}-${sortOrder}`}
|
||||
onChange={(e) => {
|
||||
const [field, order] = e.target.value.split('-');
|
||||
setSortBy(field);
|
||||
setSortOrder(order);
|
||||
}}
|
||||
>
|
||||
<option value="createdAt-desc">时间 ↓</option>
|
||||
<option value="createdAt-asc">时间 ↑</option>
|
||||
<option value="stockCode-asc">股票代码 ↑</option>
|
||||
<option value="amount-desc">金额 ↓</option>
|
||||
<option value="amount-asc">金额 ↑</option>
|
||||
</Select>
|
||||
</Box>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 交易历史列表 */}
|
||||
<Card bg={cardBg} shadow="lg">
|
||||
<CardHeader>
|
||||
<HStack justify="space-between">
|
||||
<Heading size="md">交易历史</Heading>
|
||||
<Badge colorScheme="blue" variant="solid">
|
||||
{filteredAndSortedHistory.length} 条记录
|
||||
</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{filteredAndSortedHistory.length === 0 ? (
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
没有找到符合条件的交易记录
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Box overflowX="auto">
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>订单号</Th>
|
||||
<Th>交易时间</Th>
|
||||
<Th>股票</Th>
|
||||
<Th>类型</Th>
|
||||
<Th isNumeric>数量</Th>
|
||||
<Th isNumeric>价格</Th>
|
||||
<Th isNumeric>成交金额</Th>
|
||||
<Th isNumeric>手续费</Th>
|
||||
<Th>状态</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filteredAndSortedHistory.map((trade) => (
|
||||
<Tr key={trade.orderId}>
|
||||
<Td>
|
||||
<Tooltip label={trade.orderId}>
|
||||
<Text fontSize="xs" fontFamily="mono">
|
||||
{trade.orderId.substring(0, 12)}...
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<Text fontSize="xs">
|
||||
{formatDateTime(trade.createdAt)}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td>
|
||||
<VStack spacing={0} align="start">
|
||||
<Text fontSize="sm" fontWeight="medium">
|
||||
{trade.stockCode}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={secondaryColor}>
|
||||
{trade.stockName}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Td>
|
||||
<Td>{getTypeBadge(trade.type)}</Td>
|
||||
<Td isNumeric>{trade.quantity.toLocaleString()}</Td>
|
||||
<Td isNumeric>¥{(trade.price || 0).toFixed(2)}</Td>
|
||||
<Td isNumeric>
|
||||
<Text color={trade.type === 'BUY' ? 'red.500' : 'green.500'}>
|
||||
{formatCurrency(trade.totalAmount)}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<VStack spacing={0} align="end">
|
||||
<Text fontSize="sm">
|
||||
{formatCurrency(trade.commission)}
|
||||
</Text>
|
||||
{trade.stampTax && (
|
||||
<Text fontSize="xs" color={secondaryColor}>
|
||||
印花税: {formatCurrency(trade.stampTax)}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</Td>
|
||||
<Td>{getStatusBadge(trade.status)}</Td>
|
||||
<Td>
|
||||
{trade.status === 'PENDING' && (
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={() => handleCancelOrder(trade.orderId)}
|
||||
>
|
||||
撤单
|
||||
</Button>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
536
src/views/TradingSimulation/components/TradingPanel.js
Normal file
536
src/views/TradingSimulation/components/TradingPanel.js
Normal file
@@ -0,0 +1,536 @@
|
||||
// src/views/TradingSimulation/components/TradingPanel.js - 交易面板组件(现代化版本)
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Heading,
|
||||
Text,
|
||||
Input,
|
||||
Button,
|
||||
Select,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormErrorMessage,
|
||||
VStack,
|
||||
HStack,
|
||||
Grid,
|
||||
GridItem,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Badge,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertDescription,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
useToast,
|
||||
useColorModeValue,
|
||||
Spinner,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Icon,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSearch, FiTrendingUp, FiTrendingDown, FiDollarSign, FiZap, FiTarget } from 'react-icons/fi';
|
||||
|
||||
// 导入现有的高质量组件
|
||||
import IconBox from '../../../components/Icons/IconBox';
|
||||
|
||||
export default function TradingPanel({ account, onBuyStock, onSellStock, searchStocks }) {
|
||||
const [activeTab, setActiveTab] = useState(0); // 0: 买入, 1: 卖出
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedStock, setSelectedStock] = useState(null);
|
||||
const [quantity, setQuantity] = useState(100);
|
||||
const [orderType, setOrderType] = useState('MARKET');
|
||||
const [limitPrice, setLimitPrice] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [filteredStocks, setFilteredStocks] = useState([]);
|
||||
const [showStockList, setShowStockList] = useState(false);
|
||||
|
||||
const toast = useToast();
|
||||
const searchInputRef = useRef();
|
||||
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const headerBgGradient = useColorModeValue('linear(to-r, blue.500, purple.600)', 'linear(to-r, blue.500, purple.600)');
|
||||
const stockRowHoverBg = useColorModeValue('gray.50', 'gray.700');
|
||||
const selectedInfoBg = useColorModeValue('blue.50', 'blue.900');
|
||||
const calcCardBg = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
// 搜索股票
|
||||
useEffect(() => {
|
||||
const searchDebounced = setTimeout(async () => {
|
||||
if (searchTerm.length >= 2) {
|
||||
try {
|
||||
const results = await searchStocks(searchTerm);
|
||||
// 转换为组件需要的格式
|
||||
const formattedResults = results.map(stock => [
|
||||
stock.stock_code,
|
||||
{
|
||||
name: stock.stock_name,
|
||||
price: stock.current_price || 0, // 使用后端返回的真实价格
|
||||
change: 0, // 暂时设为0,可以后续计算
|
||||
changePercent: 0
|
||||
}
|
||||
]);
|
||||
setFilteredStocks(formattedResults);
|
||||
setShowStockList(true);
|
||||
} catch (error) {
|
||||
console.error('搜索股票失败:', error);
|
||||
setFilteredStocks([]);
|
||||
setShowStockList(false);
|
||||
}
|
||||
} else {
|
||||
setFilteredStocks([]);
|
||||
setShowStockList(false);
|
||||
}
|
||||
}, 300); // 300ms 防抖
|
||||
|
||||
return () => clearTimeout(searchDebounced);
|
||||
}, [searchTerm, searchStocks]);
|
||||
|
||||
// 选择股票
|
||||
const handleSelectStock = (code, stock) => {
|
||||
setSelectedStock({ code, ...stock });
|
||||
setSearchTerm(`${code} ${stock.name}`);
|
||||
setShowStockList(false);
|
||||
setLimitPrice(stock.price.toString());
|
||||
};
|
||||
|
||||
// 清除选择
|
||||
const clearSelection = () => {
|
||||
setSelectedStock(null);
|
||||
setSearchTerm('');
|
||||
setShowStockList(false);
|
||||
setLimitPrice('');
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
// 验证表单
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!selectedStock) {
|
||||
newErrors.stock = '请选择股票';
|
||||
}
|
||||
|
||||
if (!quantity || quantity <= 0) {
|
||||
newErrors.quantity = '请输入有效的买入数量';
|
||||
}
|
||||
|
||||
if (quantity % 100 !== 0) {
|
||||
newErrors.quantity = '股票数量必须是100的倍数';
|
||||
}
|
||||
|
||||
if (orderType === 'LIMIT' && (!limitPrice || parseFloat(limitPrice) <= 0)) {
|
||||
newErrors.limitPrice = '请输入有效的限价';
|
||||
}
|
||||
|
||||
// 买入资金检查
|
||||
if (activeTab === 0 && selectedStock) {
|
||||
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedStock.price;
|
||||
const totalCost = price * quantity;
|
||||
const commission = Math.max(totalCost * 0.0003, 5);
|
||||
const totalAmount = totalCost + commission;
|
||||
|
||||
if (totalAmount > account.availableCash) {
|
||||
newErrors.funds = '可用资金不足';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
// 执行交易
|
||||
const handleTrade = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
let result;
|
||||
if (activeTab === 0) {
|
||||
// 买入
|
||||
result = await onBuyStock(
|
||||
selectedStock.code,
|
||||
quantity,
|
||||
orderType,
|
||||
orderType === 'LIMIT' ? parseFloat(limitPrice) : null
|
||||
);
|
||||
} else {
|
||||
// 卖出
|
||||
result = await onSellStock(
|
||||
selectedStock.code,
|
||||
quantity,
|
||||
orderType,
|
||||
orderType === 'LIMIT' ? parseFloat(limitPrice) : null
|
||||
);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: activeTab === 0 ? '买入成功' : '卖出成功',
|
||||
description: `订单号: ${result.orderId}`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
setQuantity(100);
|
||||
clearSelection();
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: activeTab === 0 ? '买入失败' : '卖出失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算交易金额
|
||||
const calculateTradeAmount = () => {
|
||||
if (!selectedStock || !quantity) return { totalCost: 0, commission: 0, totalAmount: 0 };
|
||||
|
||||
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) || 0 : selectedStock.price;
|
||||
const totalCost = price * quantity;
|
||||
const commission = Math.max(totalCost * 0.0003, 5);
|
||||
const stampTax = activeTab === 1 ? totalCost * 0.001 : 0; // 卖出印花税
|
||||
const totalAmount = activeTab === 0
|
||||
? totalCost + commission
|
||||
: totalCost - commission - stampTax;
|
||||
|
||||
return { totalCost, commission, stampTax, totalAmount };
|
||||
};
|
||||
|
||||
const { totalCost, commission, stampTax, totalAmount } = calculateTradeAmount();
|
||||
|
||||
// 格式化货币
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card shadow="2xl" borderRadius="2xl" overflow="hidden">
|
||||
{/* 现代化标题栏 */}
|
||||
<Box
|
||||
bgGradient={headerBgGradient}
|
||||
color="white"
|
||||
p={6}
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<HStack spacing={3}>
|
||||
<IconBox
|
||||
as="div"
|
||||
h={"40px"}
|
||||
w={"40px"}
|
||||
bg="whiteAlpha.200"
|
||||
icon={<Icon as={FiZap} color="white" w="20px" h="20px" />}
|
||||
/>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Heading size="md">智能交易面板</Heading>
|
||||
<Text fontSize="sm" opacity={0.8}>快速下单,实时成交</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Badge colorScheme="whiteAlpha" variant="solid" borderRadius="full">
|
||||
7×24小时
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<CardBody p={6}>
|
||||
<Tabs index={activeTab} onChange={setActiveTab} colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab color="green.500" _selected={{ color: 'green.600', borderColor: 'green.500' }}>
|
||||
买入
|
||||
</Tab>
|
||||
<Tab color="red.500" _selected={{ color: 'red.600', borderColor: 'red.500' }}>
|
||||
卖出
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel px={0}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 现代化股票搜索 */}
|
||||
<FormControl isInvalid={errors.stock}>
|
||||
<FormLabel fontWeight="bold" color={textColor}>选择股票</FormLabel>
|
||||
<Box position="relative">
|
||||
<InputGroup size="lg">
|
||||
<InputLeftElement>
|
||||
<Icon as={FiSearch} color="gray.400" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="输入股票代码或名称搜索..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
borderRadius="xl"
|
||||
border="2px solid"
|
||||
borderColor="gray.200"
|
||||
_focus={{
|
||||
borderColor: "blue.400",
|
||||
boxShadow: "0 0 0 3px rgba(66, 153, 225, 0.1)"
|
||||
}}
|
||||
_hover={{
|
||||
borderColor: "gray.300"
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
{showStockList && filteredStocks.length > 0 && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="100%"
|
||||
left={0}
|
||||
right={0}
|
||||
bg={cardBg}
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
shadow="lg"
|
||||
zIndex={10}
|
||||
maxH="300px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>代码</Th>
|
||||
<Th>名称</Th>
|
||||
<Th isNumeric>价格</Th>
|
||||
<Th isNumeric>涨跌</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filteredStocks.map(([code, stock]) => (
|
||||
<Tr
|
||||
key={code}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: stockRowHoverBg }}
|
||||
onClick={() => handleSelectStock(code, stock)}
|
||||
>
|
||||
<Td fontWeight="medium">{code}</Td>
|
||||
<Td>{stock.name}</Td>
|
||||
<Td isNumeric>{(stock.price || 0).toFixed(2)}</Td>
|
||||
<Td isNumeric>
|
||||
<Text color={(stock.change || 0) >= 0 ? 'green.500' : 'red.500'}>
|
||||
{(stock.change || 0) >= 0 ? '+' : ''}{(stock.change || 0).toFixed(2)}
|
||||
({(stock.changePercent || 0).toFixed(2)}%)
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{errors.stock && <FormErrorMessage>{errors.stock}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
|
||||
{/* 选中股票信息 */}
|
||||
{selectedStock && (
|
||||
<Card bg={selectedInfoBg} borderColor="blue.200">
|
||||
<CardBody py={3}>
|
||||
<Grid templateColumns="repeat(4, 1fr)" gap={4}>
|
||||
<Stat>
|
||||
<StatLabel fontSize="xs">股票代码</StatLabel>
|
||||
<StatNumber fontSize="md">{selectedStock.code}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel fontSize="xs">股票名称</StatLabel>
|
||||
<StatNumber fontSize="md">{selectedStock.name}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel fontSize="xs">当前价格</StatLabel>
|
||||
<StatNumber fontSize="md">¥{(selectedStock.price || 0).toFixed(2)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel fontSize="xs">涨跌幅</StatLabel>
|
||||
<StatNumber
|
||||
fontSize="md"
|
||||
color={(selectedStock.change || 0) >= 0 ? 'green.500' : 'red.500'}
|
||||
>
|
||||
{(selectedStock.change || 0) >= 0 ? '+' : ''}{(selectedStock.changePercent || 0).toFixed(2)}%
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
</Grid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 交易参数 */}
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
|
||||
<FormControl isInvalid={errors.quantity}>
|
||||
<FormLabel>买入数量(股)</FormLabel>
|
||||
<NumberInput
|
||||
value={quantity}
|
||||
onChange={(value) => setQuantity(parseInt(value) || 0)}
|
||||
min={100}
|
||||
step={100}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
{errors.quantity && <FormErrorMessage>{errors.quantity}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>订单类型</FormLabel>
|
||||
<Select value={orderType} onChange={(e) => setOrderType(e.target.value)}>
|
||||
<option value="MARKET">市价单</option>
|
||||
<option value="LIMIT">限价单</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 限价输入 */}
|
||||
{orderType === 'LIMIT' && (
|
||||
<FormControl isInvalid={errors.limitPrice}>
|
||||
<FormLabel>限价价格(元)</FormLabel>
|
||||
<NumberInput
|
||||
value={limitPrice}
|
||||
onChange={(value) => setLimitPrice(value)}
|
||||
precision={2}
|
||||
min={0}
|
||||
>
|
||||
<NumberInputField />
|
||||
</NumberInput>
|
||||
{errors.limitPrice && <FormErrorMessage>{errors.limitPrice}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
{/* 交易金额计算 */}
|
||||
{selectedStock && quantity > 0 && (
|
||||
<Card bg={calcCardBg}>
|
||||
<CardBody py={3}>
|
||||
<VStack spacing={2} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color={secondaryColor}>交易金额</Text>
|
||||
<Text fontSize="sm" fontWeight="medium">{formatCurrency(totalCost)}</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="md" fontWeight="bold" color={textColor}>所需资金</Text>
|
||||
<Text fontSize="md" fontWeight="bold" color="green.500">
|
||||
{formatCurrency(totalAmount)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 资金检查 */}
|
||||
{errors.funds && (
|
||||
<Alert status="error" size="sm">
|
||||
<AlertIcon />
|
||||
<AlertDescription>{errors.funds}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 现代化操作按钮 */}
|
||||
<HStack spacing={4}>
|
||||
<Button
|
||||
size="lg"
|
||||
flex={1}
|
||||
bgGradient="linear(to-r, green.400, green.600)"
|
||||
color="white"
|
||||
borderRadius="xl"
|
||||
fontWeight="bold"
|
||||
isLoading={isLoading}
|
||||
loadingText="买入中..."
|
||||
onClick={handleTrade}
|
||||
isDisabled={!selectedStock || !quantity}
|
||||
_hover={{
|
||||
bgGradient: "linear(to-r, green.500, green.700)",
|
||||
transform: "translateY(-2px)",
|
||||
shadow: "xl"
|
||||
}}
|
||||
_active={{
|
||||
transform: "translateY(0)"
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
leftIcon={<Icon as={FiTarget} />}
|
||||
>
|
||||
立即买入
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
borderRadius="xl"
|
||||
borderColor="gray.300"
|
||||
color="gray.600"
|
||||
onClick={clearSelection}
|
||||
isDisabled={isLoading}
|
||||
_hover={{
|
||||
bg: "gray.50",
|
||||
borderColor: "gray.400",
|
||||
transform: "translateY(-1px)"
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
清除
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 卖出面板 - 与买入类似,但需要检查持仓 */}
|
||||
<TabPanel px={0}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Text color={secondaryColor} textAlign="center">
|
||||
卖出功能与买入类似,需要检查持仓数量
|
||||
</Text>
|
||||
{/* 这里可以复制买入的逻辑,但需要增加持仓检查 */}
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
卖出功能请在"我的持仓"页面中进行操作
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
383
src/views/TradingSimulation/hooks/useTradingAccount.js
Normal file
383
src/views/TradingSimulation/hooks/useTradingAccount.js
Normal file
@@ -0,0 +1,383 @@
|
||||
// src/views/TradingSimulation/hooks/useTradingAccount.js - 模拟盘账户管理 Hook
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useAuth } from '../../../contexts/AuthContext';
|
||||
|
||||
// API 基础URL - 修复HTTPS混合内容问题
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||
? '' // 生产环境使用相对路径(通过nginx代理)
|
||||
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
// API 请求封装
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
credentials: 'include', // 包含session cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// 数据字段映射函数
|
||||
const mapAccountData = (backendData) => {
|
||||
return {
|
||||
id: backendData.account_id,
|
||||
accountName: backendData.account_name,
|
||||
initialCash: backendData.initial_capital,
|
||||
availableCash: backendData.available_cash,
|
||||
frozenCash: backendData.frozen_cash,
|
||||
marketValue: backendData.position_value,
|
||||
totalAssets: backendData.total_assets,
|
||||
totalProfit: backendData.total_profit,
|
||||
totalProfitPercent: backendData.total_profit_rate,
|
||||
dailyProfit: backendData.daily_profit,
|
||||
dailyProfitRate: backendData.daily_profit_rate,
|
||||
riskLevel: 'MEDIUM', // 默认值
|
||||
marginBalance: 0,
|
||||
shortBalance: 0,
|
||||
lastUpdated: backendData.updated_at,
|
||||
createdAt: backendData.created_at
|
||||
};
|
||||
};
|
||||
|
||||
const mapPositionData = (backendPositions) => {
|
||||
return backendPositions.map(pos => ({
|
||||
id: pos.id,
|
||||
stockCode: pos.stock_code,
|
||||
stockName: pos.stock_name,
|
||||
quantity: pos.position_qty,
|
||||
availableQuantity: pos.available_qty,
|
||||
frozenQuantity: pos.frozen_qty,
|
||||
avgPrice: pos.avg_cost,
|
||||
currentPrice: pos.current_price,
|
||||
totalCost: pos.position_qty * pos.avg_cost,
|
||||
marketValue: pos.market_value,
|
||||
profit: pos.profit,
|
||||
profitRate: pos.profit_rate,
|
||||
todayProfit: pos.today_profit,
|
||||
todayProfitRate: pos.today_profit_rate,
|
||||
updatedAt: pos.updated_at
|
||||
}));
|
||||
};
|
||||
|
||||
const mapOrderData = (backendOrders) => {
|
||||
return backendOrders.map(order => ({
|
||||
id: order.id,
|
||||
orderId: order.order_no,
|
||||
stockCode: order.stock_code,
|
||||
stockName: order.stock_name,
|
||||
type: order.order_type, // 添加 type 字段
|
||||
orderType: order.order_type,
|
||||
priceType: order.price_type,
|
||||
orderPrice: order.order_price,
|
||||
quantity: order.order_qty,
|
||||
filledQuantity: order.filled_qty,
|
||||
price: order.filled_price, // 添加 price 字段
|
||||
filledPrice: order.filled_price,
|
||||
totalAmount: order.filled_amount, // 添加 totalAmount 字段
|
||||
filledAmount: order.filled_amount,
|
||||
commission: order.commission,
|
||||
stampTax: order.stamp_tax,
|
||||
transferFee: order.transfer_fee,
|
||||
totalFee: order.total_fee,
|
||||
status: order.status,
|
||||
rejectReason: order.reject_reason,
|
||||
createdAt: order.order_time,
|
||||
filledAt: order.filled_time
|
||||
}));
|
||||
};
|
||||
|
||||
export function useTradingAccount() {
|
||||
const { user } = useAuth();
|
||||
const [account, setAccount] = useState(null);
|
||||
const [positions, setPositions] = useState([]);
|
||||
const [tradingHistory, setTradingHistory] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [stockQuotes, setStockQuotes] = useState({});
|
||||
|
||||
// 搜索股票
|
||||
const searchStocks = useCallback(async (keyword) => {
|
||||
// 调试模式:返回模拟数据
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:模拟股票搜索', keyword);
|
||||
const mockStocks = [
|
||||
{
|
||||
stock_code: '000001',
|
||||
stock_name: '平安银行',
|
||||
current_price: 12.50,
|
||||
pinyin_abbr: 'payh',
|
||||
security_type: 'A股',
|
||||
exchange: '深交所'
|
||||
},
|
||||
{
|
||||
stock_code: '600036',
|
||||
stock_name: '招商银行',
|
||||
current_price: 42.80,
|
||||
pinyin_abbr: 'zsyh',
|
||||
security_type: 'A股',
|
||||
exchange: '上交所'
|
||||
},
|
||||
{
|
||||
stock_code: '688256',
|
||||
stock_name: '寒武纪',
|
||||
current_price: 1394.94,
|
||||
pinyin_abbr: 'hwj',
|
||||
security_type: 'A股',
|
||||
exchange: '上交所科创板'
|
||||
}
|
||||
];
|
||||
|
||||
return mockStocks.filter(stock =>
|
||||
stock.stock_code.includes(keyword) ||
|
||||
stock.stock_name.includes(keyword) ||
|
||||
stock.pinyin_abbr.includes(keyword.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/stocks/search?q=${encodeURIComponent(keyword)}&limit=10`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('搜索股票失败:', error);
|
||||
return [];
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// 刷新账户数据
|
||||
const refreshAccount = useCallback(async () => {
|
||||
// 调试模式:使用模拟数据(因为后端API可能有CORS问题)
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:使用模拟账户数据');
|
||||
setAccount({
|
||||
id: 'demo',
|
||||
accountName: '演示账户',
|
||||
initialCash: 1000000,
|
||||
availableCash: 950000,
|
||||
frozenCash: 0,
|
||||
marketValue: 50000,
|
||||
totalAssets: 1000000,
|
||||
totalProfit: 0,
|
||||
totalProfitPercent: 0,
|
||||
dailyProfit: 0,
|
||||
dailyProfitRate: 0,
|
||||
riskLevel: 'MEDIUM',
|
||||
marginBalance: 0,
|
||||
shortBalance: 0,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
setPositions([]);
|
||||
setTradingHistory([]);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 获取账户信息
|
||||
const accountResponse = await apiRequest('/api/simulation/account');
|
||||
setAccount(mapAccountData(accountResponse.data));
|
||||
|
||||
// 获取持仓信息
|
||||
const positionsResponse = await apiRequest('/api/simulation/positions');
|
||||
setPositions(mapPositionData(positionsResponse.data || []));
|
||||
|
||||
// 获取交易历史
|
||||
const ordersResponse = await apiRequest('/api/simulation/orders?limit=100');
|
||||
setTradingHistory(mapOrderData(ordersResponse.data || []));
|
||||
|
||||
} catch (err) {
|
||||
console.error('刷新账户数据失败:', err);
|
||||
setError('加载账户数据失败: ' + err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// 买入股票
|
||||
const buyStock = useCallback(async (stockCode, quantity, orderType = 'MARKET', limitPrice = null) => {
|
||||
if (!account) {
|
||||
throw new Error('账户未初始化');
|
||||
}
|
||||
|
||||
// 调试模式:模拟买入成功
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:模拟买入', { stockCode, quantity, orderType });
|
||||
return { success: true, orderId: 'demo_' + Date.now() };
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const requestData = {
|
||||
stock_code: stockCode,
|
||||
order_type: 'BUY',
|
||||
order_qty: quantity,
|
||||
price_type: orderType
|
||||
};
|
||||
|
||||
const response = await apiRequest('/api/simulation/place-order', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
// 刷新账户数据
|
||||
await refreshAccount();
|
||||
|
||||
return { success: true, orderId: response.data.order_no };
|
||||
|
||||
} catch (err) {
|
||||
throw new Error('买入失败: ' + err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [account, user, refreshAccount]);
|
||||
|
||||
// 卖出股票
|
||||
const sellStock = useCallback(async (stockCode, quantity, orderType = 'MARKET', limitPrice = null) => {
|
||||
if (!account) {
|
||||
throw new Error('账户未初始化');
|
||||
}
|
||||
|
||||
// 调试模式:模拟卖出成功
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:模拟卖出', { stockCode, quantity, orderType });
|
||||
return { success: true, orderId: 'demo_' + Date.now() };
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const requestData = {
|
||||
stock_code: stockCode,
|
||||
order_type: 'SELL',
|
||||
order_qty: quantity,
|
||||
price_type: orderType
|
||||
};
|
||||
|
||||
const response = await apiRequest('/api/simulation/place-order', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
// 刷新账户数据
|
||||
await refreshAccount();
|
||||
|
||||
return { success: true, orderId: response.data.order_no };
|
||||
|
||||
} catch (err) {
|
||||
throw new Error('卖出失败: ' + err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [account, user, refreshAccount]);
|
||||
|
||||
// 撤销订单
|
||||
const cancelOrder = useCallback(async (orderId) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await apiRequest(`/api/simulation/cancel-order/${orderId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
// 刷新交易历史
|
||||
const ordersResponse = await apiRequest('/api/simulation/orders?limit=100');
|
||||
setTradingHistory(mapOrderData(ordersResponse.data || []));
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
throw new Error('撤单失败: ' + err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取交易记录
|
||||
const getTransactions = useCallback(async (options = {}) => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (options.limit) params.append('limit', options.limit);
|
||||
if (options.date) params.append('date', options.date);
|
||||
|
||||
const response = await apiRequest(`/api/simulation/transactions?${params.toString()}`);
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取交易记录失败:', error);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取资产历史
|
||||
const getAssetHistory = useCallback(async (days = 30) => {
|
||||
// 调试模式:demo用户返回模拟数据,避免CORS
|
||||
if (!user || user.id === 'demo') {
|
||||
const now = Date.now();
|
||||
const data = Array.from({ length: days }, (_, i) => {
|
||||
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
|
||||
// 简单生成一条平滑的收益曲线
|
||||
const value = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||||
return { date: date.toISOString().slice(0, 10), value };
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/simulation/statistics?days=${days}`);
|
||||
return response.data?.daily_returns || [];
|
||||
} catch (error) {
|
||||
console.error('获取资产历史失败:', error);
|
||||
return [];
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// 获取股票实时行情(如果需要的话)
|
||||
const getStockQuotes = useCallback(async (stockCodes) => {
|
||||
try {
|
||||
// 这里可以调用自选股实时行情接口
|
||||
const response = await apiRequest('/api/account/watchlist/quotes');
|
||||
if (response.success) {
|
||||
const quotes = {};
|
||||
response.data.forEach(item => {
|
||||
quotes[item.stock_code] = {
|
||||
name: item.stock_name,
|
||||
price: item.current_price,
|
||||
change: item.change,
|
||||
changePercent: item.change_percent
|
||||
};
|
||||
});
|
||||
setStockQuotes(quotes);
|
||||
return quotes;
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error('获取股票行情失败:', error);
|
||||
return {};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
account,
|
||||
positions,
|
||||
tradingHistory,
|
||||
isLoading,
|
||||
error,
|
||||
stockQuotes,
|
||||
buyStock,
|
||||
sellStock,
|
||||
cancelOrder,
|
||||
refreshAccount,
|
||||
searchStocks,
|
||||
getTransactions,
|
||||
getAssetHistory,
|
||||
getStockQuotes
|
||||
};
|
||||
}
|
||||
394
src/views/TradingSimulation/index.js
Normal file
394
src/views/TradingSimulation/index.js
Normal file
@@ -0,0 +1,394 @@
|
||||
// src/views/TradingSimulation/index.js - 模拟盘系统主页面
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Flex,
|
||||
Grid,
|
||||
GridItem,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Badge,
|
||||
VStack,
|
||||
HStack,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
StatArrow,
|
||||
useColorModeValue,
|
||||
Spinner,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
Link,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
// 导入子组件
|
||||
import AccountOverview from './components/AccountOverview';
|
||||
import TradingPanel from './components/TradingPanel';
|
||||
import PositionsList from './components/PositionsList';
|
||||
import TradingHistory from './components/TradingHistory';
|
||||
import MarginTrading from './components/MarginTrading';
|
||||
|
||||
// 导入现有的高质量组件
|
||||
import LineChart from '../../components/Charts/LineChart';
|
||||
|
||||
// 导入导航栏组件
|
||||
import HomeNavbar from '../../components/Navbars/HomeNavbar';
|
||||
|
||||
// 模拟盘账户管理 Hook
|
||||
import { useTradingAccount } from './hooks/useTradingAccount';
|
||||
|
||||
export default function TradingSimulation() {
|
||||
// ========== 1. 所有 Hooks 必须放在最顶部,不能有任何条件判断 ==========
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [assetHistory, setAssetHistory] = useState([]); // 移到这里!
|
||||
|
||||
// 使用模拟账户管理 Hook
|
||||
const {
|
||||
account,
|
||||
positions,
|
||||
tradingHistory,
|
||||
isLoading,
|
||||
error,
|
||||
buyStock,
|
||||
sellStock,
|
||||
cancelOrder,
|
||||
refreshAccount,
|
||||
searchStocks,
|
||||
getAssetHistory
|
||||
} = useTradingAccount();
|
||||
|
||||
// 所有的 useColorModeValue 也必须在顶部
|
||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
|
||||
|
||||
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
|
||||
useEffect(() => {
|
||||
// 调试模式:即使没有登录也加载模拟数据
|
||||
refreshAccount();
|
||||
}, [refreshAccount]);
|
||||
|
||||
// 调试:观察认证状态变化
|
||||
useEffect(() => {
|
||||
try {
|
||||
console.log('🔍 TradingSimulation mounted. isAuthenticated=', isAuthenticated, 'user=', user);
|
||||
} catch {}
|
||||
}, [isAuthenticated, user]);
|
||||
|
||||
// 获取资产历史数据的 useEffect
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
getAssetHistory(30).then(data => {
|
||||
setAssetHistory(data || []);
|
||||
}).catch(err => {
|
||||
console.error('获取资产历史失败:', err);
|
||||
setAssetHistory([]);
|
||||
});
|
||||
}
|
||||
}, [account, getAssetHistory]);
|
||||
|
||||
// ========== 3. 数据处理和计算(不是 Hooks,可以放在这里)==========
|
||||
// 准备资产走势图表数据(使用真实数据,安全处理)
|
||||
const hasAssetData = Array.isArray(assetHistory) && assetHistory.length > 0;
|
||||
|
||||
const assetTrendData = hasAssetData ? [{
|
||||
name: "总资产",
|
||||
data: assetHistory.map(item => {
|
||||
// 安全地获取数据,避免undefined错误
|
||||
if (!item) return 0;
|
||||
return item.closing_assets || item.total_assets || 0;
|
||||
})
|
||||
}] : [];
|
||||
|
||||
const assetTrendOptions = hasAssetData ? {
|
||||
chart: {
|
||||
toolbar: { show: false },
|
||||
height: 350
|
||||
},
|
||||
tooltip: {
|
||||
theme: "dark",
|
||||
y: {
|
||||
formatter: function(val) {
|
||||
return '¥' + (val || 0).toLocaleString()
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 3
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
categories: assetHistory.map(item => {
|
||||
// 安全地获取日期
|
||||
if (!item) return '';
|
||||
return item.date || '';
|
||||
}),
|
||||
labels: {
|
||||
style: {
|
||||
colors: xAxisLabelColor,
|
||||
fontSize: "12px",
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: yAxisLabelColor,
|
||||
fontSize: "12px",
|
||||
},
|
||||
formatter: function(val) {
|
||||
return '¥' + ((val || 0) / 10000).toFixed(1) + 'w'
|
||||
}
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 5,
|
||||
borderColor: gridBorderColor
|
||||
},
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
shade: "light",
|
||||
type: "vertical",
|
||||
shadeIntensity: 0.5,
|
||||
gradientToColors: undefined,
|
||||
inverseColors: true,
|
||||
opacityFrom: 0.8,
|
||||
opacityTo: 0,
|
||||
stops: [],
|
||||
},
|
||||
colors: [account?.totalProfit >= 0 ? "#48BB78" : "#F56565"],
|
||||
},
|
||||
colors: [account?.totalProfit >= 0 ? "#48BB78" : "#F56565"],
|
||||
} : {};
|
||||
|
||||
// ========== 4. 现在可以安全地进行条件返回了 ==========
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Container maxW="7xl" py={8}>
|
||||
<Flex justify="center" align="center" minH="400px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color="blue.500" />
|
||||
<Text>正在加载模拟盘数据...</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Container maxW="7xl" py={8}>
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertTitle>加载失败</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 5. 主要渲染逻辑 ==========
|
||||
return (
|
||||
<Box minH="100vh" bg={bgColor}>
|
||||
{/* 导航栏 */}
|
||||
<HomeNavbar />
|
||||
|
||||
<Container maxW="7xl" py={8}>
|
||||
{!isAuthenticated ? (
|
||||
<Alert status="warning">
|
||||
<AlertIcon />
|
||||
<AlertTitle>需要登录</AlertTitle>
|
||||
<AlertDescription>
|
||||
请先登录以访问模拟盘系统
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<VStack spacing={8} align="stretch">
|
||||
{/* 现代化页面标题 */}
|
||||
<Box textAlign="center" py={6}>
|
||||
<Heading
|
||||
size="2xl"
|
||||
bgGradient="linear(to-r, blue.400, purple.500)"
|
||||
bgClip="text"
|
||||
fontWeight="extrabold"
|
||||
mb={3}
|
||||
>
|
||||
智能模拟交易平台
|
||||
</Heading>
|
||||
<Text fontSize="xl" color="gray.600" maxW="2xl" mx="auto">
|
||||
体验真实交易环境,提升投资技能,零风险练手
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 主要功能区域 - 放在上面 */}
|
||||
<Tabs
|
||||
index={activeTab}
|
||||
onChange={setActiveTab}
|
||||
variant="soft-rounded"
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
>
|
||||
<TabList bg={cardBg} p={2} borderRadius="xl" shadow="sm">
|
||||
<Tab fontWeight="bold">💹 交易面板</Tab>
|
||||
<Tab fontWeight="bold">📊 我的持仓</Tab>
|
||||
<Tab fontWeight="bold">📋 交易历史</Tab>
|
||||
<Tab fontWeight="bold">💰 融资融券</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* 交易面板 */}
|
||||
<TabPanel px={0}>
|
||||
<TradingPanel
|
||||
account={account}
|
||||
onBuyStock={buyStock}
|
||||
onSellStock={sellStock}
|
||||
searchStocks={searchStocks}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* 我的持仓 */}
|
||||
<TabPanel px={0}>
|
||||
<PositionsList
|
||||
positions={positions}
|
||||
account={account}
|
||||
onSellStock={sellStock}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* 交易历史 */}
|
||||
<TabPanel px={0}>
|
||||
<TradingHistory
|
||||
history={tradingHistory}
|
||||
onCancelOrder={cancelOrder}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* 融资融券 */}
|
||||
<TabPanel px={0}>
|
||||
<MarginTrading
|
||||
account={account}
|
||||
onMarginBuy={buyStock}
|
||||
onShortSell={sellStock}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
{/* 统计数据区域 - 放在下面 */}
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 账户概览统计 */}
|
||||
<Box>
|
||||
<Heading size="lg" mb={4} color={useColorModeValue('gray.700', 'white')}>
|
||||
📊 账户统计分析
|
||||
</Heading>
|
||||
<AccountOverview account={account} />
|
||||
</Box>
|
||||
|
||||
{/* 资产走势图表 - 只在有数据时显示 */}
|
||||
{hasAssetData && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="lg" fontWeight="bold" color={useColorModeValue('gray.700', 'white')}>
|
||||
📈 资产走势分析
|
||||
</Text>
|
||||
<Badge colorScheme="purple" variant="outline">
|
||||
近{assetHistory.length}日
|
||||
</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="350px">
|
||||
<LineChart
|
||||
chartData={assetTrendData}
|
||||
chartOptions={assetTrendOptions}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 无数据提示 */}
|
||||
{!hasAssetData && account && (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<VStack spacing={4} py={8}>
|
||||
<Text fontSize="lg" color="gray.500">📊 暂无历史数据</Text>
|
||||
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||
开始交易后,这里将显示您的资产走势图表和详细统计分析
|
||||
</Text>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
{/* 风险提示 */}
|
||||
<Alert status="info" variant="left-accent">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>风险提示</AlertTitle>
|
||||
<AlertDescription>
|
||||
本系统为模拟交易,所有数据仅供学习和练习使用,不构成实际投资建议。
|
||||
模拟盘起始资金为100万元人民币。
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
</VStack>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{/* Footer区域 */}
|
||||
<Box bg={useColorModeValue('gray.100', 'gray.800')} py={6} mt={8}>
|
||||
<Container maxW="7xl">
|
||||
<VStack spacing={2}>
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
© 2024 价值前沿. 保留所有权利.
|
||||
</Text>
|
||||
<HStack spacing={4} fontSize="xs" color="gray.400">
|
||||
<Link
|
||||
href="https://beian.mps.gov.cn/#/query/webSearch?code=11010802046286"
|
||||
isExternal
|
||||
_hover={{ color: 'gray.600' }}
|
||||
>
|
||||
京公网安备11010802046286号
|
||||
</Link>
|
||||
<Text>京ICP备2025107343号-1</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user