Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View 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>
);
}