653 lines
22 KiB
JavaScript
653 lines
22 KiB
JavaScript
/**
|
||
* 预测话题详情页
|
||
* 展示预测市场的完整信息、交易、评论等
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
Container,
|
||
Heading,
|
||
Text,
|
||
Button,
|
||
HStack,
|
||
VStack,
|
||
Flex,
|
||
Badge,
|
||
Avatar,
|
||
Icon,
|
||
Progress,
|
||
Divider,
|
||
useDisclosure,
|
||
useToast,
|
||
SimpleGrid,
|
||
} from '@chakra-ui/react';
|
||
import {
|
||
TrendingUp,
|
||
TrendingDown,
|
||
Crown,
|
||
Users,
|
||
Clock,
|
||
DollarSign,
|
||
ShoppingCart,
|
||
ArrowLeftRight,
|
||
CheckCircle2,
|
||
} from 'lucide-react';
|
||
import { useParams, useNavigate } from 'react-router-dom';
|
||
import { motion } from 'framer-motion';
|
||
import { forumColors } from '@theme/forumTheme';
|
||
import { getTopicDetail, getUserAccount } from '@services/predictionMarketService.api';
|
||
import { useAuth } from '@contexts/AuthContext';
|
||
import TradeModal from './components/TradeModal';
|
||
import PredictionCommentSection from './components/PredictionCommentSection';
|
||
import CommentInvestModal from './components/CommentInvestModal';
|
||
|
||
const MotionBox = motion(Box);
|
||
|
||
const PredictionTopicDetail = () => {
|
||
const { topicId } = useParams();
|
||
const navigate = useNavigate();
|
||
const toast = useToast();
|
||
const { user } = useAuth();
|
||
|
||
// 状态
|
||
const [topic, setTopic] = useState(null);
|
||
const [userAccount, setUserAccount] = useState(null);
|
||
const [tradeMode, setTradeMode] = useState('buy');
|
||
const [selectedComment, setSelectedComment] = useState(null);
|
||
const [commentSectionKey, setCommentSectionKey] = useState(0);
|
||
|
||
// 模态框
|
||
const {
|
||
isOpen: isTradeModalOpen,
|
||
onOpen: onTradeModalOpen,
|
||
onClose: onTradeModalClose,
|
||
} = useDisclosure();
|
||
|
||
const {
|
||
isOpen: isInvestModalOpen,
|
||
onOpen: onInvestModalOpen,
|
||
onClose: onInvestModalClose,
|
||
} = useDisclosure();
|
||
|
||
// 加载话题数据
|
||
useEffect(() => {
|
||
const loadTopic = async () => {
|
||
try {
|
||
const response = await getTopicDetail(topicId);
|
||
if (response.success) {
|
||
setTopic(response.data);
|
||
} else {
|
||
toast({
|
||
title: '话题不存在',
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
navigate('/value-forum');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取话题详情失败:', error);
|
||
toast({
|
||
title: '加载失败',
|
||
description: error.message,
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
navigate('/value-forum');
|
||
}
|
||
};
|
||
|
||
const loadAccount = async () => {
|
||
if (!user) return;
|
||
try {
|
||
const response = await getUserAccount();
|
||
if (response.success) {
|
||
setUserAccount(response.data);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取账户失败:', error);
|
||
}
|
||
};
|
||
|
||
loadTopic();
|
||
loadAccount();
|
||
}, [topicId, user, toast, navigate]);
|
||
|
||
// 打开交易弹窗
|
||
const handleOpenTrade = (mode) => {
|
||
if (!user) {
|
||
toast({
|
||
title: '请先登录',
|
||
status: 'warning',
|
||
duration: 3000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
setTradeMode(mode);
|
||
onTradeModalOpen();
|
||
};
|
||
|
||
// 交易成功回调
|
||
const handleTradeSuccess = async () => {
|
||
// 刷新话题数据
|
||
try {
|
||
const topicResponse = await getTopicDetail(topicId);
|
||
if (topicResponse.success) {
|
||
setTopic(topicResponse.data);
|
||
}
|
||
|
||
const accountResponse = await getUserAccount();
|
||
if (accountResponse.success) {
|
||
setUserAccount(accountResponse.data);
|
||
}
|
||
} catch (error) {
|
||
console.error('刷新数据失败:', error);
|
||
}
|
||
};
|
||
|
||
// 打开投资弹窗
|
||
const handleOpenInvest = (comment) => {
|
||
if (!user) {
|
||
toast({
|
||
title: '请先登录',
|
||
status: 'warning',
|
||
duration: 3000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
setSelectedComment(comment);
|
||
onInvestModalOpen();
|
||
};
|
||
|
||
// 投资成功回调
|
||
const handleInvestSuccess = async () => {
|
||
// 刷新账户数据
|
||
try {
|
||
const accountResponse = await getUserAccount();
|
||
if (accountResponse.success) {
|
||
setUserAccount(accountResponse.data);
|
||
}
|
||
|
||
// 刷新评论区(通过更新key触发重新加载)
|
||
setCommentSectionKey((prev) => prev + 1);
|
||
|
||
toast({
|
||
title: '投资成功',
|
||
description: '评论列表已刷新',
|
||
status: 'success',
|
||
duration: 2000,
|
||
});
|
||
} catch (error) {
|
||
console.error('刷新数据失败:', error);
|
||
}
|
||
};
|
||
|
||
if (!topic) {
|
||
return null;
|
||
}
|
||
|
||
// 获取选项数据(从后端扁平结构映射到前端使用的嵌套结构)
|
||
const yesData = {
|
||
total_shares: topic.yes_total_shares || 0,
|
||
current_price: topic.yes_price || 500,
|
||
lord_id: topic.yes_lord_id || null,
|
||
};
|
||
const noData = {
|
||
total_shares: topic.no_total_shares || 0,
|
||
current_price: topic.no_price || 500,
|
||
lord_id: topic.no_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;
|
||
|
||
// 格式化时间
|
||
const formatTime = (dateString) => {
|
||
const date = new Date(dateString);
|
||
const now = new Date();
|
||
const diff = date - now;
|
||
|
||
const days = Math.floor(diff / 86400000);
|
||
const hours = Math.floor(diff / 3600000);
|
||
|
||
if (days > 0) return `${days}天后`;
|
||
if (hours > 0) return `${hours}小时后`;
|
||
return '即将截止';
|
||
};
|
||
|
||
return (
|
||
<Box minH="100vh" bg={forumColors.background.main} pt={{ base: "60px", md: "80px" }} pb={{ base: "6", md: "20" }}>
|
||
<Container maxW="container.xl" px={{ base: "3", sm: "4", md: "6" }}>
|
||
{/* 头部:返回按钮 */}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => navigate('/value-forum')}
|
||
mb="6"
|
||
color={forumColors.text.secondary}
|
||
_hover={{ bg: forumColors.background.hover }}
|
||
>
|
||
← 返回论坛
|
||
</Button>
|
||
|
||
<SimpleGrid columns={{ base: 1, lg: 3 }} spacing="6">
|
||
{/* 左侧:主要内容 */}
|
||
<Box gridColumn={{ base: '1', lg: '1 / 3' }}>
|
||
<VStack spacing="6" align="stretch">
|
||
{/* 话题信息卡片 */}
|
||
<MotionBox
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5 }}
|
||
bg={forumColors.background.card}
|
||
borderRadius="xl"
|
||
border="1px solid"
|
||
borderColor={forumColors.border.default}
|
||
overflow="hidden"
|
||
>
|
||
{/* 头部 */}
|
||
<Box
|
||
bg={forumColors.gradients.goldSubtle}
|
||
px={{ base: "4", md: "6" }}
|
||
py={{ base: "3", md: "4" }}
|
||
borderBottom="1px solid"
|
||
borderColor={forumColors.border.default}
|
||
>
|
||
<HStack justify="space-between">
|
||
<Badge
|
||
bg={forumColors.primary[500]}
|
||
color="white"
|
||
px="3"
|
||
py="1"
|
||
borderRadius="full"
|
||
fontSize="sm"
|
||
>
|
||
{topic.category}
|
||
</Badge>
|
||
|
||
<HStack spacing="3">
|
||
<Icon as={Clock} boxSize="16px" color={forumColors.text.secondary} />
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
{formatTime(topic.deadline)} 截止
|
||
</Text>
|
||
</HStack>
|
||
</HStack>
|
||
|
||
<Heading
|
||
as="h1"
|
||
fontSize={{ base: "lg", md: "2xl" }}
|
||
fontWeight="bold"
|
||
color={forumColors.text.primary}
|
||
mt={{ base: "3", md: "4" }}
|
||
lineHeight="1.4"
|
||
>
|
||
{topic.title}
|
||
</Heading>
|
||
|
||
<Text fontSize={{ base: "sm", md: "md" }} color={forumColors.text.secondary} mt={{ base: "2", md: "3" }}>
|
||
{topic.description}
|
||
</Text>
|
||
|
||
{/* 作者信息 */}
|
||
<HStack mt="4" spacing="3">
|
||
<Avatar
|
||
size="sm"
|
||
name={topic.author_name}
|
||
src={topic.author_avatar}
|
||
bg={forumColors.gradients.goldPrimary}
|
||
/>
|
||
<VStack align="start" spacing="0">
|
||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||
{topic.author_name}
|
||
</Text>
|
||
<Text fontSize="xs" color={forumColors.text.tertiary}>
|
||
发起者
|
||
</Text>
|
||
</VStack>
|
||
</HStack>
|
||
</Box>
|
||
|
||
{/* 市场数据 */}
|
||
<Box p={{ base: "4", md: "6" }}>
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: "4", md: "6" }}>
|
||
{/* Yes 方 */}
|
||
<Box
|
||
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="xl"
|
||
p={{ base: "4", md: "6" }}
|
||
position="relative"
|
||
>
|
||
{yesData.lord_id && (
|
||
<HStack
|
||
position="absolute"
|
||
top="3"
|
||
right="3"
|
||
spacing="1"
|
||
bg="yellow.400"
|
||
px="2"
|
||
py="1"
|
||
borderRadius="full"
|
||
>
|
||
<Icon as={Crown} boxSize="12px" color="white" />
|
||
<Text fontSize="xs" fontWeight="bold" color="white">
|
||
领主
|
||
</Text>
|
||
</HStack>
|
||
)}
|
||
|
||
<VStack align="start" spacing="4">
|
||
<HStack spacing="2">
|
||
<Icon as={TrendingUp} boxSize="20px" color="green.500" />
|
||
<Text fontSize="lg" fontWeight="700" color="green.600">
|
||
看涨 / Yes
|
||
</Text>
|
||
</HStack>
|
||
|
||
<VStack align="start" spacing="1">
|
||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||
当前价格
|
||
</Text>
|
||
<HStack spacing="1">
|
||
<Text fontSize="3xl" fontWeight="bold" color="green.600">
|
||
{Math.round(yesData.current_price)}
|
||
</Text>
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
积分/份
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
|
||
<Divider />
|
||
|
||
<HStack justify="space-between" w="full">
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
总份额
|
||
</Text>
|
||
<Text fontSize="md" fontWeight="600" color="green.600">
|
||
{yesData.total_shares}份
|
||
</Text>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between" w="full">
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
市场占比
|
||
</Text>
|
||
<Text fontSize="md" fontWeight="600" color="green.600">
|
||
{yesPercent.toFixed(1)}%
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
|
||
{/* No 方 */}
|
||
<Box
|
||
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="xl"
|
||
p={{ base: "4", md: "6" }}
|
||
position="relative"
|
||
>
|
||
{noData.lord_id && (
|
||
<HStack
|
||
position="absolute"
|
||
top="3"
|
||
right="3"
|
||
spacing="1"
|
||
bg="yellow.400"
|
||
px="2"
|
||
py="1"
|
||
borderRadius="full"
|
||
>
|
||
<Icon as={Crown} boxSize="12px" color="white" />
|
||
<Text fontSize="xs" fontWeight="bold" color="white">
|
||
领主
|
||
</Text>
|
||
</HStack>
|
||
)}
|
||
|
||
<VStack align="start" spacing="4">
|
||
<HStack spacing="2">
|
||
<Icon as={TrendingDown} boxSize="20px" color="red.500" />
|
||
<Text fontSize="lg" fontWeight="700" color="red.600">
|
||
看跌 / No
|
||
</Text>
|
||
</HStack>
|
||
|
||
<VStack align="start" spacing="1">
|
||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||
当前价格
|
||
</Text>
|
||
<HStack spacing="1">
|
||
<Text fontSize="3xl" fontWeight="bold" color="red.600">
|
||
{Math.round(noData.current_price)}
|
||
</Text>
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
积分/份
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
|
||
<Divider />
|
||
|
||
<HStack justify="space-between" w="full">
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
总份额
|
||
</Text>
|
||
<Text fontSize="md" fontWeight="600" color="red.600">
|
||
{noData.total_shares}份
|
||
</Text>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between" w="full">
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
市场占比
|
||
</Text>
|
||
<Text fontSize="md" fontWeight="600" color="red.600">
|
||
{noPercent.toFixed(1)}%
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
</SimpleGrid>
|
||
|
||
{/* 市场情绪进度条 */}
|
||
<Box mt="6">
|
||
<Flex justify="space-between" mb="2">
|
||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||
市场情绪分布
|
||
</Text>
|
||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||
{yesPercent.toFixed(1)}% vs {noPercent.toFixed(1)}%
|
||
</Text>
|
||
</Flex>
|
||
<Progress
|
||
value={yesPercent}
|
||
size="lg"
|
||
borderRadius="full"
|
||
bg="red.200"
|
||
sx={{
|
||
'& > div': {
|
||
bg: 'linear-gradient(90deg, #48BB78 0%, #38A169 100%)',
|
||
},
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
</MotionBox>
|
||
</VStack>
|
||
</Box>
|
||
|
||
{/* 右侧:操作面板 */}
|
||
<Box gridColumn={{ base: '1', lg: '3' }}>
|
||
<VStack spacing={{ base: "4", md: "6" }} align="stretch" position={{ base: "relative", lg: "sticky" }} top={{ base: "0", lg: "90px" }}>
|
||
{/* 奖池信息 */}
|
||
<Box
|
||
bg={forumColors.background.card}
|
||
borderRadius="xl"
|
||
border="1px solid"
|
||
borderColor={forumColors.border.gold}
|
||
p={{ base: "4", md: "6" }}
|
||
>
|
||
<VStack spacing="4" align="stretch">
|
||
<HStack justify="center" spacing="2">
|
||
<Icon as={DollarSign} boxSize="24px" color={forumColors.primary[500]} />
|
||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.secondary}>
|
||
当前奖池
|
||
</Text>
|
||
</HStack>
|
||
|
||
<Text
|
||
fontSize={{ base: "3xl", md: "4xl" }}
|
||
fontWeight="bold"
|
||
color={forumColors.primary[500]}
|
||
textAlign="center"
|
||
>
|
||
{topic.total_pool}
|
||
</Text>
|
||
|
||
<Text fontSize="sm" color={forumColors.text.secondary} textAlign="center">
|
||
积分
|
||
</Text>
|
||
|
||
<Divider />
|
||
|
||
<HStack justify="space-between" fontSize="sm">
|
||
<Text color={forumColors.text.secondary}>参与人数</Text>
|
||
<HStack spacing="1">
|
||
<Icon as={Users} boxSize="14px" color={forumColors.text.primary} />
|
||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||
{topic.participants_count || 0}
|
||
</Text>
|
||
</HStack>
|
||
</HStack>
|
||
|
||
<HStack justify="space-between" fontSize="sm">
|
||
<Text color={forumColors.text.secondary}>总交易量</Text>
|
||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||
{Math.round((topic.yes_total_shares || 0) + (topic.no_total_shares || 0))}
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
|
||
{/* 交易按钮 */}
|
||
{topic.status === 'active' && (
|
||
<VStack spacing="3">
|
||
<Button
|
||
leftIcon={<ShoppingCart size={18} />}
|
||
bg={forumColors.gradients.goldPrimary}
|
||
color={forumColors.background.main}
|
||
size="lg"
|
||
w="full"
|
||
h={{ base: "12", md: "auto" }}
|
||
fontWeight="bold"
|
||
fontSize={{ base: "md", md: "lg" }}
|
||
onClick={() => handleOpenTrade('buy')}
|
||
_hover={{
|
||
transform: 'translateY(-2px)',
|
||
boxShadow: forumColors.shadows.goldHover,
|
||
}}
|
||
_active={{ transform: 'translateY(0)' }}
|
||
>
|
||
购买席位
|
||
</Button>
|
||
|
||
<Button
|
||
leftIcon={<ArrowLeftRight size={18} />}
|
||
variant="outline"
|
||
borderColor={forumColors.border.default}
|
||
color={forumColors.text.primary}
|
||
size="lg"
|
||
w="full"
|
||
h={{ base: "12", md: "auto" }}
|
||
fontWeight="bold"
|
||
fontSize={{ base: "md", md: "lg" }}
|
||
onClick={() => handleOpenTrade('sell')}
|
||
_hover={{
|
||
bg: forumColors.background.hover,
|
||
borderColor: forumColors.border.gold,
|
||
}}
|
||
>
|
||
卖出席位
|
||
</Button>
|
||
</VStack>
|
||
)}
|
||
|
||
{/* 用户余额 */}
|
||
{user && userAccount && (
|
||
<Box
|
||
bg={forumColors.background.hover}
|
||
borderRadius="lg"
|
||
p="4"
|
||
border="1px solid"
|
||
borderColor={forumColors.border.default}
|
||
>
|
||
<VStack spacing="2" align="stretch" fontSize="sm">
|
||
<HStack justify="space-between">
|
||
<Text color={forumColors.text.secondary}>可用余额</Text>
|
||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||
{userAccount.balance} 积分
|
||
</Text>
|
||
</HStack>
|
||
<HStack justify="space-between">
|
||
<Text color={forumColors.text.secondary}>冻结积分</Text>
|
||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||
{userAccount.frozen} 积分
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
)}
|
||
</VStack>
|
||
</Box>
|
||
|
||
{/* 评论区 - 占据全宽 */}
|
||
<Box gridColumn={{ base: "1", lg: "1 / -1" }}>
|
||
<MotionBox
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5, delay: 0.3 }}
|
||
>
|
||
<PredictionCommentSection
|
||
key={commentSectionKey}
|
||
topicId={topicId}
|
||
topic={topic}
|
||
onInvest={handleOpenInvest}
|
||
/>
|
||
</MotionBox>
|
||
</Box>
|
||
</SimpleGrid>
|
||
</Container>
|
||
|
||
{/* 交易模态框 */}
|
||
<TradeModal
|
||
isOpen={isTradeModalOpen}
|
||
onClose={onTradeModalClose}
|
||
topic={topic}
|
||
mode={tradeMode}
|
||
onTradeSuccess={handleTradeSuccess}
|
||
/>
|
||
|
||
{/* 观点投资模态框 */}
|
||
<CommentInvestModal
|
||
isOpen={isInvestModalOpen}
|
||
onClose={onInvestModalClose}
|
||
comment={selectedComment}
|
||
topic={topic}
|
||
onInvestSuccess={handleInvestSuccess}
|
||
/>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default PredictionTopicDetail;
|