Files
vf_react/src/views/TradingSimulation/components/TradingHistory.js

397 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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';
import { logger } from '../../../utils/logger';
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);
logger.info('TradingHistory', '撤单成功', { orderId });
toast({
title: '撤单成功',
description: `订单 ${orderId} 已撤销`,
status: 'success',
duration: 3000,
isClosable: true,
});
} catch (error) {
logger.error('TradingHistory', 'handleCancelOrder', error, { orderId });
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>
);
}