个股论坛重做
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:
|
if len(name) > 50:
|
||||||
return api_error('频道名称不能超过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('无效的频道类型')
|
return api_error('无效的频道类型')
|
||||||
|
|
||||||
channel_id = f"ch_{generate_id()}"
|
channel_id = f"ch_{generate_id()}"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const ModeToggleButtons = React.memo(({ mode, onModeChange }) => {
|
|||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
variant={mode === 'mainline' ? 'solid' : 'outline'}
|
variant={mode === 'mainline' ? 'solid' : 'outline'}
|
||||||
>
|
>
|
||||||
主线
|
题材
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const channelIcons: Record<ChannelType, any> = {
|
|||||||
text: MessageSquare,
|
text: MessageSquare,
|
||||||
forum: FileText,
|
forum: FileText,
|
||||||
announcement: Megaphone,
|
announcement: Megaphone,
|
||||||
|
prediction: Zap,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 动画配置
|
// 动画配置
|
||||||
@@ -607,6 +608,7 @@ const ChannelSidebar: React.FC<ChannelSidebarProps> = ({
|
|||||||
>
|
>
|
||||||
<option value="text" style={{ background: '#1f2937' }}>💬 文字频道</option>
|
<option value="text" style={{ background: '#1f2937' }}>💬 文字频道</option>
|
||||||
<option value="forum" style={{ background: '#1f2937' }}>📝 论坛频道</option>
|
<option value="forum" style={{ background: '#1f2937' }}>📝 论坛频道</option>
|
||||||
|
<option value="prediction" style={{ background: '#1f2937' }}>⚡ 预测市场</option>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</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 { Channel } from '../../types';
|
||||||
import TextChannel from './TextChannel';
|
import TextChannel from './TextChannel';
|
||||||
import ForumChannel from './ForumChannel';
|
import ForumChannel from './ForumChannel';
|
||||||
|
import PredictionChannel from './PredictionChannel';
|
||||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||||
|
|
||||||
interface MessageAreaProps {
|
interface MessageAreaProps {
|
||||||
@@ -153,6 +154,13 @@ const MessageArea: React.FC<MessageAreaProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'prediction':
|
||||||
|
return (
|
||||||
|
<PredictionChannel
|
||||||
|
channel={channel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case 'announcement':
|
case 'announcement':
|
||||||
return (
|
return (
|
||||||
<TextChannel
|
<TextChannel
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// 频道相关
|
// 频道相关
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
export type ChannelType = 'text' | 'forum' | 'announcement';
|
export type ChannelType = 'text' | 'forum' | 'announcement' | 'prediction';
|
||||||
|
|
||||||
export interface ChannelCategory {
|
export interface ChannelCategory {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user