Files
vf_react/src/views/ValueForum/PostDetail.js
2025-11-23 22:27:57 +08:00

391 lines
12 KiB
JavaScript

/**
* 帖子详情页
* 展示帖子完整内容、事件时间轴、评论区
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Heading,
Text,
HStack,
VStack,
Avatar,
Badge,
Button,
Image,
SimpleGrid,
Spinner,
Center,
Flex,
IconButton,
Divider,
} from '@chakra-ui/react';
import { useParams, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import {
ArrowLeft,
Heart,
MessageCircle,
Eye,
Share2,
Bookmark,
} from 'lucide-react';
import { forumColors } from '@theme/forumTheme';
import {
getPostById,
likePost,
getEventsByPostId,
} from '@services/elasticsearchService';
import EventTimeline from './components/EventTimeline';
import CommentSection from './components/CommentSection';
import ImagePreviewModal from '@components/ImagePreviewModal';
const MotionBox = motion(Box);
const PostDetail = () => {
const { postId } = useParams();
const navigate = useNavigate();
const [post, setPost] = useState(null);
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [isLiked, setIsLiked] = useState(false);
const [likes, setLikes] = useState(0);
// 图片预览相关状态
const [isImagePreviewOpen, setIsImagePreviewOpen] = useState(false);
const [previewImageIndex, setPreviewImageIndex] = useState(0);
// 加载帖子数据
useEffect(() => {
const loadPostData = async () => {
try {
setLoading(true);
// 并行加载帖子和事件
const [postData, eventsData] = await Promise.all([
getPostById(postId),
getEventsByPostId(postId),
]);
setPost(postData);
setLikes(postData.likes_count || 0);
setEvents(eventsData);
} catch (error) {
console.error('加载帖子失败:', error);
} finally {
setLoading(false);
}
};
loadPostData();
}, [postId]);
// 处理点赞
const handleLike = async () => {
try {
if (!isLiked) {
await likePost(postId);
setLikes((prev) => prev + 1);
setIsLiked(true);
}
} catch (error) {
console.error('点赞失败:', error);
}
};
// 打开图片预览
const handleImageClick = (index) => {
setPreviewImageIndex(index);
setIsImagePreviewOpen(true);
};
// 格式化时间
const formatTime = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
if (loading) {
return (
<Box minH="100vh" bg={forumColors.background.main} pt="80px">
<Center py="20">
<VStack spacing="4">
<Spinner
size="xl"
thickness="4px"
speed="0.8s"
color={forumColors.primary[500]}
/>
<Text color={forumColors.text.secondary}>加载中...</Text>
</VStack>
</Center>
</Box>
);
}
if (!post) {
return (
<Box minH="100vh" bg={forumColors.background.main} pt="80px">
<Center py="20">
<VStack spacing="4">
<Text color={forumColors.text.secondary} fontSize="lg">
帖子不存在或已被删除
</Text>
<Button
leftIcon={<ArrowLeft size={18} />}
onClick={() => navigate('/value-forum')}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
_hover={{ opacity: 0.9 }}
>
返回论坛
</Button>
</VStack>
</Center>
</Box>
);
}
return (
<Box minH="100vh" bg={forumColors.background.main} pt="80px" pb="20">
<Container maxW="container.xl">
{/* 返回按钮 */}
<Button
leftIcon={<ArrowLeft size={18} />}
onClick={() => navigate('/value-forum')}
variant="ghost"
color={forumColors.text.secondary}
_hover={{ color: forumColors.text.primary }}
mb="6"
>
返回论坛
</Button>
<SimpleGrid columns={{ base: 1, lg: 3 }} spacing="6">
{/* 左侧:帖子内容 + 评论 */}
<Box gridColumn={{ base: '1', lg: '1 / 3' }}>
<VStack spacing="6" align="stretch">
{/* 帖子主体 */}
<MotionBox
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
bg={forumColors.background.card}
borderRadius="xl"
border="1px solid"
borderColor={forumColors.border.default}
overflow="hidden"
>
{/* 作者信息 */}
<Box p="6" borderBottomWidth="1px" borderColor={forumColors.border.default}>
<Flex justify="space-between" align="start">
<HStack spacing="3">
<Avatar
size="md"
name={post.author_name}
src={post.author_avatar}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
/>
<VStack align="start" spacing="1">
<Text fontSize="md" fontWeight="600" color={forumColors.text.primary}>
{post.author_name}
</Text>
<Text fontSize="xs" color={forumColors.text.muted}>
发布于 {formatTime(post.created_at)}
</Text>
</VStack>
</HStack>
{/* 操作按钮 */}
<HStack spacing="2">
<IconButton
icon={<Share2 size={18} />}
variant="ghost"
color={forumColors.text.tertiary}
_hover={{ color: forumColors.primary[500] }}
aria-label="分享"
/>
<IconButton
icon={<Bookmark size={18} />}
variant="ghost"
color={forumColors.text.tertiary}
_hover={{ color: forumColors.primary[500] }}
aria-label="收藏"
/>
</HStack>
</Flex>
</Box>
{/* 帖子内容 */}
<Box p="6">
{/* 标题 */}
<Heading
as="h1"
fontSize="2xl"
fontWeight="bold"
color={forumColors.text.primary}
mb="4"
>
{post.title}
</Heading>
{/* 标签 */}
{post.tags && post.tags.length > 0 && (
<HStack spacing="2" mb="6" flexWrap="wrap">
{post.tags.map((tag, index) => (
<Badge
key={index}
bg={forumColors.gradients.goldSubtle}
color={forumColors.primary[500]}
border="1px solid"
borderColor={forumColors.border.gold}
borderRadius="full"
px="3"
py="1"
fontSize="sm"
>
#{tag}
</Badge>
))}
</HStack>
)}
{/* 正文 */}
<Text
fontSize="md"
color={forumColors.text.secondary}
lineHeight="1.8"
whiteSpace="pre-wrap"
mb="6"
>
{post.content}
</Text>
{/* 图片 */}
{post.images && post.images.length > 0 && (
<SimpleGrid columns={{ base: 1, sm: 2, md: 3 }} spacing="4" mb="6">
{post.images.map((img, index) => (
<Image
key={index}
src={img}
alt={`图片 ${index + 1}`}
borderRadius="md"
border="1px solid"
borderColor={forumColors.border.default}
cursor="pointer"
onClick={() => handleImageClick(index)}
_hover={{
transform: 'scale(1.05)',
boxShadow: forumColors.shadows.gold,
}}
transition="all 0.3s"
/>
))}
</SimpleGrid>
)}
</Box>
{/* 互动栏 */}
<Box
p="4"
borderTopWidth="1px"
borderColor={forumColors.border.default}
bg={forumColors.background.secondary}
>
<Flex justify="space-between" align="center">
<HStack spacing="6">
<HStack
spacing="2"
cursor="pointer"
onClick={handleLike}
color={isLiked ? forumColors.primary[500] : forumColors.text.tertiary}
_hover={{ color: forumColors.primary[500] }}
>
<Heart size={20} fill={isLiked ? 'currentColor' : 'none'} />
<Text fontSize="sm" fontWeight="500">
{likes}
</Text>
</HStack>
<HStack spacing="2" color={forumColors.text.tertiary}>
<MessageCircle size={20} />
<Text fontSize="sm" fontWeight="500">
{post.comments_count || 0}
</Text>
</HStack>
<HStack spacing="2" color={forumColors.text.tertiary}>
<Eye size={20} />
<Text fontSize="sm" fontWeight="500">
{post.views_count || 0}
</Text>
</HStack>
</HStack>
<Button
leftIcon={<Heart size={18} />}
onClick={handleLike}
bg={isLiked ? forumColors.primary[500] : 'transparent'}
color={isLiked ? forumColors.background.main : forumColors.text.primary}
border="1px solid"
borderColor={forumColors.border.gold}
_hover={{
bg: forumColors.gradients.goldPrimary,
color: forumColors.background.main,
}}
>
{isLiked ? '已点赞' : '点赞'}
</Button>
</Flex>
</Box>
</MotionBox>
{/* 评论区 */}
<MotionBox
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<CommentSection postId={postId} />
</MotionBox>
</VStack>
</Box>
{/* 右侧:事件时间轴 */}
<Box gridColumn={{ base: '1', lg: '3' }}>
<MotionBox
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
position="sticky"
top="100px"
>
<EventTimeline events={events} />
</MotionBox>
</Box>
</SimpleGrid>
</Container>
{/* 图片预览弹窗 */}
<ImagePreviewModal
isOpen={isImagePreviewOpen}
onClose={() => setIsImagePreviewOpen(false)}
images={post?.images || []}
initialIndex={previewImageIndex}
/>
</Box>
);
};
export default PostDetail;