创作中心模块包含首页展示、个人中心、帖子审核。

“首页展示”支持广告轮播展示、推广帖子优先展示、分页显示所有帖子、导航栏便捷标签筛选帖子、全局标题模糊搜索帖子、点击帖子“查看更多”进入帖子详情页。帖子详情页展示帖子封面图片、作者时间、详细内容(可以插入种子链接对种子进行介绍与推广)等基本信息、对帖子点赞收藏举报评论回复、查看相关推荐帖子。相关推荐会推荐当前帖子作者的其他帖子(最多推荐5篇),还会推荐具有相似标签的其他帖子,两者总共最多推荐9篇帖子。

“个人中心”包含“我的中心”和“我的收藏”。
“我的中心”中可以管理已经成功发布的帖子(编辑、删除帖子),还可以发布新帖子。发布新帖子时除了填写帖子基本信息以外,帖子标签支持下拉多项选择,用户还可以选择帖子推广项目并进行支付。设置了多种推广项目,包含广告轮播推广、帖子置顶展示、限时优先展示、分类页首条展示。系统后台执行自动定时任务,每小时对帖子的推广时效性进行检查,如超出推广时限,则取消帖子的推广显示特权。用户点击发布帖子后帖子处于待审核状态,需要管理员审核通过才能正常发布在首页展示页面。编辑帖子时用户可以追加帖子推广,但如果帖子处于推广状态,则禁止修改推广项目。
“我的收藏”中可以便捷查看所有已收藏的帖子。

“帖子审核”包含“帖子发布管理”和“帖子举报管理”。“帖子审核”板块具有权限管理,只有管理员界面能够进入。
“帖子发布管理”对所有待审核帖子进行处理,支持预览待审核帖子详细内容,批准通过和拒绝通过选项。
“帖子举报管理”对所有用户的举报请求进行人工审核,如果举报内容属实,则将帖子下架处理,如果举报内容不属实,驳回举报请求。所有举报请求的处理结果均留存显示,方便后续再次审查。

Change-Id: If822351183e9d55a5a56ff5cf1e13b313fdbe231
diff --git a/src/pages/PostCenter/PostDetail.tsx b/src/pages/PostCenter/PostDetail.tsx
new file mode 100644
index 0000000..c6aedcc
--- /dev/null
+++ b/src/pages/PostCenter/PostDetail.tsx
@@ -0,0 +1,547 @@
+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; 
\ No newline at end of file