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