Files
vf_react/src/views/Community/components/EventDiscussionModal.js
zdl 87b77af187 feat:Community 组件 (2个文件,8个console)
- EventDetailModal.js - 2个
  - InvestmentCalendar.js - 6个

  EventDetail 组件 (5个文件,54个console)

  - TransmissionChainAnalysis.js - 43个 ⚠️ 最复杂
  - RelatedConcepts.js - 14个
  - LimitAnalyse.js - 5个 (保留2个toast)
  - RelatedStocks.js - 3个 (保留4个toast)
  - HistoricalEvents.js - 1个

  StockChart 组件 (1个文件,4个console)
2025-10-18 10:23:23 +08:00

615 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
Box,
Text,
VStack,
HStack,
Avatar,
Textarea,
Button,
Divider,
useToast,
Badge,
Flex,
IconButton,
Menu,
MenuButton,
MenuList,
MenuItem,
useColorModeValue,
Spinner,
Center,
Collapse,
Input,
} from '@chakra-ui/react';
import {
ChatIcon,
TimeIcon,
DeleteIcon,
EditIcon,
ChevronDownIcon,
TriangleDownIcon,
TriangleUpIcon,
} from '@chakra-ui/icons';
import { FaHeart, FaRegHeart, FaComment } from 'react-icons/fa';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { eventService } from '../../../services/eventService';
import { logger } from '../../../utils/logger';
const EventDiscussionModal = ({ isOpen, onClose, eventId, eventTitle, discussionType = '事件讨论' }) => {
const [posts, setPosts] = useState([]);
const [newPostContent, setNewPostContent] = useState('');
const [newPostTitle, setNewPostTitle] = useState('');
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [expandedPosts, setExpandedPosts] = useState({});
const [postComments, setPostComments] = useState({});
const [replyContents, setReplyContents] = useState({});
const [loadingComments, setLoadingComments] = useState({});
const toast = useToast();
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
// 加载帖子列表
const loadPosts = async () => {
if (!eventId) return;
setLoading(true);
try {
const response = await fetch(`/api/events/${eventId}/posts?sort=latest&page=1&per_page=20`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
setPosts(result.data || []);
logger.debug('EventDiscussionModal', '帖子列表加载成功', {
eventId,
postsCount: result.data?.length || 0
});
} else {
logger.error('EventDiscussionModal', 'loadPosts', new Error('API返回错误'), {
eventId,
status: response.status,
message: result.message
});
toast({
title: '加载帖子失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'loadPosts', error, { eventId });
toast({
title: '加载帖子失败',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
};
// 加载帖子的评论
const loadPostComments = async (postId) => {
setLoadingComments(prev => ({ ...prev, [postId]: true }));
try {
const response = await fetch(`/api/posts/${postId}/comments?sort=latest`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
setPostComments(prev => ({ ...prev, [postId]: result.data || [] }));
logger.debug('EventDiscussionModal', '评论加载成功', {
postId,
commentsCount: result.data?.length || 0
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'loadPostComments', error, { postId });
} finally {
setLoadingComments(prev => ({ ...prev, [postId]: false }));
}
};
// 切换展开/收起评论
const togglePostComments = async (postId) => {
const isExpanded = expandedPosts[postId];
if (!isExpanded) {
// 展开时加载评论
await loadPostComments(postId);
}
setExpandedPosts(prev => ({ ...prev, [postId]: !isExpanded }));
};
// 提交新帖子
const handleSubmitPost = async () => {
if (!newPostContent.trim()) return;
setSubmitting(true);
try {
const response = await fetch(`/api/events/${eventId}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: newPostTitle.trim(),
content: newPostContent.trim(),
content_type: 'text',
})
});
const result = await response.json();
if (response.ok && result.success) {
setNewPostContent('');
setNewPostTitle('');
loadPosts();
logger.info('EventDiscussionModal', '帖子发布成功', {
eventId,
postId: result.data?.id
});
toast({
title: '帖子发布成功',
status: 'success',
duration: 2000,
isClosable: true,
});
} else {
logger.error('EventDiscussionModal', 'handleSubmitPost', new Error('API返回错误'), {
eventId,
message: result.message
});
toast({
title: result.message || '帖子发布失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleSubmitPost', error, { eventId });
toast({
title: '帖子发布失败',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setSubmitting(false);
}
};
// 删除帖子
const handleDeletePost = async (postId) => {
if (!window.confirm('确定要删除这个帖子吗?')) return;
try {
const response = await fetch(`/api/posts/${postId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
loadPosts();
logger.info('EventDiscussionModal', '帖子删除成功', { postId });
toast({
title: '帖子已删除',
status: 'success',
duration: 2000,
isClosable: true,
});
} else {
logger.error('EventDiscussionModal', 'handleDeletePost', new Error('API返回错误'), {
postId,
message: result.message
});
toast({
title: result.message || '删除失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleDeletePost', error, { postId });
toast({
title: '删除失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
// 点赞帖子
const handleLikePost = async (postId) => {
try {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
// 更新帖子列表中的点赞状态
setPosts(prev => prev.map(post =>
post.id === postId
? { ...post, likes_count: result.likes_count, liked: result.liked }
: post
));
logger.debug('EventDiscussionModal', '点赞操作成功', {
postId,
liked: result.liked,
likesCount: result.likes_count
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleLikePost', error, { postId });
toast({
title: '操作失败',
status: 'error',
duration: 2000,
isClosable: true,
});
}
};
// 提交评论
const handleSubmitComment = async (postId) => {
const content = replyContents[postId];
if (!content?.trim()) return;
try {
const response = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
content: content.trim(),
})
});
const result = await response.json();
if (response.ok && result.success) {
setReplyContents(prev => ({ ...prev, [postId]: '' }));
// 重新加载该帖子的评论
await loadPostComments(postId);
// 更新帖子的评论数
setPosts(prev => prev.map(post =>
post.id === postId
? { ...post, comments_count: (post.comments_count || 0) + 1 }
: post
));
logger.info('EventDiscussionModal', '评论发布成功', {
postId,
commentId: result.data?.id
});
toast({
title: '评论发布成功',
status: 'success',
duration: 2000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleSubmitComment', error, { postId });
toast({
title: '评论发布失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
// 删除评论
const handleDeleteComment = async (commentId, postId) => {
if (!window.confirm('确定要删除这条评论吗?')) return;
try {
const response = await fetch(`/api/comments/${commentId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
// 重新加载该帖子的评论
await loadPostComments(postId);
// 更新帖子的评论数
setPosts(prev => prev.map(post =>
post.id === postId
? { ...post, comments_count: Math.max(0, (post.comments_count || 0) - 1) }
: post
));
logger.info('EventDiscussionModal', '评论删除成功', { commentId, postId });
toast({
title: '评论已删除',
status: 'success',
duration: 2000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleDeleteComment', error, { commentId, postId });
toast({
title: '删除失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
useEffect(() => {
if (isOpen) {
loadPosts();
}
}, [isOpen, eventId]);
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent maxH="80vh">
<ModalHeader>
<VStack align="start" spacing={1}>
<HStack>
<ChatIcon />
<Text>{discussionType}</Text>
</HStack>
{eventTitle && (
<Text fontSize="sm" color="gray.500" fontWeight="normal">
{eventTitle}
</Text>
)}
</VStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody overflowY="auto">
{/* 发布新帖子 */}
<Box mb={4}>
<Input
value={newPostTitle}
onChange={(e) => setNewPostTitle(e.target.value)}
placeholder="帖子标题(可选)"
size="sm"
mb={2}
/>
<Textarea
value={newPostContent}
onChange={(e) => setNewPostContent(e.target.value)}
placeholder="分享您的观点..."
size="sm"
resize="vertical"
minH="80px"
/>
<Flex justify="flex-end" mt={2}>
<Button
colorScheme="blue"
size="sm"
onClick={handleSubmitPost}
isLoading={submitting}
isDisabled={!newPostContent.trim()}
>
发布帖子
</Button>
</Flex>
</Box>
<Divider mb={4} />
{/* 帖子列表 */}
{loading ? (
<Center py={8}>
<Spinner size="lg" />
</Center>
) : posts.length > 0 ? (
<VStack spacing={4} align="stretch">
{posts.map((post) => (
<Box
key={post.id}
p={4}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
transition="background 0.2s"
>
{/* 帖子头部 */}
<Flex justify="space-between" align="start" mb={3}>
<HStack align="start" spacing={3}>
<Avatar
size="sm"
name={post.user?.username || '匿名用户'}
src={post.user?.avatar_url}
/>
<VStack align="start" spacing={1} flex={1}>
<HStack>
<Text fontWeight="semibold" fontSize="sm">
{post.user?.username || '匿名用户'}
</Text>
</HStack>
<HStack fontSize="xs" color="gray.500">
<TimeIcon />
<Text>
{format(new Date(post.created_at), 'MM月dd日 HH:mm', {
locale: zhCN,
})}
</Text>
</HStack>
</VStack>
</HStack>
{/* 操作菜单 */}
<Menu>
<MenuButton
as={IconButton}
icon={<ChevronDownIcon />}
variant="ghost"
size="sm"
/>
<MenuList>
<MenuItem
icon={<DeleteIcon />}
color="red.500"
onClick={() => handleDeletePost(post.id)}
>
删除帖子
</MenuItem>
</MenuList>
</Menu>
</Flex>
{/* 帖子标题 */}
{post.title && (
<Text fontSize="md" fontWeight="bold" mb={2}>
{post.title}
</Text>
)}
{/* 帖子内容 */}
<Text fontSize="sm" whiteSpace="pre-wrap" mb={3}>
{post.content}
</Text>
{/* 帖子操作栏 */}
<HStack spacing={4} mb={3}>
<Button
size="sm"
variant="ghost"
leftIcon={post.liked ? <FaHeart /> : <FaRegHeart />}
color={post.liked ? 'red.500' : 'gray.600'}
onClick={() => handleLikePost(post.id)}
>
{post.likes_count || 0}
</Button>
<Button
size="sm"
variant="ghost"
leftIcon={<FaComment />}
onClick={() => togglePostComments(post.id)}
rightIcon={expandedPosts[post.id] ? <TriangleUpIcon /> : <TriangleDownIcon />}
>
{post.comments_count || 0} 评论
</Button>
</HStack>
{/* 评论区 */}
<Collapse in={expandedPosts[post.id]} animateOpacity>
<Box borderTopWidth="1px" borderColor={borderColor} pt={3}>
{/* 评论输入框 */}
<HStack mb={3}>
<Textarea
size="sm"
placeholder="写下你的评论..."
value={replyContents[post.id] || ''}
onChange={(e) => setReplyContents(prev => ({ ...prev, [post.id]: e.target.value }))}
rows={2}
flex={1}
/>
<Button
size="sm"
colorScheme="blue"
onClick={() => handleSubmitComment(post.id)}
isDisabled={!replyContents[post.id]?.trim()}
>
评论
</Button>
</HStack>
{/* 评论列表 */}
{loadingComments[post.id] ? (
<Center py={4}>
<Spinner size="sm" />
</Center>
) : (
<VStack align="stretch" spacing={2}>
{postComments[post.id]?.map((comment) => (
<Box key={comment.id} pl={4} borderLeftWidth="2px" borderColor="gray.200">
<HStack justify="space-between" mb={1}>
<HStack spacing={2}>
<Avatar size="xs" name={comment.user?.username} src={comment.user?.avatar_url} />
<Text fontSize="sm" fontWeight="medium">
{comment.user?.username || '匿名用户'}
</Text>
<Text fontSize="xs" color="gray.500">
{format(new Date(comment.created_at), 'MM-dd HH:mm')}
</Text>
</HStack>
<IconButton
size="xs"
icon={<DeleteIcon />}
variant="ghost"
onClick={() => handleDeleteComment(comment.id, post.id)}
/>
</HStack>
<Text fontSize="sm" pl={7}>
{comment.content}
</Text>
{/* 显示回复 */}
{comment.replies && comment.replies.length > 0 && (
<VStack align="stretch" mt={2} spacing={1} pl={4}>
{comment.replies.map((reply) => (
<Box key={reply.id}>
<HStack spacing={1}>
<Text fontSize="xs" fontWeight="medium">
{reply.user?.username}:
</Text>
<Text fontSize="xs">{reply.content}</Text>
</HStack>
</Box>
))}
</VStack>
)}
</Box>
))}
{(!postComments[post.id] || postComments[post.id].length === 0) && (
<Text fontSize="sm" color="gray.500" textAlign="center" py={2}>
暂无评论
</Text>
)}
</VStack>
)}
</Box>
</Collapse>
</Box>
))}
</VStack>
) : (
<Center py={8}>
<VStack>
<ChatIcon boxSize={8} color="gray.400" />
<Text color="gray.500">暂无帖子快来发表您的观点吧</Text>
</VStack>
</Center>
)}
</ModalBody>
</ModalContent>
</Modal>
);
};
export default EventDiscussionModal;