| import React, { useState, useEffect } from 'react'; |
| import { useParams, Link } from 'umi'; |
| import { Card, Avatar, Tag, Row, Col, Divider, List, Input, Button, Typography, message, Spin, Image, Modal, Badge, Pagination } from 'antd'; |
| import { Comment } from '@ant-design/compatible'; |
| import { |
| UserOutlined, |
| ClockCircleOutlined, |
| EyeOutlined, |
| TagOutlined, |
| LikeOutlined, |
| DislikeOutlined, |
| ShareAltOutlined, |
| HeartOutlined, |
| HeartFilled, |
| ExclamationCircleOutlined, |
| CrownOutlined |
| } from '@ant-design/icons'; |
| import { getPostDetail, addComment, toggleFavorite, toggleLike, toggleCommentLike, reportPost } from '@/services/post'; |
| import PostCard from '../PostCenter/PostCard'; |
| import styles from './PostDetail.module.css'; |
| import { Post, CommentType } from '../PostCenter/types'; |
| |
| const { TextArea } = Input; |
| const { Title, Paragraph, Text } = Typography; |
| |
| const PostDetail: React.FC = () => { |
| const { id } = useParams<{ id: string }>(); |
| const [post, setPost] = useState<Post | null>(null); |
| const [recommendedPosts, setRecommendedPosts] = useState<Post[]>([]); |
| const [currentRecommendPage, setCurrentRecommendPage] = useState<number>(1); |
| const [comments, setComments] = useState<CommentType[]>([]); |
| const [replyTo, setReplyTo] = useState<number | null>(null); |
| const [commentText, setCommentText] = useState<string>(''); |
| const [loading, setLoading] = useState<boolean>(true); |
| const [favorited, setFavorited] = useState<boolean>(false); |
| const [liked, setLiked] = useState<boolean>(false); |
| const [submittingComment, setSubmittingComment] = useState<boolean>(false); |
| const [commentLikes, setCommentLikes] = useState<Record<number, boolean>>({}); |
| const [reportModalVisible, setReportModalVisible] = useState<boolean>(false); |
| const [reportReason, setReportReason] = useState<string>(''); |
| const [submittingReport, setSubmittingReport] = useState<boolean>(false); |
| |
| const recommendPageSize = 3; // 每页显示3个推荐帖子 |
| |
| useEffect(() => { |
| if (id) { |
| fetchPostDetail(Number(id)); |
| } |
| }, [id]); |
| |
| const fetchPostDetail = async (postId: number) => { |
| try { |
| setLoading(true); |
| const response = await getPostDetail(postId); |
| |
| if (response.code === 200 && response.data) { |
| const { post: postData, tags, comments: commentsData, recommendedPosts, favorited: isFavorited } = response.data; |
| |
| // 转换帖子数据格式 |
| const formattedPost: Post = { |
| ...postData, |
| id: postData.postId, |
| title: postData.title || '无标题', |
| author: postData.author || '未知作者', |
| publishTime: postData.publishTime || postData.createTime || '', |
| tags: postData.tags ? postData.tags.split(',') : [], |
| views: postData.views || 0, |
| comments: postData.comments || 0, |
| favorites: postData.favorites || 0, |
| likes: postData.likes || 0, |
| coverImage: postData.coverImage || '/images/404.png', |
| summary: postData.summary || '暂无摘要', |
| isPromoted: postData.promotionPlanId != null && postData.promotionPlanId > 0, |
| }; |
| setPost(formattedPost); |
| setFavorited(isFavorited); |
| |
| // 转换评论数据格式 |
| const formattedComments = commentsData.map((commentItem: any) => { |
| const comment = commentItem.comment; |
| const replies = commentItem.replies || []; |
| |
| return { |
| ...comment, |
| id: comment.commentId, |
| author: comment.userName || '匿名用户', |
| avatar: comment.userAvatar || '/images/404.png', |
| datetime: comment.createTime, |
| likes: comment.likes || 0, |
| replies: replies.map((reply: any) => ({ |
| ...reply, |
| id: reply.commentId, |
| author: reply.userName || '匿名用户', |
| avatar: reply.userAvatar || '/images/404.png', |
| datetime: reply.createTime, |
| likes: reply.likes || 0, |
| replies: [] |
| })) |
| }; |
| }); |
| setComments(formattedComments); |
| |
| // 转换推荐帖子数据格式 |
| const formatPosts = (posts: any[]) => posts.map((p: any) => ({ |
| ...p, |
| id: p.postId, |
| title: p.title || '无标题', |
| author: p.author || '未知作者', |
| publishTime: p.publishTime || p.createTime || '', |
| tags: p.tags ? p.tags.split(',') : [], |
| views: p.views || 0, |
| comments: p.comments || 0, |
| favorites: p.favorites || 0, |
| likes: p.likes || 0, |
| coverImage: p.coverImage || '/images/404.png', |
| summary: p.summary || '暂无摘要', |
| isPromoted: p.promotionPlanId != null && p.promotionPlanId > 0, // 添加推广标识 |
| })); |
| |
| // 设置推荐帖子 |
| const formattedRecommendedPosts = formatPosts(recommendedPosts || []); |
| setRecommendedPosts(formattedRecommendedPosts); |
| } else { |
| message.error(response.msg || '获取帖子详情失败'); |
| } |
| } catch (error) { |
| console.error('获取帖子详情失败:', error); |
| message.error('获取帖子详情失败,请稍后重试'); |
| } finally { |
| setLoading(false); |
| } |
| }; |
| |
| const handleCommentSubmit = async () => { |
| if (!commentText.trim()) { |
| message.warning('评论内容不能为空'); |
| return; |
| } |
| |
| if (!post) return; |
| |
| try { |
| setSubmittingComment(true); |
| const response = await addComment({ |
| postId: Number(post.id), |
| content: commentText, |
| parentId: replyTo || 0, |
| }); |
| |
| if (response.code === 200) { |
| message.success('评论发表成功'); |
| setCommentText(''); |
| setReplyTo(null); |
| // 重新获取帖子详情以更新评论列表 |
| fetchPostDetail(Number(post.id)); |
| } else { |
| message.error(response.msg || '评论发表失败'); |
| } |
| } catch (error) { |
| console.error('评论发表失败:', error); |
| message.error('评论发表失败,请稍后重试'); |
| } finally { |
| setSubmittingComment(false); |
| } |
| }; |
| |
| const handleFavoriteToggle = async () => { |
| if (!post) return; |
| |
| try { |
| const response = await toggleFavorite(Number(post.id), !favorited); |
| |
| if (response.code === 200) { |
| setFavorited(!favorited); |
| message.success(favorited ? '取消收藏成功' : '收藏成功'); |
| // 更新帖子收藏数 |
| setPost(prev => prev ? { |
| ...prev, |
| favorites: (prev.favorites || 0) + (favorited ? -1 : 1) |
| } : null); |
| } else { |
| message.error(response.msg || '操作失败'); |
| } |
| } catch (error) { |
| console.error('收藏操作失败:', error); |
| message.error('操作失败,请稍后重试'); |
| } |
| }; |
| |
| const handleLikeToggle = async () => { |
| if (!post) return; |
| |
| try { |
| const response = await toggleLike(Number(post.id), !liked); |
| |
| if (response.code === 200) { |
| setLiked(!liked); |
| message.success(liked ? '取消点赞成功' : '点赞成功'); |
| // 更新帖子点赞数 |
| setPost(prev => prev ? { |
| ...prev, |
| likes: (prev.likes || 0) + (liked ? -1 : 1) |
| } : null); |
| } else { |
| message.error(response.msg || '操作失败'); |
| } |
| } catch (error) { |
| console.error('点赞操作失败:', error); |
| message.error('操作失败,请稍后重试'); |
| } |
| }; |
| |
| const handleReply = (commentId: number) => { |
| setReplyTo(commentId); |
| }; |
| |
| const cancelReply = () => { |
| setReplyTo(null); |
| }; |
| |
| const handleCommentLike = async (commentId: number) => { |
| try { |
| const isLiked = commentLikes[commentId] || false; |
| const response = await toggleCommentLike(commentId, !isLiked); |
| |
| if (response.code === 200) { |
| setCommentLikes(prev => ({ |
| ...prev, |
| [commentId]: !isLiked |
| })); |
| |
| // 更新评论点赞数 |
| setComments(prev => prev.map(comment => { |
| if (comment.id === commentId) { |
| return { |
| ...comment, |
| likes: (comment.likes || 0) + (isLiked ? -1 : 1) |
| }; |
| } |
| // 检查回复 |
| if (comment.replies) { |
| const updatedReplies = comment.replies.map(reply => { |
| if (reply.id === commentId) { |
| return { |
| ...reply, |
| likes: (reply.likes || 0) + (isLiked ? -1 : 1) |
| }; |
| } |
| return reply; |
| }); |
| return { ...comment, replies: updatedReplies }; |
| } |
| return comment; |
| })); |
| |
| message.success(isLiked ? '取消点赞成功' : '点赞成功'); |
| } else { |
| message.error(response.msg || '操作失败'); |
| } |
| } catch (error) { |
| console.error('评论点赞操作失败:', error); |
| message.error('操作失败,请稍后重试'); |
| } |
| }; |
| |
| const handleReport = async () => { |
| if (!post) return; |
| |
| if (!reportReason.trim()) { |
| message.warning('请填写举报理由'); |
| return; |
| } |
| |
| try { |
| setSubmittingReport(true); |
| const response = await reportPost(Number(post.id), reportReason); |
| |
| if (response.code === 200) { |
| message.success('举报提交成功,我们会尽快处理'); |
| setReportModalVisible(false); |
| setReportReason(''); |
| } else { |
| message.error(response.msg || '举报提交失败'); |
| } |
| } catch (error) { |
| console.error('举报提交失败:', error); |
| message.error('举报提交失败,请稍后重试'); |
| } finally { |
| setSubmittingReport(false); |
| } |
| }; |
| |
| if (loading) { |
| return ( |
| <div className={styles.postDetailLoading}> |
| <Spin size="large" /> |
| <div style={{ marginTop: 16 }}>加载中...</div> |
| </div> |
| ); |
| } |
| |
| if (!post) { |
| return <div className={styles.postDetailError}>帖子不存在或已被删除</div>; |
| } |
| |
| return ( |
| <div className={styles.postDetailContainer}> |
| {/* 帖子封面图片 */} |
| {post.coverImage && ( |
| <div className={styles.postCoverSection}> |
| <div className={styles.coverImageContainer}> |
| <Image |
| src={post.coverImage} |
| alt={post.title} |
| className={styles.coverImage} |
| preview={{ |
| mask: <div className={styles.previewMask}>点击预览</div> |
| }} |
| onError={(e) => { |
| e.currentTarget.src = '/images/404.png'; |
| }} |
| /> |
| <div className={styles.coverOverlay}> |
| <div className={styles.coverGradient}></div> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| {/* 帖子头部信息 */} |
| <Card className={styles.postDetailHeader}> |
| <div className={styles.titleContainer}> |
| <Title level={2}>{post.title}</Title> |
| {post.isPromoted && ( |
| <div className={styles.promotionBadge}> |
| <CrownOutlined /> |
| <span>推广</span> |
| </div> |
| )} |
| </div> |
| |
| <div className={styles.postMeta}> |
| <div className={styles.postAuthor}> |
| <Avatar size="small" icon={<UserOutlined />} /> |
| <Text strong style={{ marginLeft: 8 }}>{post.author}</Text> |
| </div> |
| |
| <div className={styles.postTime}> |
| <ClockCircleOutlined /> |
| <Text type="secondary" style={{ marginLeft: 8 }}>{post.publishTime}</Text> |
| </div> |
| |
| <div className={styles.postViews}> |
| <EyeOutlined /> |
| <Text type="secondary" style={{ marginLeft: 8 }}>{post.views} 查看</Text> |
| </div> |
| |
| <div className={styles.postTags}> |
| <TagOutlined /> |
| <span style={{ marginLeft: 8 }}> |
| {post.tags.map(tag => ( |
| <Tag key={tag} color="blue">{tag}</Tag> |
| ))} |
| </span> |
| </div> |
| </div> |
| |
| {/* 操作按钮 */} |
| <div className={styles.postActions}> |
| <Button |
| type={favorited ? "primary" : "default"} |
| icon={favorited ? <HeartFilled /> : <HeartOutlined />} |
| onClick={handleFavoriteToggle} |
| > |
| {favorited ? '已收藏' : '收藏'} ({post.favorites || 0}) |
| </Button> |
| <Button |
| type={liked ? "primary" : "default"} |
| icon={<LikeOutlined />} |
| onClick={handleLikeToggle} |
| > |
| {liked ? '已点赞' : '点赞'} ({post.likes || 0}) |
| </Button> |
| {/* <Button icon={<ShareAltOutlined />}> |
| 分享 |
| </Button> */} |
| <Button |
| icon={<ExclamationCircleOutlined />} |
| onClick={() => setReportModalVisible(true)} |
| > |
| 举报 |
| </Button> |
| </div> |
| </Card> |
| |
| {/* 帖子内容 */} |
| <Card className={styles.postContent}> |
| <div dangerouslySetInnerHTML={{ __html: post.content || post.summary || '' }} /> |
| </Card> |
| |
| {/* 评论区 */} |
| <Card className={styles.commentSection}> |
| <Title level={4}>评论 ({comments.length})</Title> |
| |
| <div className={styles.commentInput}> |
| <TextArea |
| value={commentText} |
| onChange={e => setCommentText(e.target.value)} |
| placeholder={replyTo ? '回复评论...' : '写下你的评论...'} |
| rows={4} |
| /> |
| <div className={styles.commentActions}> |
| {replyTo && ( |
| <Button onClick={cancelReply}>取消回复</Button> |
| )} |
| <Button |
| type="primary" |
| onClick={handleCommentSubmit} |
| loading={submittingComment} |
| > |
| 发表{replyTo ? '回复' : '评论'} |
| </Button> |
| </div> |
| </div> |
| |
| <List |
| className={styles.commentList} |
| itemLayout="horizontal" |
| dataSource={comments} |
| renderItem={comment => ( |
| <li> |
| <Comment |
| author={comment.author} |
| avatar={comment.avatar} |
| content={comment.content} |
| datetime={comment.datetime} |
| actions={[ |
| <span key="like"> |
| <Button |
| type={commentLikes[comment.id] ? "primary" : "text"} |
| size="small" |
| icon={<LikeOutlined />} |
| onClick={() => handleCommentLike(comment.id)} |
| > |
| {comment.likes || 0} |
| </Button> |
| </span>, |
| <span key="reply" onClick={() => handleReply(comment.id)}>回复</span> |
| ]} |
| > |
| {comment.replies && comment.replies.length > 0 && ( |
| <List |
| className={styles.replyList} |
| itemLayout="horizontal" |
| dataSource={comment.replies} |
| renderItem={reply => ( |
| <li> |
| <Comment |
| author={reply.author} |
| avatar={reply.avatar} |
| content={reply.content} |
| datetime={reply.datetime} |
| actions={[ |
| <span key="like"> |
| <Button |
| type={commentLikes[reply.id] ? "primary" : "text"} |
| size="small" |
| icon={<LikeOutlined />} |
| onClick={() => handleCommentLike(reply.id)} |
| > |
| {reply.likes || 0} |
| </Button> |
| </span> |
| ]} |
| /> |
| </li> |
| )} |
| /> |
| )} |
| </Comment> |
| </li> |
| )} |
| /> |
| </Card> |
| |
| {/* 举报弹窗 */} |
| <Modal |
| title="举报帖子" |
| open={reportModalVisible} |
| onOk={handleReport} |
| onCancel={() => { |
| setReportModalVisible(false); |
| setReportReason(''); |
| }} |
| confirmLoading={submittingReport} |
| okText="提交举报" |
| cancelText="取消" |
| > |
| <div style={{ marginBottom: 16 }}> |
| <strong>帖子:</strong>{post?.title} |
| </div> |
| <div> |
| <strong>举报理由:</strong> |
| <TextArea |
| value={reportReason} |
| onChange={(e) => setReportReason(e.target.value)} |
| placeholder="请详细描述举报理由..." |
| rows={4} |
| style={{ marginTop: 8 }} |
| /> |
| </div> |
| </Modal> |
| |
| {/* 相关推荐 */} |
| {recommendedPosts.length > 0 && ( |
| <div className={styles.relatedPosts}> |
| <div className={styles.recommendHeader}> |
| <Title level={4}>相关推荐</Title> |
| {recommendedPosts.length > recommendPageSize && ( |
| <Pagination |
| current={currentRecommendPage} |
| total={recommendedPosts.length} |
| pageSize={recommendPageSize} |
| onChange={(page) => setCurrentRecommendPage(page)} |
| showSizeChanger={false} |
| showQuickJumper={false} |
| showTotal={(total, range) => `${range[0]}-${range[1]} / ${total}`} |
| size="small" |
| /> |
| )} |
| </div> |
| <Row gutter={[24, 24]}> |
| {recommendedPosts |
| .slice((currentRecommendPage - 1) * recommendPageSize, currentRecommendPage * recommendPageSize) |
| .map(post => ( |
| <Col xs={24} sm={12} md={8} key={post.id}> |
| <PostCard post={post} /> |
| </Col> |
| ))} |
| </Row> |
| </div> |
| )} |
| </div> |
| ); |
| }; |
| |
| export default PostDetail; |