Files
vf_react/src/views/ValueForum/PredictionTopicDetail.js
2025-11-24 08:05:19 +08:00

653 lines
22 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, { 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;