Files
vf_react/src/views/ValueForum/components/PredictionTopicCard.js

344 lines
9.9 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.

/**
* 预测话题卡片组件
* 展示预测市场的话题概览
*/
import React, { useMemo, useState, useEffect } from 'react';
import {
Box,
Text,
HStack,
VStack,
Badge,
Progress,
Flex,
Avatar,
Icon,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import {
TrendingUp,
TrendingDown,
Crown,
Users,
Coins,
Zap,
Lock,
CheckCircle2,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { forumColors } from '@theme/forumTheme';
import CountdownTimer, { useCountdown } from './CountdownTimer';
const MotionBox = motion(Box);
const PredictionTopicCard = ({ topic }) => {
const navigate = useNavigate();
// 使用倒计时 Hook 获取实时状态
const timeLeft = useCountdown(topic.deadline);
// 处理卡片点击
const handleCardClick = () => {
navigate(`/value-forum/prediction/${topic.id}`);
};
// 格式化数字
const formatNumber = (num) => {
if (num >= 10000) return `${(num / 10000).toFixed(1)}`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num;
};
// 计算实际状态(基于截止时间自动判断)
const [effectiveStatus, setEffectiveStatus] = useState(topic.status);
useEffect(() => {
// 如果已经是结算状态,保持不变
if (topic.status === 'settled') {
setEffectiveStatus('settled');
return;
}
// 如果已过截止时间,自动变为已截止
if (timeLeft.expired && topic.status === 'active') {
setEffectiveStatus('trading_closed');
return;
}
setEffectiveStatus(topic.status);
}, [topic.status, timeLeft.expired]);
// 获取选项数据
const yesData = topic.positions?.yes || { total_shares: 0, current_price: 500, lord_id: null };
const noData = topic.positions?.no || { total_shares: 0, current_price: 500, lord_id: null };
// 计算总份额
const totalShares = yesData.total_shares + noData.total_shares;
// 计算百分比
const yesPercent = totalShares > 0 ? (yesData.total_shares / totalShares) * 100 : 50;
const noPercent = totalShares > 0 ? (noData.total_shares / totalShares) * 100 : 50;
// 状态颜色(使用 effectiveStatus
const statusColorMap = {
active: forumColors.success[500],
trading_closed: forumColors.warning[500],
settled: forumColors.text.secondary,
};
const statusLabelMap = {
active: '交易中',
trading_closed: '等待结算',
settled: '已结算',
};
const statusIconMap = {
active: Zap,
trading_closed: Lock,
settled: CheckCircle2,
};
return (
<MotionBox
bg={forumColors.background.card}
borderRadius="xl"
overflow="hidden"
border="2px solid"
borderColor={forumColors.border.default}
cursor="pointer"
onClick={handleCardClick}
whileHover={{ y: -8, scale: 1.02 }}
transition={{ duration: 0.3 }}
_hover={{
borderColor: forumColors.border.gold,
boxShadow: forumColors.shadows.gold,
}}
>
{/* 头部:状态标识 */}
<Box
bg={forumColors.gradients.goldSubtle}
px="4"
py="2"
borderBottom="1px solid"
borderColor={forumColors.border.default}
>
<Flex justify="space-between" align="center">
<HStack spacing="2">
<Icon as={statusIconMap[effectiveStatus]} boxSize="16px" color={statusColorMap[effectiveStatus]} />
<Text fontSize="xs" fontWeight="bold" color={statusColorMap[effectiveStatus]}>
{statusLabelMap[effectiveStatus]}
</Text>
</HStack>
<Badge
bg={statusColorMap[effectiveStatus]}
color="white"
px="3"
py="1"
borderRadius="full"
fontSize="xs"
fontWeight="bold"
>
{effectiveStatus === 'active' ? '可交易' : effectiveStatus === 'trading_closed' ? '待结算' : '已完成'}
</Badge>
</Flex>
</Box>
{/* 内容区域 */}
<VStack align="stretch" p="5" spacing="4">
{/* 话题标题 */}
<Text
fontSize="lg"
fontWeight="700"
color={forumColors.text.primary}
noOfLines={2}
lineHeight="1.4"
>
{topic.title}
</Text>
{/* 描述 */}
{topic.description && (
<Text
fontSize="sm"
color={forumColors.text.secondary}
noOfLines={2}
lineHeight="1.6"
>
{topic.description}
</Text>
)}
{/* 双向价格卡片 */}
<HStack spacing="3" w="full">
{/* Yes 方 */}
<Box
flex="1"
bg="linear-gradient(135deg, rgba(72, 187, 120, 0.1) 0%, rgba(72, 187, 120, 0.05) 100%)"
border="2px solid"
borderColor="green.400"
borderRadius="lg"
p="3"
position="relative"
overflow="hidden"
>
{/* 领主徽章 */}
{yesData.lord_id && (
<Icon
as={Crown}
position="absolute"
top="2"
right="2"
boxSize="16px"
color="yellow.400"
/>
)}
<VStack spacing="1" align="start">
<HStack spacing="1">
<Icon as={TrendingUp} boxSize="14px" color="green.500" />
<Text fontSize="xs" fontWeight="600" color="green.600">
看涨 / Yes
</Text>
</HStack>
<Text fontSize="2xl" fontWeight="bold" color="green.600">
{Math.round(yesData.current_price)}
<Text as="span" fontSize="xs" ml="1">
积分
</Text>
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
{yesData.total_shares} · {yesPercent.toFixed(0)}%
</Text>
</VStack>
</Box>
{/* No 方 */}
<Box
flex="1"
bg="linear-gradient(135deg, rgba(245, 101, 101, 0.1) 0%, rgba(245, 101, 101, 0.05) 100%)"
border="2px solid"
borderColor="red.400"
borderRadius="lg"
p="3"
position="relative"
overflow="hidden"
>
{/* 领主徽章 */}
{noData.lord_id && (
<Icon
as={Crown}
position="absolute"
top="2"
right="2"
boxSize="16px"
color="yellow.400"
/>
)}
<VStack spacing="1" align="start">
<HStack spacing="1">
<Icon as={TrendingDown} boxSize="14px" color="red.500" />
<Text fontSize="xs" fontWeight="600" color="red.600">
看跌 / No
</Text>
</HStack>
<Text fontSize="2xl" fontWeight="bold" color="red.600">
{Math.round(noData.current_price)}
<Text as="span" fontSize="xs" ml="1">
积分
</Text>
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
{noData.total_shares} · {noPercent.toFixed(0)}%
</Text>
</VStack>
</Box>
</HStack>
{/* 市场情绪进度条 */}
<Box>
<Flex justify="space-between" mb="1">
<Text fontSize="xs" color={forumColors.text.tertiary}>
市场情绪
</Text>
<Text fontSize="xs" color={forumColors.text.tertiary}>
{yesPercent.toFixed(0)}% vs {noPercent.toFixed(0)}%
</Text>
</Flex>
<Progress
value={yesPercent}
size="sm"
borderRadius="full"
bg="red.100"
sx={{
'& > div': {
bg: 'linear-gradient(90deg, #48BB78 0%, #38A169 100%)',
},
}}
/>
</Box>
{/* 奖池和数据 */}
<HStack spacing="4" fontSize="sm" color={forumColors.text.secondary} flexWrap="wrap">
<HStack spacing="1">
<Icon as={Coins} boxSize="16px" color={forumColors.primary[500]} />
<Text fontWeight="600" color={forumColors.primary[500]}>
{formatNumber(topic.total_pool)}
</Text>
<Text fontSize="xs">奖池</Text>
</HStack>
<HStack spacing="1">
<Icon as={Users} boxSize="16px" />
<Text>{topic.participants_count || topic.stats?.unique_traders?.size || 0}</Text>
</HStack>
{/* 使用新的倒计时组件 */}
<CountdownTimer
deadline={topic.deadline}
status={effectiveStatus}
size="sm"
showIcon={true}
/>
</HStack>
{/* 底部:作者信息 */}
<Flex justify="space-between" align="center" pt="2" borderTop="1px solid" borderColor={forumColors.border.default}>
<HStack spacing="2">
<Avatar
size="xs"
name={topic.author_name}
src={topic.author_avatar}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
/>
<Text fontSize="xs" color={forumColors.text.tertiary}>
{topic.author_name}
</Text>
</HStack>
{/* 分类标签 */}
{topic.category && (
<Badge
bg={forumColors.background.hover}
color={forumColors.text.primary}
px="2"
py="1"
borderRadius="md"
fontSize="xs"
>
{topic.category}
</Badge>
)}
</Flex>
</VStack>
</MotionBox>
);
};
export default PredictionTopicCard;