update pay function
This commit is contained in:
532
src/views/ValueForum/PredictionTopicDetail.js
Normal file
532
src/views/ValueForum/PredictionTopicDetail.js
Normal file
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* 预测话题详情页
|
||||
* 展示预测市场的完整信息、交易、评论等
|
||||
*/
|
||||
|
||||
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 { getTopic } from '@services/predictionMarketService';
|
||||
import { getUserAccount } from '@services/creditSystemService';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
import TradeModal from './components/TradeModal';
|
||||
|
||||
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 {
|
||||
isOpen: isTradeModalOpen,
|
||||
onOpen: onTradeModalOpen,
|
||||
onClose: onTradeModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// 加载话题数据
|
||||
useEffect(() => {
|
||||
const loadTopic = () => {
|
||||
const topicData = getTopic(topicId);
|
||||
if (topicData) {
|
||||
setTopic(topicData);
|
||||
} else {
|
||||
toast({
|
||||
title: '话题不存在',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
navigate('/value-forum');
|
||||
}
|
||||
};
|
||||
|
||||
loadTopic();
|
||||
|
||||
if (user) {
|
||||
setUserAccount(getUserAccount(user.id));
|
||||
}
|
||||
}, [topicId, user]);
|
||||
|
||||
// 打开交易弹窗
|
||||
const handleOpenTrade = (mode) => {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTradeMode(mode);
|
||||
onTradeModalOpen();
|
||||
};
|
||||
|
||||
// 交易成功回调
|
||||
const handleTradeSuccess = () => {
|
||||
// 刷新话题数据
|
||||
setTopic(getTopic(topicId));
|
||||
setUserAccount(getUserAccount(user.id));
|
||||
};
|
||||
|
||||
if (!topic) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取选项数据
|
||||
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;
|
||||
|
||||
// 格式化时间
|
||||
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="80px" pb="20">
|
||||
<Container maxW="container.xl">
|
||||
{/* 头部:返回按钮 */}
|
||||
<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="6"
|
||||
py="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="2xl"
|
||||
fontWeight="bold"
|
||||
color={forumColors.text.primary}
|
||||
mt="4"
|
||||
>
|
||||
{topic.title}
|
||||
</Heading>
|
||||
|
||||
<Text fontSize="md" color={forumColors.text.secondary} mt="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="6">
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing="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="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="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="6" align="stretch" position="sticky" top="90px">
|
||||
{/* 奖池信息 */}
|
||||
<Box
|
||||
bg={forumColors.background.card}
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
p="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="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" />
|
||||
<Text fontWeight="600">{topic.stats.unique_traders.size}</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<HStack justify="space-between" fontSize="sm">
|
||||
<Text color={forumColors.text.secondary}>总交易量</Text>
|
||||
<Text fontWeight="600">{Math.round(topic.stats.total_volume)}</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"
|
||||
fontWeight="bold"
|
||||
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"
|
||||
fontWeight="bold"
|
||||
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>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
|
||||
{/* 交易模态框 */}
|
||||
<TradeModal
|
||||
isOpen={isTradeModalOpen}
|
||||
onClose={onTradeModalClose}
|
||||
topic={topic}
|
||||
mode={tradeMode}
|
||||
onTradeSuccess={handleTradeSuccess}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PredictionTopicDetail;
|
||||
386
src/views/ValueForum/components/CreatePredictionModal.js
Normal file
386
src/views/ValueForum/components/CreatePredictionModal.js
Normal file
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* 创建预测话题模态框
|
||||
* 用户可以发起新的预测市场话题
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
VStack,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Textarea,
|
||||
Select,
|
||||
HStack,
|
||||
Text,
|
||||
Box,
|
||||
Icon,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { Zap, Calendar, DollarSign } from 'lucide-react';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { createTopic } from '@services/predictionMarketService';
|
||||
import { getUserAccount, CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'stock',
|
||||
deadline_days: 7,
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 获取用户余额
|
||||
const userAccount = user ? getUserAccount(user.id) : null;
|
||||
|
||||
// 处理表单变化
|
||||
const handleChange = (field, value) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// 验证
|
||||
if (!formData.title.trim()) {
|
||||
toast({
|
||||
title: '请填写话题标题',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
toast({
|
||||
title: '请填写话题描述',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查余额
|
||||
if (userAccount.balance < CREDIT_CONFIG.CREATE_TOPIC_COST) {
|
||||
toast({
|
||||
title: '积分不足',
|
||||
description: `创建话题需要${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算截止时间
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + parseInt(formData.deadline_days));
|
||||
|
||||
const settlement_date = new Date(deadline);
|
||||
settlement_date.setDate(settlement_date.getDate() + 1);
|
||||
|
||||
// 创建话题
|
||||
const newTopic = createTopic({
|
||||
author_id: user.id,
|
||||
author_name: user.name || user.username,
|
||||
author_avatar: user.avatar,
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
category: formData.category,
|
||||
deadline: deadline.toISOString(),
|
||||
settlement_date: settlement_date.toISOString(),
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '创建成功!',
|
||||
description: `话题已发布,扣除${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'stock',
|
||||
deadline_days: 7,
|
||||
});
|
||||
|
||||
// 通知父组件
|
||||
if (onTopicCreated) {
|
||||
onTopicCreated(newTopic);
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('创建话题失败:', error);
|
||||
toast({
|
||||
title: '创建失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl" isCentered>
|
||||
<ModalOverlay backdropFilter="blur(4px)" />
|
||||
<ModalContent
|
||||
bg={forumColors.background.card}
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<ModalHeader
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
borderTopRadius="xl"
|
||||
borderBottom="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={Zap} boxSize="20px" color={forumColors.primary[500]} />
|
||||
<Text color={forumColors.text.primary}>发起预测话题</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color={forumColors.text.primary} />
|
||||
|
||||
<ModalBody py="6">
|
||||
<VStack spacing="5" align="stretch">
|
||||
{/* 提示信息 */}
|
||||
<Alert
|
||||
status="info"
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<AlertIcon color={forumColors.primary[500]} />
|
||||
<VStack align="start" spacing="1" flex="1">
|
||||
<Text fontSize="sm" color={forumColors.text.primary} fontWeight="600">
|
||||
创建预测话题
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
• 创建费用:{CREDIT_CONFIG.CREATE_TOPIC_COST}积分(进入奖池)
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
• 作者不能参与自己发起的话题
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
• 截止后由作者提交结果进行结算
|
||||
</Text>
|
||||
</VStack>
|
||||
</Alert>
|
||||
|
||||
{/* 话题标题 */}
|
||||
<FormControl isRequired>
|
||||
<FormLabel fontSize="sm" color={forumColors.text.primary}>
|
||||
话题标题
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="例如:贵州茅台下周会涨吗?"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_placeholder={{ color: forumColors.text.tertiary }}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 话题描述 */}
|
||||
<FormControl isRequired>
|
||||
<FormLabel fontSize="sm" color={forumColors.text.primary}>
|
||||
话题描述
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
placeholder="详细描述预测的内容、判断标准、数据来源等..."
|
||||
value={formData.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
rows={4}
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_placeholder={{ color: forumColors.text.tertiary }}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 分类 */}
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm" color={forumColors.text.primary}>
|
||||
分类
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={formData.category}
|
||||
onChange={(e) => handleChange('category', e.target.value)}
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
>
|
||||
<option value="stock">股票行情</option>
|
||||
<option value="index">指数走势</option>
|
||||
<option value="concept">概念板块</option>
|
||||
<option value="policy">政策影响</option>
|
||||
<option value="event">事件预测</option>
|
||||
<option value="other">其他</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 截止时间 */}
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm" color={forumColors.text.primary}>
|
||||
<HStack spacing="2">
|
||||
<Icon as={Calendar} boxSize="16px" />
|
||||
<Text>交易截止时间</Text>
|
||||
</HStack>
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={formData.deadline_days}
|
||||
onChange={(e) => handleChange('deadline_days', e.target.value)}
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
>
|
||||
<option value="1">1天后</option>
|
||||
<option value="3">3天后</option>
|
||||
<option value="7">7天后(推荐)</option>
|
||||
<option value="14">14天后</option>
|
||||
<option value="30">30天后</option>
|
||||
</Select>
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary} mt="2">
|
||||
截止后次日可提交结果进行结算
|
||||
</Text>
|
||||
</FormControl>
|
||||
|
||||
{/* 费用说明 */}
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing="1">
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
创建费用
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
将进入奖池,奖励给获胜者
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<HStack spacing="1">
|
||||
<Icon as={DollarSign} boxSize="20px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="2xl" fontWeight="bold" color={forumColors.primary[500]}>
|
||||
{CREDIT_CONFIG.CREATE_TOPIC_COST}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
积分
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Box mt="3" pt="3" borderTop="1px solid" borderColor={forumColors.border.default}>
|
||||
<HStack justify="space-between" fontSize="sm">
|
||||
<Text color={forumColors.text.secondary}>你的余额:</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{userAccount?.balance || 0} 积分
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between" fontSize="sm" mt="1">
|
||||
<Text color={forumColors.text.secondary}>创建后:</Text>
|
||||
<Text
|
||||
fontWeight="600"
|
||||
color={
|
||||
(userAccount?.balance || 0) >= CREDIT_CONFIG.CREATE_TOPIC_COST
|
||||
? forumColors.success[500]
|
||||
: forumColors.error[500]
|
||||
}
|
||||
>
|
||||
{(userAccount?.balance || 0) - CREDIT_CONFIG.CREATE_TOPIC_COST} 积分
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter borderTop="1px solid" borderColor={forumColors.border.default}>
|
||||
<HStack spacing="3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
color={forumColors.text.secondary}
|
||||
_hover={{ bg: forumColors.background.hover }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
fontWeight="bold"
|
||||
onClick={handleSubmit}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="创建中..."
|
||||
isDisabled={(userAccount?.balance || 0) < CREDIT_CONFIG.CREATE_TOPIC_COST}
|
||||
_hover={{
|
||||
opacity: 0.9,
|
||||
transform: 'translateY(-2px)',
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
发布话题
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePredictionModal;
|
||||
327
src/views/ValueForum/components/PredictionTopicCard.js
Normal file
327
src/views/ValueForum/components/PredictionTopicCard.js
Normal file
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 预测话题卡片组件
|
||||
* 展示预测市场的话题概览
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
HStack,
|
||||
VStack,
|
||||
Badge,
|
||||
Progress,
|
||||
Flex,
|
||||
Avatar,
|
||||
Icon,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Crown,
|
||||
Users,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const PredictionTopicCard = ({ topic }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 处理卡片点击
|
||||
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 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 '即将截止';
|
||||
};
|
||||
|
||||
// 获取选项数据
|
||||
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;
|
||||
|
||||
// 状态颜色
|
||||
const statusColorMap = {
|
||||
active: forumColors.success[500],
|
||||
trading_closed: forumColors.warning[500],
|
||||
settled: forumColors.text.secondary,
|
||||
};
|
||||
|
||||
const statusLabelMap = {
|
||||
active: '交易中',
|
||||
trading_closed: '已截止',
|
||||
settled: '已结算',
|
||||
};
|
||||
|
||||
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={Zap} boxSize="16px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="xs" fontWeight="bold" color={forumColors.primary[500]}>
|
||||
预测市场
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Badge
|
||||
bg={statusColorMap[topic.status]}
|
||||
color="white"
|
||||
px="3"
|
||||
py="1"
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{statusLabelMap[topic.status]}
|
||||
</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}>
|
||||
<HStack spacing="1">
|
||||
<Icon as={DollarSign} 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.stats?.unique_traders?.size || 0}人</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing="1">
|
||||
<Icon as={Clock} boxSize="16px" />
|
||||
<Text>{formatTime(topic.deadline)}</Text>
|
||||
</HStack>
|
||||
</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;
|
||||
494
src/views/ValueForum/components/TradeModal.js
Normal file
494
src/views/ValueForum/components/TradeModal.js
Normal file
@@ -0,0 +1,494 @@
|
||||
/**
|
||||
* 交易模态框组件
|
||||
* 用于买入/卖出预测市场席位
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Box,
|
||||
Icon,
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Stack,
|
||||
Flex,
|
||||
useToast,
|
||||
Badge,
|
||||
} from '@chakra-ui/react';
|
||||
import { TrendingUp, TrendingDown, DollarSign, AlertCircle, Zap } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import {
|
||||
buyPosition,
|
||||
sellPosition,
|
||||
calculateBuyCost,
|
||||
calculateSellRevenue,
|
||||
calculateTax,
|
||||
getTopic,
|
||||
} from '@services/predictionMarketService';
|
||||
import { getUserAccount, CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const TradeModal = ({ isOpen, onClose, topic, mode = 'buy', onTradeSuccess }) => {
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
// 状态
|
||||
const [selectedOption, setSelectedOption] = useState('yes');
|
||||
const [shares, setShares] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 获取用户账户
|
||||
const userAccount = user ? getUserAccount(user.id) : null;
|
||||
|
||||
// 重置状态
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedOption('yes');
|
||||
setShares(1);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!topic || !userAccount) return null;
|
||||
|
||||
// 获取市场数据
|
||||
const selectedSide = topic.positions[selectedOption];
|
||||
const otherOption = selectedOption === 'yes' ? 'no' : 'yes';
|
||||
const otherSide = topic.positions[otherOption];
|
||||
|
||||
// 计算交易数据
|
||||
let cost = 0;
|
||||
let tax = 0;
|
||||
let totalCost = 0;
|
||||
let avgPrice = 0;
|
||||
|
||||
if (mode === 'buy') {
|
||||
cost = calculateBuyCost(selectedSide.total_shares, otherSide.total_shares, shares);
|
||||
tax = calculateTax(cost);
|
||||
totalCost = cost + tax;
|
||||
avgPrice = cost / shares;
|
||||
} else {
|
||||
cost = calculateSellRevenue(selectedSide.total_shares, otherSide.total_shares, shares);
|
||||
tax = calculateTax(cost);
|
||||
totalCost = cost - tax;
|
||||
avgPrice = cost / shares;
|
||||
}
|
||||
|
||||
// 获取用户在该方向的持仓
|
||||
const userPosition = userAccount.active_positions?.find(
|
||||
(p) => p.topic_id === topic.id && p.option_id === selectedOption
|
||||
);
|
||||
|
||||
const maxShares = mode === 'buy' ? 10 : userPosition?.shares || 0;
|
||||
|
||||
// 检查是否可以交易
|
||||
const canTrade = () => {
|
||||
if (mode === 'buy') {
|
||||
// 检查余额
|
||||
if (userAccount.balance < totalCost) {
|
||||
return { ok: false, reason: '积分不足' };
|
||||
}
|
||||
|
||||
// 检查单次上限
|
||||
if (totalCost > CREDIT_CONFIG.MAX_SINGLE_BET) {
|
||||
return { ok: false, reason: `单次购买上限${CREDIT_CONFIG.MAX_SINGLE_BET}积分` };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
} else {
|
||||
// 检查持仓
|
||||
if (!userPosition || userPosition.shares < shares) {
|
||||
return { ok: false, reason: '持仓不足' };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
};
|
||||
|
||||
const tradeCheck = canTrade();
|
||||
|
||||
// 处理交易
|
||||
const handleTrade = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
let result;
|
||||
if (mode === 'buy') {
|
||||
result = buyPosition({
|
||||
user_id: user.id,
|
||||
user_name: user.name || user.username,
|
||||
user_avatar: user.avatar,
|
||||
topic_id: topic.id,
|
||||
option_id: selectedOption,
|
||||
shares,
|
||||
});
|
||||
} else {
|
||||
result = sellPosition({
|
||||
user_id: user.id,
|
||||
topic_id: topic.id,
|
||||
option_id: selectedOption,
|
||||
shares,
|
||||
});
|
||||
}
|
||||
|
||||
toast({
|
||||
title: mode === 'buy' ? '购买成功!' : '卖出成功!',
|
||||
description: mode === 'buy' ? `花费${totalCost}积分` : `获得${totalCost}积分`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
// 通知父组件刷新
|
||||
if (onTradeSuccess) {
|
||||
onTradeSuccess(result);
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('交易失败:', error);
|
||||
toast({
|
||||
title: '交易失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered>
|
||||
<ModalOverlay backdropFilter="blur(4px)" />
|
||||
<ModalContent
|
||||
bg={forumColors.background.card}
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<ModalHeader
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
borderTopRadius="xl"
|
||||
borderBottom="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon
|
||||
as={mode === 'buy' ? Zap : DollarSign}
|
||||
boxSize="20px"
|
||||
color={forumColors.primary[500]}
|
||||
/>
|
||||
<Text color={forumColors.text.primary}>
|
||||
{mode === 'buy' ? '购买席位' : '卖出席位'}
|
||||
</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color={forumColors.text.primary} />
|
||||
|
||||
<ModalBody py="6">
|
||||
<VStack spacing="5" align="stretch">
|
||||
{/* 话题标题 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p="3"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
{topic.title}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 选择方向 */}
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary} mb="3">
|
||||
选择方向
|
||||
</Text>
|
||||
<RadioGroup value={selectedOption} onChange={setSelectedOption}>
|
||||
<Stack spacing="3">
|
||||
{/* Yes 选项 */}
|
||||
<MotionBox
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Box
|
||||
bg={
|
||||
selectedOption === 'yes'
|
||||
? 'linear-gradient(135deg, rgba(72, 187, 120, 0.2) 0%, rgba(72, 187, 120, 0.1) 100%)'
|
||||
: forumColors.background.hover
|
||||
}
|
||||
border="2px solid"
|
||||
borderColor={selectedOption === 'yes' ? 'green.400' : forumColors.border.default}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
cursor="pointer"
|
||||
onClick={() => setSelectedOption('yes')}
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing="3">
|
||||
<Radio value="yes" colorScheme="green" />
|
||||
<VStack align="start" spacing="0">
|
||||
<HStack spacing="2">
|
||||
<Icon as={TrendingUp} boxSize="16px" color="green.500" />
|
||||
<Text fontWeight="600" color="green.600">
|
||||
看涨 / Yes
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
{topic.positions.yes.total_shares}份持仓
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
<VStack align="end" spacing="0">
|
||||
<Text fontSize="xl" fontWeight="bold" color="green.600">
|
||||
{Math.round(topic.positions.yes.current_price)}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
积分/份
|
||||
</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
</MotionBox>
|
||||
|
||||
{/* No 选项 */}
|
||||
<MotionBox
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Box
|
||||
bg={
|
||||
selectedOption === 'no'
|
||||
? 'linear-gradient(135deg, rgba(245, 101, 101, 0.2) 0%, rgba(245, 101, 101, 0.1) 100%)'
|
||||
: forumColors.background.hover
|
||||
}
|
||||
border="2px solid"
|
||||
borderColor={selectedOption === 'no' ? 'red.400' : forumColors.border.default}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
cursor="pointer"
|
||||
onClick={() => setSelectedOption('no')}
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing="3">
|
||||
<Radio value="no" colorScheme="red" />
|
||||
<VStack align="start" spacing="0">
|
||||
<HStack spacing="2">
|
||||
<Icon as={TrendingDown} boxSize="16px" color="red.500" />
|
||||
<Text fontWeight="600" color="red.600">
|
||||
看跌 / No
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
{topic.positions.no.total_shares}份持仓
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
<VStack align="end" spacing="0">
|
||||
<Text fontSize="xl" fontWeight="bold" color="red.600">
|
||||
{Math.round(topic.positions.no.current_price)}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
积分/份
|
||||
</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
</MotionBox>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
|
||||
{/* 购买份额 */}
|
||||
<Box>
|
||||
<Flex justify="space-between" mb="3">
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
{mode === 'buy' ? '购买份额' : '卖出份额'}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{shares} 份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Slider
|
||||
value={shares}
|
||||
onChange={setShares}
|
||||
min={1}
|
||||
max={maxShares}
|
||||
step={1}
|
||||
focusThumbOnChange={false}
|
||||
>
|
||||
<SliderTrack bg={forumColors.background.hover}>
|
||||
<SliderFilledTrack bg={forumColors.gradients.goldPrimary} />
|
||||
</SliderTrack>
|
||||
<SliderThumb boxSize="6" bg={forumColors.primary[500]}>
|
||||
<Box as={Icon} as={DollarSign} boxSize="12px" color="white" />
|
||||
</SliderThumb>
|
||||
</Slider>
|
||||
|
||||
<HStack justify="space-between" mt="2" fontSize="xs" color={forumColors.text.tertiary}>
|
||||
<Text>1份</Text>
|
||||
<Text>{maxShares}份 (最大)</Text>
|
||||
</HStack>
|
||||
|
||||
{mode === 'sell' && userPosition && (
|
||||
<Text fontSize="xs" color={forumColors.text.secondary} mt="2">
|
||||
你的持仓:{userPosition.shares}份 · 平均成本:{Math.round(userPosition.avg_cost)}积分/份
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 费用明细 */}
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Flex justify="space-between" fontSize="sm">
|
||||
<Text color={forumColors.text.secondary}>
|
||||
{mode === 'buy' ? '购买成本' : '卖出收益'}
|
||||
</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{Math.round(cost)} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" fontSize="sm">
|
||||
<Text color={forumColors.text.secondary}>平均价格</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{Math.round(avgPrice)} 积分/份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" fontSize="sm">
|
||||
<Text color={forumColors.text.secondary}>交易税 (2%)</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{Math.round(tax)} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt="2" mt="1">
|
||||
<Flex justify="space-between">
|
||||
<Text fontWeight="bold" color={forumColors.text.primary}>
|
||||
{mode === 'buy' ? '总计' : '净收益'}
|
||||
</Text>
|
||||
<HStack spacing="1">
|
||||
<Icon as={DollarSign} boxSize="20px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="2xl" fontWeight="bold" color={forumColors.primary[500]}>
|
||||
{Math.round(totalCost)}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
积分
|
||||
</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 余额提示 */}
|
||||
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt="2">
|
||||
<Flex justify="space-between" fontSize="sm">
|
||||
<Text color={forumColors.text.secondary}>你的余额:</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{userAccount.balance} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize="sm" mt="1">
|
||||
<Text color={forumColors.text.secondary}>
|
||||
{mode === 'buy' ? '交易后:' : '交易后:'}
|
||||
</Text>
|
||||
<Text
|
||||
fontWeight="600"
|
||||
color={
|
||||
mode === 'buy'
|
||||
? userAccount.balance >= totalCost
|
||||
? forumColors.success[500]
|
||||
: forumColors.error[500]
|
||||
: forumColors.success[500]
|
||||
}
|
||||
>
|
||||
{mode === 'buy'
|
||||
? userAccount.balance - totalCost
|
||||
: userAccount.balance + totalCost}{' '}
|
||||
积分
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 警告提示 */}
|
||||
{!tradeCheck.ok && (
|
||||
<Box
|
||||
bg="red.50"
|
||||
border="1px solid"
|
||||
borderColor="red.200"
|
||||
borderRadius="lg"
|
||||
p="3"
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={AlertCircle} boxSize="16px" color="red.500" />
|
||||
<Text fontSize="sm" color="red.600" fontWeight="600">
|
||||
{tradeCheck.reason}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter borderTop="1px solid" borderColor={forumColors.border.default}>
|
||||
<HStack spacing="3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
color={forumColors.text.secondary}
|
||||
_hover={{ bg: forumColors.background.hover }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
bg={mode === 'buy' ? forumColors.gradients.goldPrimary : 'red.500'}
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
onClick={handleTrade}
|
||||
isLoading={isSubmitting}
|
||||
loadingText={mode === 'buy' ? '购买中...' : '卖出中...'}
|
||||
isDisabled={!tradeCheck.ok}
|
||||
_hover={{
|
||||
opacity: 0.9,
|
||||
transform: 'translateY(-2px)',
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
{mode === 'buy' ? `购买 ${shares} 份` : `卖出 ${shares} 份`}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeModal;
|
||||
@@ -22,26 +22,38 @@ import {
|
||||
useDisclosure,
|
||||
Flex,
|
||||
Badge,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { Search, PenSquare, TrendingUp, Clock, Heart } from 'lucide-react';
|
||||
import { Search, PenSquare, TrendingUp, Clock, Heart, Zap } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { getPosts, searchPosts } from '@services/elasticsearchService';
|
||||
import { getTopics } from '@services/predictionMarketService';
|
||||
import PostCard from './components/PostCard';
|
||||
import PredictionTopicCard from './components/PredictionTopicCard';
|
||||
import CreatePostModal from './components/CreatePostModal';
|
||||
import CreatePredictionModal from './components/CreatePredictionModal';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const ValueForum = () => {
|
||||
const [posts, setPosts] = useState([]);
|
||||
const [predictionTopics, setPredictionTopics] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [sortBy, setSortBy] = useState('created_at');
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: isPostModalOpen, onOpen: onPostModalOpen, onClose: onPostModalClose } = useDisclosure();
|
||||
const { isOpen: isPredictionModalOpen, onOpen: onPredictionModalOpen, onClose: onPredictionModalClose } = useDisclosure();
|
||||
|
||||
// 获取帖子列表
|
||||
const fetchPosts = async (currentPage = 1, reset = false) => {
|
||||
@@ -78,10 +90,27 @@ const ValueForum = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取预测话题列表
|
||||
const fetchPredictionTopics = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const topics = getTopics({ status: 'active', sortBy });
|
||||
setPredictionTopics(topics);
|
||||
} catch (error) {
|
||||
console.error('获取预测话题失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
fetchPosts(1, true);
|
||||
}, [sortBy]);
|
||||
if (activeTab === 0) {
|
||||
fetchPosts(1, true);
|
||||
} else {
|
||||
fetchPredictionTopics();
|
||||
}
|
||||
}, [sortBy, activeTab]);
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
@@ -102,6 +131,11 @@ const ValueForum = () => {
|
||||
fetchPosts(1, true);
|
||||
};
|
||||
|
||||
// 预测话题创建成功回调
|
||||
const handlePredictionCreated = (newTopic) => {
|
||||
setPredictionTopics((prev) => [newTopic, ...prev]);
|
||||
};
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = [
|
||||
{ value: 'created_at', label: '最新发布', icon: Clock },
|
||||
@@ -143,21 +177,41 @@ const ValueForum = () => {
|
||||
</VStack>
|
||||
|
||||
{/* 发帖按钮 */}
|
||||
<Button
|
||||
leftIcon={<PenSquare size={18} />}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
size="lg"
|
||||
fontWeight="bold"
|
||||
onClick={onOpen}
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: forumColors.shadows.goldHover,
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
发布帖子
|
||||
</Button>
|
||||
<HStack spacing="3">
|
||||
<Button
|
||||
leftIcon={<PenSquare size={18} />}
|
||||
bg={forumColors.background.card}
|
||||
color={forumColors.text.primary}
|
||||
size="lg"
|
||||
fontWeight="bold"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
onClick={onPostModalOpen}
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
borderColor: forumColors.border.gold,
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
发布帖子
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
leftIcon={<Zap size={18} />}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
size="lg"
|
||||
fontWeight="bold"
|
||||
onClick={onPredictionModalOpen}
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: forumColors.shadows.goldHover,
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
发起预测
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 搜索和筛选栏 */}
|
||||
@@ -224,86 +278,190 @@ const ValueForum = () => {
|
||||
</VStack>
|
||||
</MotionBox>
|
||||
|
||||
{/* 帖子网格 */}
|
||||
{loading && page === 1 ? (
|
||||
<Center py="20">
|
||||
<VStack spacing="4">
|
||||
<Spinner
|
||||
size="xl"
|
||||
thickness="4px"
|
||||
speed="0.8s"
|
||||
color={forumColors.primary[500]}
|
||||
/>
|
||||
<Text color={forumColors.text.secondary}>加载中...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : posts.length === 0 ? (
|
||||
<Center py="20">
|
||||
<VStack spacing="4">
|
||||
<Text color={forumColors.text.secondary} fontSize="lg">
|
||||
{searchKeyword ? '未找到相关帖子' : '暂无帖子,快来发布第一篇吧!'}
|
||||
</Text>
|
||||
{!searchKeyword && (
|
||||
<Button
|
||||
leftIcon={<PenSquare size={18} />}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
onClick={onOpen}
|
||||
_hover={{ opacity: 0.9 }}
|
||||
{/* 标签页 */}
|
||||
<Tabs
|
||||
index={activeTab}
|
||||
onChange={setActiveTab}
|
||||
variant="soft-rounded"
|
||||
colorScheme="yellow"
|
||||
>
|
||||
<TabList mb="8" bg={forumColors.background.card} p="2" borderRadius="xl">
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={PenSquare} boxSize="16px" />
|
||||
<Text>社区帖子</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{
|
||||
bg: forumColors.gradients.goldPrimary,
|
||||
color: forumColors.background.main,
|
||||
}}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={Zap} boxSize="16px" />
|
||||
<Text>预测市场</Text>
|
||||
<Badge
|
||||
bg="red.500"
|
||||
color="white"
|
||||
borderRadius="full"
|
||||
px="2"
|
||||
fontSize="xs"
|
||||
>
|
||||
发布帖子
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="6">
|
||||
<AnimatePresence>
|
||||
{posts.map((post, index) => (
|
||||
<MotionBox
|
||||
key={post.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<PostCard post={post} />
|
||||
</MotionBox>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</SimpleGrid>
|
||||
NEW
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
{/* 加载更多按钮 */}
|
||||
{hasMore && (
|
||||
<Center mt="10">
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
isLoading={loading}
|
||||
loadingText="加载中..."
|
||||
bg={forumColors.background.card}
|
||||
color={forumColors.text.primary}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
_hover={{
|
||||
borderColor: forumColors.border.gold,
|
||||
bg: forumColors.background.hover,
|
||||
}}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
</Center>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TabPanels>
|
||||
{/* 普通帖子标签页 */}
|
||||
<TabPanel p="0">
|
||||
{loading && page === 1 ? (
|
||||
<Center py="20">
|
||||
<VStack spacing="4">
|
||||
<Spinner
|
||||
size="xl"
|
||||
thickness="4px"
|
||||
speed="0.8s"
|
||||
color={forumColors.primary[500]}
|
||||
/>
|
||||
<Text color={forumColors.text.secondary}>加载中...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : posts.length === 0 ? (
|
||||
<Center py="20">
|
||||
<VStack spacing="4">
|
||||
<Text color={forumColors.text.secondary} fontSize="lg">
|
||||
{searchKeyword ? '未找到相关帖子' : '暂无帖子,快来发布第一篇吧!'}
|
||||
</Text>
|
||||
{!searchKeyword && (
|
||||
<Button
|
||||
leftIcon={<PenSquare size={18} />}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
onClick={onPostModalOpen}
|
||||
_hover={{ opacity: 0.9 }}
|
||||
>
|
||||
发布帖子
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="6">
|
||||
<AnimatePresence>
|
||||
{posts.map((post, index) => (
|
||||
<MotionBox
|
||||
key={post.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<PostCard post={post} />
|
||||
</MotionBox>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 加载更多按钮 */}
|
||||
{hasMore && (
|
||||
<Center mt="10">
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
isLoading={loading}
|
||||
loadingText="加载中..."
|
||||
bg={forumColors.background.card}
|
||||
color={forumColors.text.primary}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
_hover={{
|
||||
borderColor: forumColors.border.gold,
|
||||
bg: forumColors.background.hover,
|
||||
}}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
</Center>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* 预测市场标签页 */}
|
||||
<TabPanel p="0">
|
||||
{loading ? (
|
||||
<Center py="20">
|
||||
<VStack spacing="4">
|
||||
<Spinner
|
||||
size="xl"
|
||||
thickness="4px"
|
||||
speed="0.8s"
|
||||
color={forumColors.primary[500]}
|
||||
/>
|
||||
<Text color={forumColors.text.secondary}>加载中...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : predictionTopics.length === 0 ? (
|
||||
<Center py="20">
|
||||
<VStack spacing="4">
|
||||
<Icon as={Zap} boxSize="48px" color={forumColors.text.tertiary} />
|
||||
<Text color={forumColors.text.secondary} fontSize="lg">
|
||||
暂无预测话题,快来发起第一个吧!
|
||||
</Text>
|
||||
<Button
|
||||
leftIcon={<Zap size={18} />}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
onClick={onPredictionModalOpen}
|
||||
_hover={{ opacity: 0.9 }}
|
||||
>
|
||||
发起预测
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing="6">
|
||||
<AnimatePresence>
|
||||
{predictionTopics.map((topic, index) => (
|
||||
<MotionBox
|
||||
key={topic.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<PredictionTopicCard topic={topic} />
|
||||
</MotionBox>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Container>
|
||||
|
||||
{/* 发帖模态框 */}
|
||||
<CreatePostModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isOpen={isPostModalOpen}
|
||||
onClose={onPostModalClose}
|
||||
onPostCreated={handlePostCreated}
|
||||
/>
|
||||
|
||||
{/* 发起预测模态框 */}
|
||||
<CreatePredictionModal
|
||||
isOpen={isPredictionModalOpen}
|
||||
onClose={onPredictionModalClose}
|
||||
onTopicCreated={handlePredictionCreated}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user