885 lines
36 KiB
JavaScript
885 lines
36 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { useParams, useLocation } from 'react-router-dom';
|
||
import {
|
||
Box,
|
||
Container,
|
||
VStack,
|
||
HStack,
|
||
Spinner,
|
||
Alert,
|
||
AlertIcon,
|
||
AlertTitle,
|
||
AlertDescription,
|
||
Flex,
|
||
useColorModeValue,
|
||
Grid,
|
||
GridItem,
|
||
Icon,
|
||
Text,
|
||
Badge,
|
||
Divider,
|
||
useDisclosure,
|
||
Button,
|
||
Heading,
|
||
Stat,
|
||
StatLabel,
|
||
StatNumber,
|
||
StatHelpText,
|
||
SimpleGrid,
|
||
Tabs,
|
||
TabList,
|
||
TabPanels,
|
||
Tab,
|
||
TabPanel,
|
||
Textarea,
|
||
Avatar,
|
||
IconButton,
|
||
Input,
|
||
Collapse,
|
||
Center,
|
||
useToast,
|
||
Skeleton,
|
||
} from '@chakra-ui/react';
|
||
import { FiLock } from 'react-icons/fi';
|
||
import {
|
||
FiTrendingUp,
|
||
FiActivity,
|
||
FiMessageSquare,
|
||
FiClock,
|
||
FiBarChart2,
|
||
FiLink,
|
||
FiZap,
|
||
FiGlobe,
|
||
FiHeart,
|
||
FiTrash2,
|
||
FiChevronDown,
|
||
FiChevronUp,
|
||
} from 'react-icons/fi';
|
||
import { FaHeart, FaRegHeart, FaComment } from 'react-icons/fa';
|
||
import { format } from 'date-fns';
|
||
import { zhCN } from 'date-fns/locale';
|
||
|
||
// 导入新建的业务组件
|
||
import EventHeader from './components/EventHeader';
|
||
import RelatedConcepts from './components/RelatedConcepts';
|
||
import HistoricalEvents from './components/HistoricalEvents';
|
||
import RelatedStocks from './components/RelatedStocks';
|
||
// Navigation bar now provided by MainLayout
|
||
// import HomeNavbar from '../../components/Navbars/HomeNavbar';
|
||
import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal';
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
import { useSubscription } from '../../hooks/useSubscription';
|
||
import TransmissionChainAnalysis from './components/TransmissionChainAnalysis';
|
||
|
||
// 导入你的 Flask API 服务
|
||
import { eventService } from '../../services/eventService';
|
||
import { debugEventService } from '../../utils/debugEventService';
|
||
import { logger } from '../../utils/logger';
|
||
import { useEventDetailEvents } from './hooks/useEventDetailEvents';
|
||
|
||
// 临时调试代码 - 生产环境测试后请删除
|
||
if (typeof window !== 'undefined') {
|
||
logger.debug('EventDetail', '调试 eventService');
|
||
debugEventService();
|
||
}
|
||
|
||
// 统计卡片组件 - 更简洁的设计
|
||
const StatCard = ({ icon, label, value, color }) => {
|
||
const bg = useColorModeValue('white', 'gray.800');
|
||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||
const iconColor = useColorModeValue(`${color}.500`, `${color}.300`);
|
||
|
||
return (
|
||
<Stat
|
||
p={6}
|
||
bg={bg}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={borderColor}
|
||
_hover={{ shadow: 'md' }}
|
||
transition="all 0.2s"
|
||
>
|
||
<HStack spacing={3} align="flex-start">
|
||
<Icon as={icon} boxSize={5} color={iconColor} mt={1} />
|
||
<Box flex={1}>
|
||
<StatLabel color="gray.500" fontSize="sm">{label}</StatLabel>
|
||
<StatNumber fontSize="2xl" color={iconColor}>{value}</StatNumber>
|
||
</Box>
|
||
</HStack>
|
||
</Stat>
|
||
);
|
||
};
|
||
|
||
// 帖子组件
|
||
const PostItem = ({ post, onRefresh, eventEvents }) => {
|
||
const [showComments, setShowComments] = useState(false);
|
||
const [comments, setComments] = useState([]);
|
||
const [newComment, setNewComment] = useState('');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [liked, setLiked] = useState(post.liked || false);
|
||
const [likesCount, setLikesCount] = useState(post.likes_count || 0);
|
||
const toast = useToast();
|
||
const { user } = useAuth();
|
||
const bg = useColorModeValue('white', 'gray.800');
|
||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||
|
||
const loadComments = async () => {
|
||
if (!showComments) {
|
||
setShowComments(true);
|
||
setIsLoading(true);
|
||
try {
|
||
const result = await eventService.getPostComments(post.id);
|
||
if (result.success) {
|
||
setComments(result.data);
|
||
}
|
||
} catch (error) {
|
||
logger.error('PostItem', 'loadComments', error, { postId: post.id });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
} else {
|
||
setShowComments(false);
|
||
}
|
||
};
|
||
|
||
const handleLike = async () => {
|
||
try {
|
||
const result = await eventService.likePost(post.id);
|
||
if (result.success) {
|
||
const newLikedState = result.liked;
|
||
setLiked(newLikedState);
|
||
setLikesCount(result.likes_count);
|
||
|
||
// 🎯 追踪评论点赞
|
||
if (eventEvents && eventEvents.trackCommentLiked) {
|
||
eventEvents.trackCommentLiked(post.id, newLikedState);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: '操作失败',
|
||
status: 'error',
|
||
duration: 2000,
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleAddComment = async () => {
|
||
if (!newComment.trim()) return;
|
||
|
||
try {
|
||
const result = await eventService.addPostComment(post.id, {
|
||
content: newComment,
|
||
});
|
||
|
||
if (result.success) {
|
||
// 🎯 追踪添加评论
|
||
if (eventEvents && eventEvents.trackCommentAdded) {
|
||
eventEvents.trackCommentAdded(
|
||
result.data?.id || post.id,
|
||
newComment.length
|
||
);
|
||
}
|
||
|
||
toast({
|
||
title: '评论发表成功',
|
||
status: 'success',
|
||
duration: 2000,
|
||
});
|
||
setNewComment('');
|
||
// 重新加载评论
|
||
const commentsResult = await eventService.getPostComments(post.id);
|
||
if (commentsResult.success) {
|
||
setComments(commentsResult.data);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: '评论失败',
|
||
status: 'error',
|
||
duration: 2000,
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleDelete = async () => {
|
||
if (window.confirm('确定要删除这个帖子吗?')) {
|
||
try {
|
||
const result = await eventService.deletePost(post.id);
|
||
if (result.success) {
|
||
// 🎯 追踪删除评论
|
||
if (eventEvents && eventEvents.trackCommentDeleted) {
|
||
eventEvents.trackCommentDeleted(post.id);
|
||
}
|
||
|
||
toast({
|
||
title: '删除成功',
|
||
status: 'success',
|
||
duration: 2000,
|
||
});
|
||
onRefresh();
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: '删除失败',
|
||
status: 'error',
|
||
duration: 2000,
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Box
|
||
bg={bg}
|
||
borderWidth="1px"
|
||
borderColor={borderColor}
|
||
borderRadius="lg"
|
||
p={6}
|
||
mb={4}
|
||
>
|
||
{/* 帖子头部 */}
|
||
<HStack justify="space-between" mb={4}>
|
||
<HStack spacing={3}>
|
||
<Avatar
|
||
size="sm"
|
||
name={post.user?.username}
|
||
src={post.user?.avatar_url}
|
||
/>
|
||
<VStack align="start" spacing={0}>
|
||
<Text fontWeight="medium">{post.user?.username || '匿名用户'}</Text>
|
||
<Text fontSize="sm" color="gray.500">
|
||
{format(new Date(post.created_at), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
||
</Text>
|
||
</VStack>
|
||
</HStack>
|
||
<IconButton
|
||
icon={<FiTrash2 />}
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={handleDelete}
|
||
/>
|
||
</HStack>
|
||
|
||
{/* 帖子内容 */}
|
||
{post.title && (
|
||
<Heading size="md" mb={2}>
|
||
{post.title}
|
||
</Heading>
|
||
)}
|
||
<Text mb={4} whiteSpace="pre-wrap">
|
||
{post.content}
|
||
</Text>
|
||
|
||
{/* 操作栏 */}
|
||
<HStack spacing={4}>
|
||
<Button
|
||
size="sm"
|
||
variant="ghost"
|
||
leftIcon={liked ? <FaHeart /> : <FaRegHeart />}
|
||
color={liked ? 'red.500' : 'gray.500'}
|
||
onClick={handleLike}
|
||
>
|
||
{likesCount}
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="ghost"
|
||
leftIcon={<FaComment />}
|
||
rightIcon={showComments ? <FiChevronUp /> : <FiChevronDown />}
|
||
onClick={loadComments}
|
||
>
|
||
{post.comments_count || 0} 评论
|
||
</Button>
|
||
</HStack>
|
||
|
||
{/* 评论区 */}
|
||
<Collapse in={showComments} animateOpacity>
|
||
<Box mt={4} pt={4} borderTopWidth="1px" borderColor={borderColor}>
|
||
{/* 评论输入 */}
|
||
<HStack mb={4}>
|
||
<Textarea
|
||
placeholder="写下你的评论..."
|
||
size="sm"
|
||
value={newComment}
|
||
onChange={(e) => setNewComment(e.target.value)}
|
||
rows={2}
|
||
/>
|
||
<Button
|
||
colorScheme="blue"
|
||
size="sm"
|
||
onClick={handleAddComment}
|
||
isDisabled={!newComment.trim()}
|
||
>
|
||
评论
|
||
</Button>
|
||
</HStack>
|
||
|
||
{/* 评论列表 */}
|
||
{isLoading ? (
|
||
<Center py={4}>
|
||
<Spinner size="sm" />
|
||
</Center>
|
||
) : (
|
||
<VStack align="stretch" spacing={3}>
|
||
{comments.map((comment) => (
|
||
<Box key={comment.id} pl={4} borderLeftWidth="2px" borderColor="gray.200">
|
||
<HStack mb={1}>
|
||
<Text fontWeight="medium" fontSize="sm">
|
||
{comment.user?.username || '匿名用户'}
|
||
</Text>
|
||
<Text fontSize="xs" color="gray.500">
|
||
{format(new Date(comment.created_at), 'MM-dd HH:mm')}
|
||
</Text>
|
||
</HStack>
|
||
<Text fontSize="sm">{comment.content}</Text>
|
||
</Box>
|
||
))}
|
||
{comments.length === 0 && (
|
||
<Text color="gray.500" textAlign="center" py={2}>
|
||
暂无评论
|
||
</Text>
|
||
)}
|
||
</VStack>
|
||
)}
|
||
</Box>
|
||
</Collapse>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
const EventDetail = () => {
|
||
const { eventId } = useParams();
|
||
const location = useLocation();
|
||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||
const toast = useToast();
|
||
|
||
// 用户认证和权限控制
|
||
const { user } = useAuth();
|
||
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
||
|
||
// State hooks
|
||
const [eventData, setEventData] = useState(null);
|
||
const [relatedStocks, setRelatedStocks] = useState([]);
|
||
const [relatedConcepts, setRelatedConcepts] = useState([]);
|
||
const [historicalEvents, setHistoricalEvents] = useState([]);
|
||
const [posts, setPosts] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [postsLoading, setPostsLoading] = useState(false);
|
||
const [error, setError] = useState(null);
|
||
const [activeTab, setActiveTab] = useState(0);
|
||
|
||
// 🎯 初始化事件详情埋点Hook(传入event对象)
|
||
const eventEvents = useEventDetailEvents({
|
||
event: eventData ? {
|
||
id: eventData.id,
|
||
title: eventData.title,
|
||
importance: eventData.importance
|
||
} : null
|
||
});
|
||
const [newPostContent, setNewPostContent] = useState('');
|
||
const [newPostTitle, setNewPostTitle] = useState('');
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [upgradeModal, setUpgradeModal] = useState({ isOpen: false, feature: '功能', required: 'pro' });
|
||
|
||
// 从URL路径中提取eventId(处理多种URL格式)
|
||
const getEventIdFromPath = () => {
|
||
const pathParts = location.pathname.split('/');
|
||
const lastPart = pathParts[pathParts.length - 1];
|
||
const secondLastPart = pathParts[pathParts.length - 2];
|
||
|
||
if (!isNaN(lastPart) && lastPart) {
|
||
return lastPart;
|
||
}
|
||
if (!isNaN(secondLastPart) && secondLastPart) {
|
||
return secondLastPart;
|
||
}
|
||
return eventId;
|
||
};
|
||
|
||
const actualEventId = getEventIdFromPath();
|
||
|
||
const loadEventData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
// 加载基本事件信息(免费用户也可以访问)
|
||
const eventResponse = await eventService.getEventDetail(actualEventId);
|
||
setEventData(eventResponse.data);
|
||
|
||
// 总是尝试加载相关股票(权限在组件内部检查)
|
||
let stocksCount = 0;
|
||
try {
|
||
const stocksResponse = await eventService.getRelatedStocks(actualEventId);
|
||
setRelatedStocks(stocksResponse.data || []);
|
||
stocksCount = stocksResponse.data?.length || 0;
|
||
} catch (e) {
|
||
logger.warn('EventDetail', '加载相关股票失败', { eventId: actualEventId, error: e.message });
|
||
setRelatedStocks([]);
|
||
}
|
||
|
||
// 根据权限决定是否加载相关概念
|
||
if (hasFeatureAccess('related_concepts')) {
|
||
try {
|
||
const conceptsResponse = await eventService.getRelatedConcepts(actualEventId);
|
||
setRelatedConcepts(conceptsResponse.data || []);
|
||
} catch (e) {
|
||
logger.warn('EventDetail', '加载相关概念失败', { eventId: actualEventId, error: e.message });
|
||
}
|
||
}
|
||
|
||
// 历史事件所有用户都可以访问,但免费用户只看到前2条
|
||
let timelineCount = 0;
|
||
try {
|
||
const eventsResponse = await eventService.getHistoricalEvents(actualEventId);
|
||
setHistoricalEvents(eventsResponse.data || []);
|
||
timelineCount = eventsResponse.data?.length || 0;
|
||
} catch (e) {
|
||
logger.warn('EventDetail', '历史事件加载失败', { eventId: actualEventId, error: e.message });
|
||
}
|
||
|
||
// 🎯 追踪事件分析内容查看(数据加载完成后)
|
||
if (eventResponse.data && eventEvents) {
|
||
eventEvents.trackEventAnalysisViewed({
|
||
type: 'overview',
|
||
relatedStockCount: stocksCount,
|
||
timelineEventCount: timelineCount,
|
||
marketImpact: eventResponse.data.market_impact
|
||
});
|
||
}
|
||
|
||
} catch (err) {
|
||
logger.error('EventDetail', 'loadEventData', err, { eventId: actualEventId });
|
||
setError(err.message || '加载事件数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const refetchStocks = async () => {
|
||
if (!hasFeatureAccess('related_stocks')) return;
|
||
try {
|
||
const stocksResponse = await eventService.getRelatedStocks(actualEventId);
|
||
setRelatedStocks(stocksResponse.data);
|
||
} catch (err) {
|
||
logger.error('EventDetail', 'refetchStocks', err, { eventId: actualEventId });
|
||
}
|
||
};
|
||
|
||
const handleFollowToggle = async () => {
|
||
try {
|
||
await eventService.toggleFollow(actualEventId, eventData.is_following);
|
||
|
||
setEventData(prev => ({
|
||
...prev,
|
||
is_following: !prev.is_following,
|
||
follower_count: prev.is_following
|
||
? prev.follower_count - 1
|
||
: prev.follower_count + 1
|
||
}));
|
||
} catch (err) {
|
||
logger.error('EventDetail', 'handleFollowToggle', err, { eventId: actualEventId });
|
||
}
|
||
};
|
||
|
||
// 加载帖子列表
|
||
const loadPosts = async () => {
|
||
setPostsLoading(true);
|
||
try {
|
||
const result = await eventService.getPosts(actualEventId);
|
||
if (result.success) {
|
||
setPosts(result.data || []);
|
||
}
|
||
} catch (err) {
|
||
logger.error('EventDetail', 'loadPosts', err, { eventId: actualEventId });
|
||
} finally {
|
||
setPostsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 创建新帖子
|
||
const handleCreatePost = async () => {
|
||
if (!newPostContent.trim()) return;
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
const result = await eventService.createPost(actualEventId, {
|
||
title: newPostTitle.trim(),
|
||
content: newPostContent.trim(),
|
||
content_type: 'text',
|
||
});
|
||
|
||
if (result.success) {
|
||
toast({
|
||
title: '帖子发布成功',
|
||
status: 'success',
|
||
duration: 2000,
|
||
});
|
||
setNewPostContent('');
|
||
setNewPostTitle('');
|
||
loadPosts();
|
||
// 更新帖子数
|
||
setEventData(prev => ({
|
||
...prev,
|
||
post_count: (prev.post_count || 0) + 1
|
||
}));
|
||
}
|
||
} catch (err) {
|
||
toast({
|
||
title: '发布失败',
|
||
description: err.message,
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Effect hook - must be called after all state hooks
|
||
useEffect(() => {
|
||
if (actualEventId) {
|
||
loadEventData();
|
||
loadPosts();
|
||
} else {
|
||
setError('无效的事件ID');
|
||
setLoading(false);
|
||
}
|
||
}, [actualEventId, location.pathname]);
|
||
|
||
// 加载状态
|
||
if (loading) {
|
||
return (
|
||
<Box bg={bgColor} minH="100vh" w="100%" p={4}>
|
||
<Container maxW="7xl" py={8}>
|
||
<VStack spacing={6}>
|
||
<Skeleton height="150px" borderRadius="lg" />
|
||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} w="100%">
|
||
{[1, 2, 3, 4].map((i) => (
|
||
<Skeleton key={i} height="80px" borderRadius="md" />
|
||
))}
|
||
</SimpleGrid>
|
||
<Grid templateColumns={{ base: "1fr", lg: "1fr 1fr" }} gap={6} w="100%">
|
||
<Skeleton height="300px" borderRadius="lg" />
|
||
<Skeleton height="300px" borderRadius="lg" />
|
||
</Grid>
|
||
</VStack>
|
||
</Container>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// 错误状态
|
||
if (error) {
|
||
return (
|
||
<Box bg={bgColor} minH="100vh" w="100%" p={4}>
|
||
<Container maxW="7xl" py={8}>
|
||
<Center minH="60vh">
|
||
<Alert
|
||
status="error"
|
||
borderRadius="lg"
|
||
maxW="md"
|
||
flexDirection="column"
|
||
textAlign="center"
|
||
p={6}
|
||
>
|
||
<AlertIcon boxSize="40px" mr={0} />
|
||
<AlertTitle mt={4} mb={2} fontSize="lg">
|
||
加载失败
|
||
</AlertTitle>
|
||
<AlertDescription maxWidth="sm">
|
||
{error}
|
||
{actualEventId && (
|
||
<Text mt={2} fontSize="sm" color="gray.500">
|
||
事件ID: {actualEventId}
|
||
</Text>
|
||
)}
|
||
</AlertDescription>
|
||
</Alert>
|
||
</Center>
|
||
</Container>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// 主要内容
|
||
return (
|
||
<Box bg={bgColor} minH="100vh" w="100%">
|
||
{/* Navigation bar provided by MainLayout */}
|
||
<Container maxW="7xl" py={8}>
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 事件基本信息 */}
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={6}
|
||
>
|
||
<EventHeader
|
||
event={eventData}
|
||
onFollowToggle={handleFollowToggle}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 统计数据 */}
|
||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
|
||
<StatCard
|
||
icon={FiTrendingUp}
|
||
label="关注度"
|
||
value={eventData?.follower_count || 0}
|
||
color="blue"
|
||
/>
|
||
<StatCard
|
||
icon={hasFeatureAccess('related_stocks') ? FiActivity : FiLock}
|
||
label="相关标的"
|
||
value={hasFeatureAccess('related_stocks') ? relatedStocks.length : '🔒需Pro'}
|
||
color={hasFeatureAccess('related_stocks') ? "green" : "orange"}
|
||
/>
|
||
<StatCard
|
||
icon={FiZap}
|
||
label="预期偏离度"
|
||
value={`${(eventData?.expectation_surprise_score || 0).toFixed(1)}%`}
|
||
color="purple"
|
||
/>
|
||
<StatCard
|
||
icon={FiMessageSquare}
|
||
label="讨论数"
|
||
value={eventData?.post_count || 0}
|
||
color="orange"
|
||
/>
|
||
</SimpleGrid>
|
||
|
||
{/* 主要内容标签页 */}
|
||
<Tabs colorScheme="blue" size="md">
|
||
<TabList>
|
||
<Tab>
|
||
相关标的
|
||
{!hasFeatureAccess('related_stocks') && (
|
||
<Icon as={FiLock} ml={1} boxSize={3} color="orange.400" />
|
||
)}
|
||
</Tab>
|
||
<Tab>
|
||
相关概念
|
||
{!hasFeatureAccess('related_concepts') && (
|
||
<Icon as={FiLock} ml={1} boxSize={3} color="orange.400" />
|
||
)}
|
||
</Tab>
|
||
<Tab>历史事件</Tab>
|
||
<Tab>
|
||
传导链分析
|
||
{!hasFeatureAccess('transmission_chain') && (
|
||
<Icon as={FiLock} ml={1} boxSize={3} color="purple.400" />
|
||
)}
|
||
</Tab>
|
||
<Tab>讨论区</Tab>
|
||
</TabList>
|
||
|
||
<TabPanels>
|
||
{/* 相关标的标签页 */}
|
||
<TabPanel px={0}>
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={6}
|
||
>
|
||
{!hasFeatureAccess('related_stocks') ? (
|
||
<VStack spacing={3} align="center" py={8}>
|
||
<Icon as={FiLock} boxSize={8} color="orange.400" />
|
||
<Text>该功能为Pro专享,请升级订阅后查看相关标的。</Text>
|
||
<Button colorScheme="blue" onClick={() => setUpgradeModal({ isOpen: true, feature: '相关标的', required: 'pro' })}>升级到Pro版</Button>
|
||
</VStack>
|
||
) : (
|
||
<RelatedStocks
|
||
eventId={actualEventId}
|
||
eventTime={eventData?.created_at}
|
||
stocks={relatedStocks}
|
||
loading={false}
|
||
error={null}
|
||
onStockAdded={refetchStocks}
|
||
onStockDeleted={refetchStocks}
|
||
/>
|
||
)}
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* 相关概念标签页 */}
|
||
<TabPanel px={0}>
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={6}
|
||
>
|
||
{!hasFeatureAccess('related_concepts') ? (
|
||
<VStack spacing={3} align="center" py={8}>
|
||
<Icon as={FiLock} boxSize={8} color="orange.400" />
|
||
<Text>该功能为Pro专享,请升级订阅后查看相关概念。</Text>
|
||
<Button colorScheme="blue" onClick={() => setUpgradeModal({ isOpen: true, feature: '相关概念', required: 'pro' })}>升级到Pro版</Button>
|
||
</VStack>
|
||
) : (
|
||
<RelatedConcepts
|
||
eventTitle={eventData?.title}
|
||
eventTime={eventData?.created_at}
|
||
eventId={actualEventId}
|
||
loading={loading}
|
||
error={error}
|
||
/>
|
||
)}
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* 历史事件标签页 */}
|
||
<TabPanel px={0}>
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={6}
|
||
>
|
||
<HistoricalEvents
|
||
events={historicalEvents}
|
||
expectationScore={eventData?.expectation_surprise_score}
|
||
loading={false}
|
||
error={null}
|
||
/>
|
||
{!hasFeatureAccess('historical_events_full') && historicalEvents.length > 0 && (
|
||
<Box mt={4} p={3} bg="orange.50" borderRadius="md" border="1px solid" borderColor="orange.200">
|
||
<HStack>
|
||
<Icon as={FiLock} color="orange.400" />
|
||
<Text color="orange.700" fontSize="sm">
|
||
免费版仅展示前2条历史事件,
|
||
<Button
|
||
variant="link"
|
||
colorScheme="orange"
|
||
size="sm"
|
||
onClick={() => setUpgradeModal({ isOpen: true, feature: '完整历史事件', required: 'pro' })}
|
||
>
|
||
升级Pro版
|
||
</Button>
|
||
可查看全部。
|
||
</Text>
|
||
</HStack>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* 传导链分析标签页 */}
|
||
<TabPanel px={0}>
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={6}
|
||
>
|
||
{!hasFeatureAccess('transmission_chain') ? (
|
||
<VStack spacing={3} align="center" py={8}>
|
||
<Icon as={FiLock} boxSize={8} color="purple.400" />
|
||
<Text>传导链分析为Max专享,请升级订阅后查看。</Text>
|
||
<Button colorScheme="purple" onClick={() => setUpgradeModal({ isOpen: true, feature: '传导链分析', required: 'max' })}>升级到Max版</Button>
|
||
</VStack>
|
||
) : (
|
||
<TransmissionChainAnalysis
|
||
eventId={actualEventId}
|
||
eventService={eventService}
|
||
/>
|
||
)}
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* 讨论区标签页 */}
|
||
<TabPanel px={0}>
|
||
<VStack spacing={6}>
|
||
{/* 发布新帖子 */}
|
||
{user && (
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={6}
|
||
w="100%"
|
||
>
|
||
<VStack spacing={4}>
|
||
<Input
|
||
placeholder="帖子标题(可选)"
|
||
value={newPostTitle}
|
||
onChange={(e) => setNewPostTitle(e.target.value)}
|
||
/>
|
||
<Textarea
|
||
placeholder="分享你的想法..."
|
||
value={newPostContent}
|
||
onChange={(e) => setNewPostContent(e.target.value)}
|
||
rows={4}
|
||
/>
|
||
<HStack w="100%" justify="flex-end">
|
||
<Button
|
||
colorScheme="blue"
|
||
onClick={handleCreatePost}
|
||
isLoading={submitting}
|
||
isDisabled={!newPostContent.trim()}
|
||
>
|
||
发布
|
||
</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 帖子列表 */}
|
||
<Box w="100%">
|
||
{postsLoading ? (
|
||
<VStack spacing={4}>
|
||
{[1, 2, 3].map((i) => (
|
||
<Skeleton key={i} height="120px" w="100%" borderRadius="lg" />
|
||
))}
|
||
</VStack>
|
||
) : posts.length > 0 ? (
|
||
posts.map((post) => (
|
||
<PostItem
|
||
key={post.id}
|
||
post={post}
|
||
onRefresh={loadPosts}
|
||
eventEvents={eventEvents}
|
||
/>
|
||
))
|
||
) : (
|
||
<Box
|
||
bg={useColorModeValue('white', 'gray.800')}
|
||
borderRadius="lg"
|
||
borderWidth="1px"
|
||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||
p={8}
|
||
textAlign="center"
|
||
>
|
||
<Text color="gray.500">还没有讨论,来发布第一个帖子吧!</Text>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
</VStack>
|
||
</TabPanel>
|
||
</TabPanels>
|
||
</Tabs>
|
||
</VStack>
|
||
</Container>
|
||
|
||
{/* 升级弹窗 */}
|
||
<SubscriptionUpgradeModal
|
||
isOpen={upgradeModal.isOpen}
|
||
onClose={() => setUpgradeModal({ isOpen: false, feature: '功能', required: 'pro' })}
|
||
requiredLevel={upgradeModal.required}
|
||
featureName={upgradeModal.feature}
|
||
currentLevel={user?.subscription_type || 'free'}
|
||
/>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default EventDetail; |