个股论坛重做
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
-- 添加预测市场频道到社区
|
||||
-- 在 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';
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
Star,
|
||||
Settings,
|
||||
Users,
|
||||
Zap,
|
||||
Flame,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Channel, ChannelCategory, ChannelType } from '../../types';
|
||||
@@ -67,7 +67,7 @@ const channelIcons: Record<ChannelType, any> = {
|
||||
text: MessageSquare,
|
||||
forum: FileText,
|
||||
announcement: Megaphone,
|
||||
prediction: Zap,
|
||||
voice: Users,
|
||||
};
|
||||
|
||||
// 动画配置
|
||||
@@ -281,7 +281,7 @@ const ChannelSidebar: React.FC<ChannelSidebarProps> = ({
|
||||
|
||||
{/* 热门标记 */}
|
||||
{channel.isHot && (
|
||||
<Icon as={Zap} boxSize={4} color="orange.400" mr={1} />
|
||||
<Icon as={Flame} boxSize={4} color="orange.400" mr={1} />
|
||||
)}
|
||||
|
||||
{/* 概念频道股票数 */}
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
/**
|
||||
* 预测市场频道组件 - 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,7 +17,6 @@ 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 {
|
||||
@@ -154,13 +153,6 @@ const MessageArea: React.FC<MessageAreaProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
case 'prediction':
|
||||
return (
|
||||
<PredictionChannel
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'announcement':
|
||||
return (
|
||||
<TextChannel
|
||||
|
||||
474
src/views/StockCommunity/components/PredictionMarket/index.tsx
Normal file
474
src/views/StockCommunity/components/PredictionMarket/index.tsx
Normal file
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* 预测市场独立板块 - HeroUI 深色风格
|
||||
* Polymarket 风格的预测市场,作为独立 Tab 页面
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Button,
|
||||
Icon,
|
||||
IconButton,
|
||||
HStack,
|
||||
VStack,
|
||||
Select,
|
||||
Spinner,
|
||||
useDisclosure,
|
||||
Badge,
|
||||
Tooltip,
|
||||
SimpleGrid,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Heading,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Plus,
|
||||
TrendingUp,
|
||||
Filter,
|
||||
Zap,
|
||||
Trophy,
|
||||
HelpCircle,
|
||||
Coins,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Clock,
|
||||
Flame,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
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';
|
||||
|
||||
type SortOption = 'latest' | 'hot' | 'ending_soon' | 'highest_pool';
|
||||
type FilterOption = 'all' | 'active' | 'settled';
|
||||
|
||||
const PredictionMarket: React.FC = () => {
|
||||
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 [searchKeyword, setSearchKeyword] = useState('');
|
||||
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: 30,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
let items = response.data.items || response.data || [];
|
||||
|
||||
// 前端搜索过滤
|
||||
if (searchKeyword.trim()) {
|
||||
const keyword = searchKeyword.toLowerCase();
|
||||
items = items.filter((topic: any) =>
|
||||
topic.title?.toLowerCase().includes(keyword) ||
|
||||
topic.description?.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
setTopics(items);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载预测话题失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [sortBy, filterBy, searchKeyword]);
|
||||
|
||||
// 加载用户账户
|
||||
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 handleSearch = () => {
|
||||
loadTopics();
|
||||
};
|
||||
|
||||
// 创建成功回调
|
||||
const handleTopicCreated = (newTopic: any) => {
|
||||
setTopics(prev => [newTopic, ...prev]);
|
||||
onCreateClose();
|
||||
loadUserAccount(); // 刷新余额
|
||||
};
|
||||
|
||||
// 点击话题卡片
|
||||
const handleTopicClick = (topicId: string) => {
|
||||
navigate(`/value-forum/prediction/${topicId}`);
|
||||
};
|
||||
|
||||
// 排序选项配置
|
||||
const sortOptions = [
|
||||
{ value: 'latest', label: '最新发布', icon: Clock },
|
||||
{ value: 'hot', label: '最热门', icon: Flame },
|
||||
{ value: 'ending_soon', label: '即将截止', icon: Clock },
|
||||
{ value: 'highest_pool', label: '奖池最高', icon: Coins },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box h="full" overflow="auto" bg="transparent">
|
||||
{/* 顶部区域 */}
|
||||
<Box
|
||||
px={6}
|
||||
py={6}
|
||||
borderBottom="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
bg="rgba(17, 24, 39, 0.4)"
|
||||
backdropFilter={GLASS_BLUR.sm}
|
||||
>
|
||||
{/* 标题和操作栏 */}
|
||||
<Flex justify="space-between" align="center" mb={6}>
|
||||
<VStack align="start" spacing={1}>
|
||||
<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={6} color="yellow.400" />
|
||||
</Box>
|
||||
<Heading
|
||||
fontSize="2xl"
|
||||
fontWeight="bold"
|
||||
bgGradient="linear(to-r, yellow.300, orange.300)"
|
||||
bgClip="text"
|
||||
>
|
||||
预测市场
|
||||
</Heading>
|
||||
<Badge
|
||||
bg="linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(220, 38, 38, 0.3))"
|
||||
color="red.300"
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
borderRadius="full"
|
||||
>
|
||||
BETA
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
类似 Polymarket 的预测交易市场,用积分参与预测,赢取奖池
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<HStack spacing={3}>
|
||||
{/* 用户积分 */}
|
||||
{isAuthenticated && userAccount && (
|
||||
<Tooltip label="我的积分余额" placement="bottom">
|
||||
<HStack
|
||||
px={4}
|
||||
py={2}
|
||||
bg="rgba(251, 191, 36, 0.1)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(251, 191, 36, 0.3)"
|
||||
borderRadius="xl"
|
||||
spacing={2}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
bg: 'rgba(251, 191, 36, 0.15)',
|
||||
borderColor: 'rgba(251, 191, 36, 0.5)',
|
||||
}}
|
||||
onClick={() => navigate('/value-forum/my-points')}
|
||||
>
|
||||
<Icon as={Coins} boxSize={5} color="yellow.400" />
|
||||
<Text fontSize="md" fontWeight="bold" color="yellow.300">
|
||||
{userAccount.balance?.toLocaleString()}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 玩法说明 */}
|
||||
<Tooltip label="玩法说明" placement="bottom">
|
||||
<IconButton
|
||||
aria-label="玩法说明"
|
||||
icon={<HelpCircle className="w-5 h-5" />}
|
||||
variant="outline"
|
||||
borderColor="rgba(255, 255, 255, 0.2)"
|
||||
color="gray.300"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white', borderColor: 'yellow.400' }}
|
||||
onClick={onGuideOpen}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 排行榜 */}
|
||||
<Tooltip label="积分排行榜" placement="bottom">
|
||||
<IconButton
|
||||
aria-label="排行榜"
|
||||
icon={<Trophy className="w-5 h-5" />}
|
||||
variant="outline"
|
||||
borderColor="rgba(255, 255, 255, 0.2)"
|
||||
color="gray.300"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'yellow.400', borderColor: 'yellow.400' }}
|
||||
onClick={() => navigate('/value-forum/my-points')}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 发起预测按钮 */}
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
leftIcon={<Plus className="w-5 h-5" />}
|
||||
size="md"
|
||||
bg="linear-gradient(135deg, #F59E0B, #D97706)"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
_hover={{
|
||||
bg: 'linear-gradient(135deg, #D97706, #B45309)',
|
||||
boxShadow: '0 0 30px rgba(245, 158, 11, 0.4)',
|
||||
}}
|
||||
onClick={onCreateOpen}
|
||||
>
|
||||
发起预测
|
||||
</Button>
|
||||
</motion.div>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 搜索和筛选栏 */}
|
||||
<Flex gap={4} align="center" flexWrap="wrap">
|
||||
{/* 搜索框 */}
|
||||
<InputGroup maxW="400px" flex={1}>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<Search className="w-4 h-4" color="#6B7280" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="搜索预测话题..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
color="white"
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
_hover={{ borderColor: 'rgba(255, 255, 255, 0.2)' }}
|
||||
_focus={{
|
||||
borderColor: 'yellow.400',
|
||||
boxShadow: '0 0 0 1px var(--chakra-colors-yellow-400)',
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
{/* 状态筛选 */}
|
||||
<Select
|
||||
w="120px"
|
||||
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}>
|
||||
{sortOptions.map((option) => {
|
||||
const IconComp = option.icon;
|
||||
const isActive = sortBy === option.value;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
leftIcon={<IconComp className="w-4 h-4" />}
|
||||
size="sm"
|
||||
variant={isActive ? 'solid' : 'outline'}
|
||||
bg={isActive ? 'linear-gradient(135deg, #F59E0B, #D97706)' : 'transparent'}
|
||||
color={isActive ? 'white' : 'gray.400'}
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: isActive ? 'linear-gradient(135deg, #D97706, #B45309)' : 'whiteAlpha.100',
|
||||
borderColor: 'yellow.400',
|
||||
color: isActive ? 'white' : 'yellow.300',
|
||||
}}
|
||||
onClick={() => setSortBy(option.value as SortOption)}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<IconButton
|
||||
aria-label="刷新"
|
||||
icon={<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />}
|
||||
variant="ghost"
|
||||
color="gray.400"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={() => loadTopics(true)}
|
||||
isDisabled={refreshing}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 话题列表 */}
|
||||
<Box
|
||||
px={6}
|
||||
py={6}
|
||||
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="300px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color="yellow.400" thickness="4px" />
|
||||
<Text color="gray.500">加载预测话题中...</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={16}>
|
||||
<Box
|
||||
p={6}
|
||||
bg="linear-gradient(135deg, rgba(251, 191, 36, 0.15), rgba(245, 158, 11, 0.15))"
|
||||
borderRadius="full"
|
||||
mb={6}
|
||||
>
|
||||
<Icon as={TrendingUp} boxSize={12} color="yellow.400" />
|
||||
</Box>
|
||||
<Text fontSize="xl" color="gray.300" mb={2} fontWeight="medium">
|
||||
{searchKeyword ? '未找到相关预测话题' : '暂无预测话题'}
|
||||
</Text>
|
||||
<Text color="gray.500" mb={6}>
|
||||
{searchKeyword ? '换个关键词试试?' : '成为第一个发起预测的人!'}
|
||||
</Text>
|
||||
{!searchKeyword && (
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
size="lg"
|
||||
bg="linear-gradient(135deg, #F59E0B, #D97706)"
|
||||
color="white"
|
||||
leftIcon={<Zap className="w-5 h-5" />}
|
||||
fontWeight="bold"
|
||||
_hover={{
|
||||
bg: 'linear-gradient(135deg, #D97706, #B45309)',
|
||||
boxShadow: '0 0 30px rgba(245, 158, 11, 0.4)',
|
||||
}}
|
||||
onClick={onCreateOpen}
|
||||
>
|
||||
发起第一个预测
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</Flex>
|
||||
</motion.div>
|
||||
) : (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={5}>
|
||||
<AnimatePresence>
|
||||
{topics.map((topic, index) => (
|
||||
<motion.div
|
||||
key={topic.id}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -15 }}
|
||||
transition={{ delay: index * 0.03, duration: 0.3 }}
|
||||
>
|
||||
<PredictionTopicCardDark
|
||||
topic={topic}
|
||||
onClick={() => handleTopicClick(topic.id)}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 创建话题弹窗 */}
|
||||
<CreatePredictionModal
|
||||
isOpen={isCreateOpen}
|
||||
onClose={onCreateClose}
|
||||
onTopicCreated={handleTopicCreated}
|
||||
/>
|
||||
|
||||
{/* 玩法说明弹窗 */}
|
||||
<PredictionGuideModal
|
||||
isOpen={isGuideOpen}
|
||||
onClose={onGuideClose}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PredictionMarket;
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 股票社区主页面 - HeroUI 深色风格
|
||||
* Discord 风格三栏布局:频道列表 | 消息区域 | 右侧面板
|
||||
* 支持 Tab 切换:社区频道 / 预测市场
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
@@ -16,14 +17,21 @@ import {
|
||||
HStack,
|
||||
Icon,
|
||||
Tooltip,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Badge,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Menu, Users, Hash, Settings, Bell, Search } from 'lucide-react';
|
||||
import { Menu, Users, Hash, Settings, Bell, Search, MessageSquare, Zap } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import ChannelSidebar from './components/ChannelSidebar';
|
||||
import MessageArea from './components/MessageArea';
|
||||
import RightPanel from './components/RightPanel';
|
||||
import PredictionMarket from './components/PredictionMarket';
|
||||
import { useCommunitySocket } from './hooks/useCommunitySocket';
|
||||
import { Channel } from './types';
|
||||
import { GLASS_BLUR, GLASS_BG, GLASS_BORDER, GLASS_SHADOW } from '@/constants/glassConfig';
|
||||
@@ -39,6 +47,7 @@ const StockCommunity: React.FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [activeChannel, setActiveChannel] = useState<Channel | null>(null);
|
||||
const [rightPanelContent, setRightPanelContent] = useState<'members' | 'thread' | 'info'>('members');
|
||||
const [activeTab, setActiveTab] = useState(0); // 0: 社区频道, 1: 预测市场
|
||||
|
||||
// 移动端抽屉
|
||||
const { isOpen: isLeftOpen, onOpen: onLeftOpen, onClose: onLeftClose } = useDisclosure();
|
||||
@@ -98,82 +107,13 @@ const StockCommunity: React.FC = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
// 渲染社区频道视图(原有的 Discord 风格三栏布局)
|
||||
const renderCommunityView = () => (
|
||||
<Flex
|
||||
h="calc(100vh - 60px)"
|
||||
bg="gray.900"
|
||||
h="full"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
>
|
||||
{/* 背景装饰 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="-5%"
|
||||
w="400px"
|
||||
h="400px"
|
||||
bg="purple.500"
|
||||
filter="blur(150px)"
|
||||
opacity="0.08"
|
||||
borderRadius="full"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="-10%"
|
||||
right="-5%"
|
||||
w="350px"
|
||||
h="350px"
|
||||
bg="blue.500"
|
||||
filter="blur(150px)"
|
||||
opacity="0.06"
|
||||
borderRadius="full"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
{/* 移动端顶部操作栏 */}
|
||||
{isMobile && (
|
||||
<Box
|
||||
position="fixed"
|
||||
top="60px"
|
||||
left={0}
|
||||
right={0}
|
||||
h="56px"
|
||||
bg="rgba(17, 24, 39, 0.95)"
|
||||
backdropFilter={GLASS_BLUR.lg}
|
||||
borderBottom="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
px={4}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
zIndex={20}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="打开频道列表"
|
||||
icon={<Menu className="w-5 h-5" />}
|
||||
variant="ghost"
|
||||
color="gray.300"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={onLeftOpen}
|
||||
/>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Hash} boxSize={4} color="gray.400" />
|
||||
<Text fontWeight="semibold" color="white" fontSize="md">
|
||||
{activeChannel?.name || '股票社区'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<IconButton
|
||||
aria-label="打开详情面板"
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
variant="ghost"
|
||||
color="gray.300"
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={onRightOpen}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 左侧频道列表 - 桌面端 */}
|
||||
<AnimatePresence>
|
||||
{showLeftSidebar && (
|
||||
@@ -219,7 +159,6 @@ const StockCommunity: React.FC = () => {
|
||||
<Box
|
||||
flex={1}
|
||||
h="full"
|
||||
mt={isMobile ? '56px' : 0}
|
||||
overflow="hidden"
|
||||
bg="rgba(15, 23, 42, 0.6)"
|
||||
position="relative"
|
||||
@@ -270,6 +209,171 @@ const StockCommunity: React.FC = () => {
|
||||
</Drawer>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
h="calc(100vh - 60px)"
|
||||
bg="gray.900"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
>
|
||||
{/* 背景装饰 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="-5%"
|
||||
w="400px"
|
||||
h="400px"
|
||||
bg={activeTab === 0 ? 'purple.500' : 'yellow.500'}
|
||||
filter="blur(150px)"
|
||||
opacity="0.08"
|
||||
borderRadius="full"
|
||||
pointerEvents="none"
|
||||
transition="background 0.5s ease"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="-10%"
|
||||
right="-5%"
|
||||
w="350px"
|
||||
h="350px"
|
||||
bg={activeTab === 0 ? 'blue.500' : 'orange.500'}
|
||||
filter="blur(150px)"
|
||||
opacity="0.06"
|
||||
borderRadius="full"
|
||||
pointerEvents="none"
|
||||
transition="background 0.5s ease"
|
||||
/>
|
||||
|
||||
{/* Tabs 容器 */}
|
||||
<Tabs
|
||||
index={activeTab}
|
||||
onChange={(index) => setActiveTab(index)}
|
||||
isLazy
|
||||
h="full"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
{/* Tab 切换栏 */}
|
||||
<Box
|
||||
bg="rgba(17, 24, 39, 0.95)"
|
||||
backdropFilter={GLASS_BLUR.lg}
|
||||
borderBottom="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.08)"
|
||||
px={4}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
{/* 移动端菜单按钮 */}
|
||||
{isMobile && activeTab === 0 && (
|
||||
<IconButton
|
||||
aria-label="打开频道列表"
|
||||
icon={<Menu className="w-5 h-5" />}
|
||||
variant="ghost"
|
||||
color="gray.300"
|
||||
size="sm"
|
||||
mr={2}
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={onLeftOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TabList
|
||||
border="none"
|
||||
gap={2}
|
||||
py={2}
|
||||
flex={1}
|
||||
>
|
||||
<Tab
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="lg"
|
||||
fontWeight="semibold"
|
||||
fontSize="sm"
|
||||
color="gray.400"
|
||||
bg="transparent"
|
||||
border="none"
|
||||
_selected={{
|
||||
color: 'white',
|
||||
bg: 'linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(59, 130, 246, 0.3))',
|
||||
boxShadow: '0 0 20px rgba(139, 92, 246, 0.2)',
|
||||
}}
|
||||
_hover={{
|
||||
color: 'white',
|
||||
bg: 'whiteAlpha.100',
|
||||
}}
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<Text>社区频道</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
<Tab
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="lg"
|
||||
fontWeight="semibold"
|
||||
fontSize="sm"
|
||||
color="gray.400"
|
||||
bg="transparent"
|
||||
border="none"
|
||||
_selected={{
|
||||
color: 'white',
|
||||
bg: 'linear-gradient(135deg, rgba(251, 191, 36, 0.3), rgba(245, 158, 11, 0.3))',
|
||||
boxShadow: '0 0 20px rgba(251, 191, 36, 0.2)',
|
||||
}}
|
||||
_hover={{
|
||||
color: 'white',
|
||||
bg: 'whiteAlpha.100',
|
||||
}}
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Zap className="w-4 h-4" />
|
||||
<Text>预测市场</Text>
|
||||
<Badge
|
||||
bg="linear-gradient(135deg, rgba(239, 68, 68, 0.4), rgba(220, 38, 38, 0.4))"
|
||||
color="red.200"
|
||||
fontSize="2xs"
|
||||
px={1.5}
|
||||
borderRadius="full"
|
||||
>
|
||||
BETA
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
{/* 移动端右侧按钮 */}
|
||||
{isMobile && activeTab === 0 && (
|
||||
<IconButton
|
||||
aria-label="打开详情面板"
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
variant="ghost"
|
||||
color="gray.300"
|
||||
size="sm"
|
||||
ml={2}
|
||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||
onClick={onRightOpen}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* Tab 内容区域 */}
|
||||
<TabPanels flex={1} overflow="hidden">
|
||||
{/* 社区频道 */}
|
||||
<TabPanel p={0} h="full">
|
||||
{renderCommunityView()}
|
||||
</TabPanel>
|
||||
|
||||
{/* 预测市场 */}
|
||||
<TabPanel p={0} h="full">
|
||||
<PredictionMarket />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockCommunity;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// 频道相关
|
||||
// ============================================================
|
||||
|
||||
export type ChannelType = 'text' | 'forum' | 'announcement' | 'prediction';
|
||||
export type ChannelType = 'text' | 'forum' | 'announcement' | 'voice';
|
||||
|
||||
export interface ChannelCategory {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user