个股论坛重做
This commit is contained in:
16
add_prediction_channel.sql
Normal file
16
add_prediction_channel.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 添加预测市场频道到社区
|
||||
-- 在 MySQL 中执行
|
||||
|
||||
-- 首先检查是否已存在预测市场分类
|
||||
INSERT IGNORE INTO community_categories (id, name, icon, position, is_collapsible, is_system, created_at, updated_at)
|
||||
VALUES ('cat_prediction', '预测市场', '⚡', 2, 1, 1, NOW(), NOW());
|
||||
|
||||
-- 添加预测市场频道
|
||||
INSERT IGNORE INTO community_channels
|
||||
(id, category_id, name, type, topic, position, slow_mode, is_readonly, is_visible, is_system, subscriber_count, message_count, created_at, updated_at)
|
||||
VALUES
|
||||
('ch_prediction', 'cat_prediction', '预测大厅', 'prediction', 'Polymarket 风格预测市场,用积分参与预测,赢取奖池', 1, 0, 0, 1, 1, 0, 0, NOW(), NOW());
|
||||
|
||||
-- 查看结果
|
||||
SELECT * FROM community_categories WHERE id = 'cat_prediction';
|
||||
SELECT * FROM community_channels WHERE type = 'prediction';
|
||||
@@ -229,7 +229,7 @@ def create_channel():
|
||||
if len(name) > 50:
|
||||
return api_error('频道名称不能超过50个字符')
|
||||
|
||||
if channel_type not in ['text', 'forum', 'voice', 'announcement']:
|
||||
if channel_type not in ['text', 'forum', 'voice', 'announcement', 'prediction']:
|
||||
return api_error('无效的频道类型')
|
||||
|
||||
channel_id = f"ch_{generate_id()}"
|
||||
|
||||
@@ -24,7 +24,7 @@ const ModeToggleButtons = React.memo(({ mode, onModeChange }) => {
|
||||
colorScheme="blue"
|
||||
variant={mode === 'mainline' ? 'solid' : 'outline'}
|
||||
>
|
||||
主线
|
||||
题材
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
|
||||
@@ -67,6 +67,7 @@ const channelIcons: Record<ChannelType, any> = {
|
||||
text: MessageSquare,
|
||||
forum: FileText,
|
||||
announcement: Megaphone,
|
||||
prediction: Zap,
|
||||
};
|
||||
|
||||
// 动画配置
|
||||
@@ -607,6 +608,7 @@ const ChannelSidebar: React.FC<ChannelSidebarProps> = ({
|
||||
>
|
||||
<option value="text" style={{ background: '#1f2937' }}>💬 文字频道</option>
|
||||
<option value="forum" style={{ background: '#1f2937' }}>📝 论坛频道</option>
|
||||
<option value="prediction" style={{ background: '#1f2937' }}>⚡ 预测市场</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 预测话题卡片组件 - HeroUI 深色风格
|
||||
* 展示预测市场的话题概览
|
||||
*/
|
||||
import React 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,
|
||||
Clock,
|
||||
Coins,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
interface PredictionTopicCardDarkProps {
|
||||
topic: any;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const PredictionTopicCardDark: React.FC<PredictionTopicCardDarkProps> = ({ topic, onClick }) => {
|
||||
// 格式化数字
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(1)}万`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
||||
return num;
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
|
||||
const days = Math.floor(diff / 86400000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
|
||||
if (diff < 0) return '已截止';
|
||||
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 statusConfig: Record<string, { bg: string; color: string; label: string }> = {
|
||||
active: { bg: 'rgba(74, 222, 128, 0.2)', color: 'green.300', label: '交易中' },
|
||||
trading_closed: { bg: 'rgba(251, 191, 36, 0.2)', color: 'yellow.300', label: '已截止' },
|
||||
settled: { bg: 'rgba(148, 163, 184, 0.2)', color: 'gray.400', label: '已结算' },
|
||||
};
|
||||
|
||||
const status = statusConfig[topic.status] || statusConfig.active;
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
bg="rgba(255, 255, 255, 0.03)"
|
||||
borderRadius="xl"
|
||||
overflow="hidden"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
cursor="pointer"
|
||||
onClick={onClick}
|
||||
whileHover={{ y: -4, scale: 1.01 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
_hover={{
|
||||
borderColor: 'rgba(251, 191, 36, 0.4)',
|
||||
boxShadow: '0 8px 30px rgba(251, 191, 36, 0.15)',
|
||||
bg: 'rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
>
|
||||
{/* 头部:状态标识 */}
|
||||
<Box
|
||||
bg="linear-gradient(135deg, rgba(251, 191, 36, 0.1) 0%, rgba(245, 158, 11, 0.05) 100%)"
|
||||
px={4}
|
||||
py={2}
|
||||
borderBottom="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Zap} boxSize={4} color="yellow.400" />
|
||||
<Text fontSize="xs" fontWeight="bold" color="yellow.400">
|
||||
预测市场
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Badge
|
||||
bg={status.bg}
|
||||
color={status.color}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{status.label}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<VStack align="stretch" p={4} spacing={3}>
|
||||
{/* 话题标题 */}
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontWeight="700"
|
||||
color="white"
|
||||
noOfLines={2}
|
||||
lineHeight="1.4"
|
||||
>
|
||||
{topic.title}
|
||||
</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
{topic.description && (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="gray.400"
|
||||
noOfLines={2}
|
||||
lineHeight="1.5"
|
||||
>
|
||||
{topic.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 双向价格卡片 */}
|
||||
<HStack spacing={3} w="full">
|
||||
{/* Yes 方 */}
|
||||
<Box
|
||||
flex={1}
|
||||
bg="linear-gradient(135deg, rgba(74, 222, 128, 0.1) 0%, rgba(74, 222, 128, 0.03) 100%)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(74, 222, 128, 0.3)"
|
||||
borderRadius="lg"
|
||||
p={3}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 领主徽章 */}
|
||||
{yesData.lord_id && (
|
||||
<Icon
|
||||
as={Crown}
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
boxSize={4}
|
||||
color="yellow.400"
|
||||
/>
|
||||
)}
|
||||
|
||||
<VStack spacing={1} align="start">
|
||||
<HStack spacing={1}>
|
||||
<Icon as={TrendingUp} boxSize={3.5} color="green.400" />
|
||||
<Text fontSize="xs" fontWeight="600" color="green.400">
|
||||
看涨 / Yes
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="xl" fontWeight="bold" color="green.400">
|
||||
{Math.round(yesData.current_price)}
|
||||
<Text as="span" fontSize="xs" ml={1} color="green.500">
|
||||
积分
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{yesData.total_shares}份 · {yesPercent.toFixed(0)}%
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* No 方 */}
|
||||
<Box
|
||||
flex={1}
|
||||
bg="linear-gradient(135deg, rgba(248, 113, 113, 0.1) 0%, rgba(248, 113, 113, 0.03) 100%)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(248, 113, 113, 0.3)"
|
||||
borderRadius="lg"
|
||||
p={3}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 领主徽章 */}
|
||||
{noData.lord_id && (
|
||||
<Icon
|
||||
as={Crown}
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
boxSize={4}
|
||||
color="yellow.400"
|
||||
/>
|
||||
)}
|
||||
|
||||
<VStack spacing={1} align="start">
|
||||
<HStack spacing={1}>
|
||||
<Icon as={TrendingDown} boxSize={3.5} color="red.400" />
|
||||
<Text fontSize="xs" fontWeight="600" color="red.400">
|
||||
看跌 / No
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="xl" fontWeight="bold" color="red.400">
|
||||
{Math.round(noData.current_price)}
|
||||
<Text as="span" fontSize="xs" ml={1} color="red.500">
|
||||
积分
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{noData.total_shares}份 · {noPercent.toFixed(0)}%
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
{/* 市场情绪进度条 */}
|
||||
<Box>
|
||||
<Flex justify="space-between" mb={1}>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
市场情绪
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{yesPercent.toFixed(0)}% vs {noPercent.toFixed(0)}%
|
||||
</Text>
|
||||
</Flex>
|
||||
<Progress
|
||||
value={yesPercent}
|
||||
size="sm"
|
||||
borderRadius="full"
|
||||
bg="rgba(248, 113, 113, 0.3)"
|
||||
sx={{
|
||||
'& > div': {
|
||||
bg: 'linear-gradient(90deg, #4ADE80 0%, #22C55E 100%)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 奖池和数据 */}
|
||||
<HStack spacing={4} fontSize="sm" color="gray.500">
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Coins} boxSize={4} color="yellow.400" />
|
||||
<Text fontWeight="600" color="yellow.400">
|
||||
{formatNumber(topic.total_pool || 0)}
|
||||
</Text>
|
||||
<Text fontSize="xs">奖池</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Users} boxSize={4} />
|
||||
<Text>{topic.stats?.unique_traders?.size || topic.participants_count || 0}人</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Clock} boxSize={4} />
|
||||
<Text>{formatTime(topic.deadline)}</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 底部:作者信息 */}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
pt={2}
|
||||
borderTop="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Avatar
|
||||
size="xs"
|
||||
name={topic.author_name || topic.creator_name}
|
||||
src={topic.author_avatar}
|
||||
bg="linear-gradient(135deg, rgba(251, 191, 36, 0.6), rgba(245, 158, 11, 0.6))"
|
||||
/>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{topic.author_name || topic.creator_name}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 分类标签 */}
|
||||
{topic.category && (
|
||||
<Badge
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.400"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
>
|
||||
{topic.category}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</VStack>
|
||||
</MotionBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default PredictionTopicCardDark;
|
||||
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* 预测市场频道组件 - HeroUI 深色风格
|
||||
* Polymarket 风格的预测市场
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Button,
|
||||
Icon,
|
||||
IconButton,
|
||||
HStack,
|
||||
VStack,
|
||||
Select,
|
||||
Spinner,
|
||||
useDisclosure,
|
||||
Badge,
|
||||
Tooltip,
|
||||
SimpleGrid,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Plus,
|
||||
TrendingUp,
|
||||
Filter,
|
||||
Zap,
|
||||
Trophy,
|
||||
HelpCircle,
|
||||
Coins,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Channel } from '../../../types';
|
||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { getTopics, getUserAccount } from '@services/predictionMarketService.api';
|
||||
|
||||
// 导入原有组件
|
||||
import CreatePredictionModal from '@views/ValueForum/components/CreatePredictionModal';
|
||||
import PredictionGuideModal from '@views/ValueForum/components/PredictionGuideModal';
|
||||
import PredictionTopicCardDark from './PredictionTopicCardDark';
|
||||
|
||||
interface PredictionChannelProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
type SortOption = 'latest' | 'hot' | 'ending_soon' | 'highest_pool';
|
||||
type FilterOption = 'all' | 'active' | 'settled';
|
||||
|
||||
const PredictionChannel: React.FC<PredictionChannelProps> = ({ channel }) => {
|
||||
const navigate = useNavigate();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
|
||||
const [topics, setTopics] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<SortOption>('latest');
|
||||
const [filterBy, setFilterBy] = useState<FilterOption>('active');
|
||||
const [userAccount, setUserAccount] = useState<any>(null);
|
||||
|
||||
const {
|
||||
isOpen: isCreateOpen,
|
||||
onOpen: onCreateOpen,
|
||||
onClose: onCreateClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isGuideOpen,
|
||||
onOpen: onGuideOpen,
|
||||
onClose: onGuideClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// 加载话题列表
|
||||
const loadTopics = useCallback(async (showRefreshing = false) => {
|
||||
try {
|
||||
if (showRefreshing) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
const response = await getTopics({
|
||||
status: filterBy === 'all' ? undefined : filterBy,
|
||||
sort: sortBy,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setTopics(response.data.items || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载预测话题失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [sortBy, filterBy]);
|
||||
|
||||
// 加载用户账户
|
||||
const loadUserAccount = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
try {
|
||||
const response = await getUserAccount();
|
||||
if (response.success) {
|
||||
setUserAccount(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户账户失败:', error);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTopics();
|
||||
}, [loadTopics]);
|
||||
|
||||
useEffect(() => {
|
||||
loadUserAccount();
|
||||
}, [loadUserAccount]);
|
||||
|
||||
// 创建成功回调
|
||||
const handleTopicCreated = (newTopic: any) => {
|
||||
setTopics(prev => [newTopic, ...prev]);
|
||||
onCreateClose();
|
||||
loadUserAccount(); // 刷新余额
|
||||
};
|
||||
|
||||
// 点击话题卡片
|
||||
const handleTopicClick = (topicId: string) => {
|
||||
navigate(`/value-forum/prediction/${topicId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" h="full">
|
||||
{/* 频道头部 */}
|
||||
<Box
|
||||
px={4}
|
||||
py={3}
|
||||
borderBottom="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
bg="rgba(17, 24, 39, 0.6)"
|
||||
backdropFilter={GLASS_BLUR.sm}
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing={3}>
|
||||
<Box
|
||||
p={2}
|
||||
bg="linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(245, 158, 11, 0.2))"
|
||||
borderRadius="lg"
|
||||
>
|
||||
<Icon as={Zap} boxSize={5} color="yellow.400" />
|
||||
</Box>
|
||||
<Box>
|
||||
<HStack spacing={2}>
|
||||
<Text fontWeight="semibold" color="white" fontSize="md">
|
||||
{channel.name}
|
||||
</Text>
|
||||
<Badge
|
||||
bg="linear-gradient(135deg, rgba(251, 191, 36, 0.3), rgba(245, 158, 11, 0.3))"
|
||||
color="yellow.300"
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
borderRadius="full"
|
||||
>
|
||||
预测市场
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.500" mt={0.5}>
|
||||
类似 Polymarket 的预测交易市场
|
||||
</Text>
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={2}>
|
||||
{/* 用户积分 */}
|
||||
{isAuthenticated && userAccount && (
|
||||
<Tooltip label="我的积分" placement="bottom">
|
||||
<HStack
|
||||
px={3}
|
||||
py={1.5}
|
||||
bg="rgba(251, 191, 36, 0.1)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(251, 191, 36, 0.3)"
|
||||
borderRadius="lg"
|
||||
spacing={1}
|
||||
>
|
||||
<Icon as={Coins} boxSize={4} color="yellow.400" />
|
||||
<Text fontSize="sm" fontWeight="bold" color="yellow.300">
|
||||
{userAccount.balance?.toLocaleString()}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 玩法说明 */}
|
||||
<Tooltip label="玩法说明" placement="bottom">
|
||||
<IconButton
|
||||
aria-label="玩法说明"
|
||||
icon={<HelpCircle className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="gray.400"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={onGuideOpen}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 排行榜入口 */}
|
||||
<Tooltip label="积分排行榜" placement="bottom">
|
||||
<IconButton
|
||||
aria-label="排行榜"
|
||||
icon={<Trophy className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="gray.400"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'yellow.400' }}
|
||||
onClick={() => navigate('/value-forum/my-points')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
px={4}
|
||||
py={3}
|
||||
borderBottom="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
bg="rgba(17, 24, 39, 0.4)"
|
||||
>
|
||||
<HStack spacing={3}>
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
leftIcon={<Plus className="w-4 h-4" />}
|
||||
size="sm"
|
||||
bg="linear-gradient(135deg, #F59E0B, #D97706)"
|
||||
color="white"
|
||||
_hover={{
|
||||
bg: 'linear-gradient(135deg, #D97706, #B45309)',
|
||||
boxShadow: '0 0 20px rgba(245, 158, 11, 0.4)',
|
||||
}}
|
||||
onClick={onCreateOpen}
|
||||
>
|
||||
发起预测
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<IconButton
|
||||
aria-label="刷新"
|
||||
icon={<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="gray.400"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={() => loadTopics(true)}
|
||||
isDisabled={refreshing}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={3}>
|
||||
{/* 状态筛选 */}
|
||||
<Select
|
||||
size="sm"
|
||||
w="100px"
|
||||
value={filterBy}
|
||||
onChange={(e) => setFilterBy(e.target.value as FilterOption)}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
color="white"
|
||||
_focus={{
|
||||
borderColor: 'yellow.400',
|
||||
boxShadow: '0 0 0 1px var(--chakra-colors-yellow-400)',
|
||||
}}
|
||||
sx={{
|
||||
option: {
|
||||
bg: '#1f2937',
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<option value="all">全部</option>
|
||||
<option value="active">进行中</option>
|
||||
<option value="settled">已结算</option>
|
||||
</Select>
|
||||
|
||||
{/* 排序 */}
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Filter} boxSize={4} color="gray.500" />
|
||||
<Select
|
||||
size="sm"
|
||||
w="120px"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
color="white"
|
||||
_focus={{
|
||||
borderColor: 'yellow.400',
|
||||
boxShadow: '0 0 0 1px var(--chakra-colors-yellow-400)',
|
||||
}}
|
||||
sx={{
|
||||
option: {
|
||||
bg: '#1f2937',
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<option value="latest">最新发布</option>
|
||||
<option value="hot">最热门</option>
|
||||
<option value="ending_soon">即将截止</option>
|
||||
<option value="highest_pool">奖池最高</option>
|
||||
</Select>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 话题列表 */}
|
||||
<Box
|
||||
flex={1}
|
||||
overflowY="auto"
|
||||
px={4}
|
||||
py={4}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'rgba(255, 255, 255, 0.02)',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'rgba(245, 158, 11, 0.2)',
|
||||
borderRadius: '4px',
|
||||
'&:hover': {
|
||||
background: 'rgba(245, 158, 11, 0.4)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Flex justify="center" align="center" h="200px">
|
||||
<VStack spacing={3}>
|
||||
<Spinner size="lg" color="yellow.400" thickness="3px" />
|
||||
<Text color="gray.500" fontSize="sm">加载预测话题中...</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
) : topics.length === 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
py={12}
|
||||
>
|
||||
<Box
|
||||
p={4}
|
||||
bg="linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(245, 158, 11, 0.2))"
|
||||
borderRadius="full"
|
||||
mb={4}
|
||||
>
|
||||
<Icon as={TrendingUp} boxSize={8} color="yellow.400" />
|
||||
</Box>
|
||||
<Text color="gray.400" mb={2}>暂无预测话题</Text>
|
||||
<Text color="gray.500" fontSize="sm" mb={4}>
|
||||
成为第一个发起预测的人!
|
||||
</Text>
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
bg="linear-gradient(135deg, #F59E0B, #D97706)"
|
||||
color="white"
|
||||
leftIcon={<Plus className="w-4 h-4" />}
|
||||
_hover={{
|
||||
bg: 'linear-gradient(135deg, #D97706, #B45309)',
|
||||
boxShadow: '0 0 20px rgba(245, 158, 11, 0.4)',
|
||||
}}
|
||||
onClick={onCreateOpen}
|
||||
>
|
||||
发起第一个预测
|
||||
</Button>
|
||||
</motion.div>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
) : (
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={4}>
|
||||
<AnimatePresence>
|
||||
{topics.map((topic, index) => (
|
||||
<motion.div
|
||||
key={topic.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<PredictionTopicCardDark
|
||||
topic={topic}
|
||||
onClick={() => handleTopicClick(topic.id)}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 创建话题弹窗 */}
|
||||
<CreatePredictionModal
|
||||
isOpen={isCreateOpen}
|
||||
onClose={onCreateClose}
|
||||
onTopicCreated={handleTopicCreated}
|
||||
/>
|
||||
|
||||
{/* 玩法说明弹窗 */}
|
||||
<PredictionGuideModal
|
||||
isOpen={isGuideOpen}
|
||||
onClose={onGuideClose}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default PredictionChannel;
|
||||
@@ -17,6 +17,7 @@ import { MessageSquare, Hash, Users } from 'lucide-react';
|
||||
import { Channel } from '../../types';
|
||||
import TextChannel from './TextChannel';
|
||||
import ForumChannel from './ForumChannel';
|
||||
import PredictionChannel from './PredictionChannel';
|
||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||
|
||||
interface MessageAreaProps {
|
||||
@@ -153,6 +154,13 @@ const MessageArea: React.FC<MessageAreaProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
case 'prediction':
|
||||
return (
|
||||
<PredictionChannel
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'announcement':
|
||||
return (
|
||||
<TextChannel
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// 频道相关
|
||||
// ============================================================
|
||||
|
||||
export type ChannelType = 'text' | 'forum' | 'announcement';
|
||||
export type ChannelType = 'text' | 'forum' | 'announcement' | 'prediction';
|
||||
|
||||
export interface ChannelCategory {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user