Initial commit
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user