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 ( {discussionType} {eventTitle && ( {eventTitle} )} {/* 发布新帖子 */} setNewPostTitle(e.target.value)} placeholder="帖子标题(可选)" size="sm" mb={2} />