From 526337847b550a8fe6010bcec8ba701e39dea6d2 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Tue, 6 Jan 2026 11:30:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AA=E8=82=A1=E8=AE=BA=E5=9D=9B=E9=87=8D?= =?UTF-8?q?=E5=81=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- add_prediction_channel.sql | 16 + community_api.py | 2 +- .../DynamicNews/ModeToggleButtons.js | 2 +- .../components/ChannelSidebar/index.tsx | 2 + .../PredictionTopicCardDark.tsx | 324 +++++++++++++ .../MessageArea/PredictionChannel/index.tsx | 433 ++++++++++++++++++ .../components/MessageArea/index.tsx | 8 + src/views/StockCommunity/types/index.ts | 2 +- 8 files changed, 786 insertions(+), 3 deletions(-) create mode 100644 add_prediction_channel.sql create mode 100644 src/views/StockCommunity/components/MessageArea/PredictionChannel/PredictionTopicCardDark.tsx create mode 100644 src/views/StockCommunity/components/MessageArea/PredictionChannel/index.tsx diff --git a/add_prediction_channel.sql b/add_prediction_channel.sql new file mode 100644 index 00000000..fe022f8f --- /dev/null +++ b/add_prediction_channel.sql @@ -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'; diff --git a/community_api.py b/community_api.py index da5641e1..2334cf2e 100644 --- a/community_api.py +++ b/community_api.py @@ -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()}" diff --git a/src/views/Community/components/DynamicNews/ModeToggleButtons.js b/src/views/Community/components/DynamicNews/ModeToggleButtons.js index f75dc910..552600ff 100644 --- a/src/views/Community/components/DynamicNews/ModeToggleButtons.js +++ b/src/views/Community/components/DynamicNews/ModeToggleButtons.js @@ -24,7 +24,7 @@ const ModeToggleButtons = React.memo(({ mode, onModeChange }) => { colorScheme="blue" variant={mode === 'mainline' ? 'solid' : 'outline'} > - 主线 + 题材 ); diff --git a/src/views/StockCommunity/components/ChannelSidebar/index.tsx b/src/views/StockCommunity/components/ChannelSidebar/index.tsx index 4bc07a62..164c3e91 100644 --- a/src/views/StockCommunity/components/ChannelSidebar/index.tsx +++ b/src/views/StockCommunity/components/ChannelSidebar/index.tsx @@ -67,6 +67,7 @@ const channelIcons: Record = { text: MessageSquare, forum: FileText, announcement: Megaphone, + prediction: Zap, }; // 动画配置 @@ -607,6 +608,7 @@ const ChannelSidebar: React.FC = ({ > + diff --git a/src/views/StockCommunity/components/MessageArea/PredictionChannel/PredictionTopicCardDark.tsx b/src/views/StockCommunity/components/MessageArea/PredictionChannel/PredictionTopicCardDark.tsx new file mode 100644 index 00000000..c77288c0 --- /dev/null +++ b/src/views/StockCommunity/components/MessageArea/PredictionChannel/PredictionTopicCardDark.tsx @@ -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 = ({ 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 = { + 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 ( + + {/* 头部:状态标识 */} + + + + + + 预测市场 + + + + + {status.label} + + + + + {/* 内容区域 */} + + {/* 话题标题 */} + + {topic.title} + + + {/* 描述 */} + {topic.description && ( + + {topic.description} + + )} + + {/* 双向价格卡片 */} + + {/* Yes 方 */} + + {/* 领主徽章 */} + {yesData.lord_id && ( + + )} + + + + + + 看涨 / Yes + + + + + {Math.round(yesData.current_price)} + + 积分 + + + + + {yesData.total_shares}份 · {yesPercent.toFixed(0)}% + + + + + {/* No 方 */} + + {/* 领主徽章 */} + {noData.lord_id && ( + + )} + + + + + + 看跌 / No + + + + + {Math.round(noData.current_price)} + + 积分 + + + + + {noData.total_shares}份 · {noPercent.toFixed(0)}% + + + + + + {/* 市场情绪进度条 */} + + + + 市场情绪 + + + {yesPercent.toFixed(0)}% vs {noPercent.toFixed(0)}% + + + div': { + bg: 'linear-gradient(90deg, #4ADE80 0%, #22C55E 100%)', + }, + }} + /> + + + {/* 奖池和数据 */} + + + + + {formatNumber(topic.total_pool || 0)} + + 奖池 + + + + + {topic.stats?.unique_traders?.size || topic.participants_count || 0}人 + + + + + {formatTime(topic.deadline)} + + + + {/* 底部:作者信息 */} + + + + + {topic.author_name || topic.creator_name} + + + + {/* 分类标签 */} + {topic.category && ( + + {topic.category} + + )} + + + + ); +}; + +export default PredictionTopicCardDark; diff --git a/src/views/StockCommunity/components/MessageArea/PredictionChannel/index.tsx b/src/views/StockCommunity/components/MessageArea/PredictionChannel/index.tsx new file mode 100644 index 00000000..b0194501 --- /dev/null +++ b/src/views/StockCommunity/components/MessageArea/PredictionChannel/index.tsx @@ -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 = ({ channel }) => { + const navigate = useNavigate(); + const { user, isAuthenticated } = useAuth(); + + const [topics, setTopics] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [sortBy, setSortBy] = useState('latest'); + const [filterBy, setFilterBy] = useState('active'); + const [userAccount, setUserAccount] = useState(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 ( + + {/* 频道头部 */} + + + + + + + + + + {channel.name} + + + 预测市场 + + + + 类似 Polymarket 的预测交易市场 + + + + + + {/* 用户积分 */} + {isAuthenticated && userAccount && ( + + + + + {userAccount.balance?.toLocaleString()} + + + + )} + + {/* 玩法说明 */} + + } + variant="ghost" + size="sm" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'white' }} + onClick={onGuideOpen} + /> + + + {/* 排行榜入口 */} + + } + variant="ghost" + size="sm" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'yellow.400' }} + onClick={() => navigate('/value-forum/my-points')} + /> + + + + + + {/* 工具栏 */} + + + + + + + {/* 刷新按钮 */} + } + variant="ghost" + size="sm" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'white' }} + onClick={() => loadTopics(true)} + isDisabled={refreshing} + /> + + + + {/* 状态筛选 */} + + + {/* 排序 */} + + + + + + + + {/* 话题列表 */} + + {loading ? ( + + + + 加载预测话题中... + + + ) : topics.length === 0 ? ( + + + + + + 暂无预测话题 + + 成为第一个发起预测的人! + + + + + + + ) : ( + + + {topics.map((topic, index) => ( + + handleTopicClick(topic.id)} + /> + + ))} + + + )} + + + {/* 创建话题弹窗 */} + + + {/* 玩法说明弹窗 */} + + + ); +}; + +export default PredictionChannel; diff --git a/src/views/StockCommunity/components/MessageArea/index.tsx b/src/views/StockCommunity/components/MessageArea/index.tsx index c2bc5c2f..0a890905 100644 --- a/src/views/StockCommunity/components/MessageArea/index.tsx +++ b/src/views/StockCommunity/components/MessageArea/index.tsx @@ -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 = ({ /> ); + case 'prediction': + return ( + + ); + case 'announcement': return (