创作中心模块包含首页展示、个人中心、帖子审核。
“首页展示”支持广告轮播展示、推广帖子优先展示、分页显示所有帖子、导航栏便捷标签筛选帖子、全局标题模糊搜索帖子、点击帖子“查看更多”进入帖子详情页。帖子详情页展示帖子封面图片、作者时间、详细内容(可以插入种子链接对种子进行介绍与推广)等基本信息、对帖子点赞收藏举报评论回复、查看相关推荐帖子。相关推荐会推荐当前帖子作者的其他帖子(最多推荐5篇),还会推荐具有相似标签的其他帖子,两者总共最多推荐9篇帖子。
“个人中心”包含“我的中心”和“我的收藏”。
“我的中心”中可以管理已经成功发布的帖子(编辑、删除帖子),还可以发布新帖子。发布新帖子时除了填写帖子基本信息以外,帖子标签支持下拉多项选择,用户还可以选择帖子推广项目并进行支付。设置了多种推广项目,包含广告轮播推广、帖子置顶展示、限时优先展示、分类页首条展示。系统后台执行自动定时任务,每小时对帖子的推广时效性进行检查,如超出推广时限,则取消帖子的推广显示特权。用户点击发布帖子后帖子处于待审核状态,需要管理员审核通过才能正常发布在首页展示页面。编辑帖子时用户可以追加帖子推广,但如果帖子处于推广状态,则禁止修改推广项目。
“我的收藏”中可以便捷查看所有已收藏的帖子。
“帖子审核”包含“帖子发布管理”和“帖子举报管理”。“帖子审核”板块具有权限管理,只有管理员界面能够进入。
“帖子发布管理”对所有待审核帖子进行处理,支持预览待审核帖子详细内容,批准通过和拒绝通过选项。
“帖子举报管理”对所有用户的举报请求进行人工审核,如果举报内容属实,则将帖子下架处理,如果举报内容不属实,驳回举报请求。所有举报请求的处理结果均留存显示,方便后续再次审查。
Change-Id: If822351183e9d55a5a56ff5cf1e13b313fdbe231
diff --git a/src/pages/PostCenter/PostCard.module.css b/src/pages/PostCenter/PostCard.module.css
new file mode 100644
index 0000000..3f0623f
--- /dev/null
+++ b/src/pages/PostCenter/PostCard.module.css
@@ -0,0 +1,192 @@
+.postCardWrapper {
+ width: 100%;
+ height: 100%;
+}
+
+.postCard {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ background: white;
+}
+
+.postCard:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+}
+
+.coverContainer {
+ width: 100%;
+ height: 200px;
+ overflow: hidden;
+ position: relative;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.coverImage {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center;
+ transition: transform 0.3s ease;
+ background: #f5f5f5;
+}
+
+.postCard:hover .coverImage {
+ transform: scale(1.02);
+}
+
+.promotionBadge {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: linear-gradient(45deg, #ff6b6b, #ffa500);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 11px;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ z-index: 10;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.cardContent {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.postTitle {
+ font-size: 16px;
+ font-weight: 600;
+ margin: 0 0 12px 0;
+ line-height: 1.4;
+ color: #262626;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ height: 44px;
+}
+
+.postMeta {
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+ color: #8c8c8c;
+ margin-bottom: 12px;
+ height: 20px;
+}
+
+.authorName {
+ font-weight: 500;
+ color: #595959;
+}
+
+.publishTime {
+ color: #8c8c8c;
+}
+
+.tagsContainer {
+ margin-bottom: 12px;
+ height: 24px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ overflow: hidden;
+}
+
+.tag {
+ font-size: 11px;
+ padding: 2px 6px;
+ margin: 0;
+ border-radius: 4px;
+}
+
+.postSummary {
+ flex: 1;
+ font-size: 13px;
+ color: #595959;
+ line-height: 1.5;
+ margin-bottom: 16px;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ height: 60px;
+}
+
+.postFooter {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: auto;
+ padding-top: 12px;
+ border-top: 1px solid #f0f0f0;
+ height: 32px;
+}
+
+.stats {
+ display: flex;
+ gap: 16px;
+}
+
+.statItem {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ color: #8c8c8c;
+}
+
+.statItem .anticon {
+ font-size: 12px;
+}
+
+.readMoreBtn {
+ padding: 0;
+ font-size: 12px;
+ color: #1890ff;
+ font-weight: 500;
+}
+
+.readMoreBtn:hover {
+ color: #40a9ff;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .coverContainer {
+ height: 160px;
+ }
+
+ .postTitle {
+ font-size: 14px;
+ height: 40px;
+ }
+
+ .postSummary {
+ font-size: 12px;
+ -webkit-line-clamp: 2;
+ height: 36px;
+ }
+
+ .stats {
+ gap: 12px;
+ }
+
+ .statItem {
+ font-size: 11px;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/PostCenter/PostCard.tsx b/src/pages/PostCenter/PostCard.tsx
new file mode 100644
index 0000000..947ecc3
--- /dev/null
+++ b/src/pages/PostCenter/PostCard.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import { Card, Tag, Button, Avatar, Badge } from 'antd';
+import { EyeOutlined, CommentOutlined, UserOutlined, ClockCircleOutlined, HeartOutlined, CrownOutlined } from '@ant-design/icons';
+import { history } from 'umi';
+import styles from './PostCard.module.css';
+import { Post } from '../PostCenter/types';
+
+interface PostCardProps {
+ post: Post;
+}
+
+const PostCard: React.FC<PostCardProps> = ({ post }) => {
+ const {
+ id,
+ title,
+ author,
+ publishTime,
+ tags,
+ views,
+ comments,
+ favorites,
+ likes,
+ coverImage,
+ summary,
+ promotionPlanId,
+ isPromoted
+ } = post;
+
+ const goToDetail = () => {
+ history.push(`/post-detail/${id}`);
+ };
+
+ return (
+ <div className={styles.postCardWrapper}>
+ <Card
+ hoverable
+ cover={
+ <div className={styles.coverContainer}>
+ {isPromoted && (
+ <div className={styles.promotionBadge}>
+ <CrownOutlined />
+ <span>推广</span>
+ </div>
+ )}
+ <img
+ alt={title}
+ src={coverImage}
+ className={styles.coverImage}
+ onError={(e) => {
+ e.currentTarget.src = '/images/404.png';
+ }}
+ />
+ </div>
+ }
+ className={styles.postCard}
+ bodyStyle={{ padding: '16px', height: '240px', display: 'flex', flexDirection: 'column' }}
+ >
+ <div className={styles.cardContent}>
+ <h3 className={styles.postTitle} title={title}>{title}</h3>
+
+ <div className={styles.postMeta}>
+ <Avatar size="small" style={{ marginRight: 6 }} icon={<UserOutlined />}>
+ {author && author[0]}
+ </Avatar>
+ <span className={styles.authorName}>{author}</span>
+ <ClockCircleOutlined style={{ marginLeft: 12, marginRight: 4 }} />
+ <span className={styles.publishTime}>{publishTime}</span>
+ </div>
+
+ <div className={styles.tagsContainer}>
+ {(Array.isArray(tags) ? tags : []).slice(0, 3).map(tag => (
+ <Tag color="blue" key={tag} className={styles.tag}>{tag}</Tag>
+ ))}
+ {tags && tags.length > 3 && (
+ <Tag color="default" className={styles.tag}>+{tags.length - 3}</Tag>
+ )}
+ </div>
+
+ <div className={styles.postSummary} title={summary}>{summary}</div>
+
+ <div className={styles.postFooter}>
+ <div className={styles.stats}>
+ <span className={styles.statItem}>
+ <EyeOutlined /> {views || 0}
+ </span>
+ <span className={styles.statItem}>
+ <CommentOutlined /> {comments || 0}
+ </span>
+ <span className={styles.statItem}>
+ <HeartOutlined /> {favorites || 0}
+ </span>
+ </div>
+ <Button
+ type="link"
+ className={styles.readMoreBtn}
+ onClick={goToDetail}
+ >
+ 查看更多 »
+ </Button>
+ </div>
+ </div>
+ </Card>
+ </div>
+ );
+};
+
+export default PostCard;
\ No newline at end of file
diff --git a/src/pages/PostCenter/PostDetail.module.css b/src/pages/PostCenter/PostDetail.module.css
new file mode 100644
index 0000000..da072d1
--- /dev/null
+++ b/src/pages/PostCenter/PostDetail.module.css
@@ -0,0 +1,233 @@
+.postDetailContainer {
+ max-width: 1200px;
+ margin: 0 auto;
+ background: #f5f5f5;
+ min-height: 100vh;
+}
+
+.postCoverSection {
+ margin-bottom: 24px;
+ border-radius: 0 0 16px 16px;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.coverImageContainer {
+ position: relative;
+ width: 100%;
+ height: 400px;
+ overflow: hidden;
+ background: #f0f0f0;
+}
+
+.coverImage {
+ width: 100% !important;
+ height: 100% !important;
+ object-fit: cover !important;
+ transition: transform 0.3s ease;
+}
+
+.coverImage:hover {
+ transform: scale(1.02);
+}
+
+.coverOverlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+}
+
+.coverGradient {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 100px;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.3));
+}
+
+.previewMask {
+ background: rgba(0, 0, 0, 0.5);
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.postDetailHeader {
+ margin: 0 24px 24px;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.titleContainer {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.titleContainer h2 {
+ margin: 0;
+ flex: 1;
+}
+
+.promotionBadge {
+ background: linear-gradient(45deg, #ff6b6b, #ffa500);
+ color: white;
+ padding: 6px 12px;
+ border-radius: 16px;
+ font-size: 12px;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ white-space: nowrap;
+}
+
+.postMeta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+ margin: 16px 0;
+ padding: 16px 0;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.postAuthor,
+.postTime,
+.postViews,
+.postTags {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.postActions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-top: 16px;
+}
+
+.postContent {
+ margin: 0 24px 24px;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.postContent img {
+ max-width: 100%;
+ height: auto;
+}
+
+.commentSection {
+ margin: 0 24px 24px;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.commentInput {
+ margin-bottom: 24px;
+}
+
+.commentActions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.commentList {
+ margin-top: 24px;
+}
+
+.replyList {
+ margin-top: 16px;
+ padding-left: 24px;
+ border-left: 2px solid #f0f0f0;
+}
+
+.relatedPosts {
+ margin: 0 24px 24px;
+ padding: 24px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.recommendHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+}
+
+.recommendHeader h4 {
+ margin: 0;
+}
+
+.postDetailLoading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ color: #666;
+}
+
+.postDetailError {
+ text-align: center;
+ padding: 60px 24px;
+ color: #999;
+ font-size: 16px;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .postDetailContainer {
+ margin: 0;
+ }
+
+ .coverImageContainer {
+ height: 250px;
+ }
+
+ .postDetailHeader,
+ .postContent,
+ .commentSection,
+ .relatedPosts {
+ margin: 0 12px 16px;
+ border-radius: 8px;
+ }
+
+ .postMeta {
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .postActions {
+ flex-direction: column;
+ }
+
+ .postActions button {
+ width: 100%;
+ }
+}
+
+@media (max-width: 480px) {
+ .coverImageContainer {
+ height: 200px;
+ }
+
+ .postDetailHeader,
+ .postContent,
+ .commentSection,
+ .relatedPosts {
+ margin: 0 8px 12px;
+ }
+}
\ No newline at end of file
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
diff --git a/src/pages/PostCenter/index.module.css b/src/pages/PostCenter/index.module.css
new file mode 100644
index 0000000..2b262e2
--- /dev/null
+++ b/src/pages/PostCenter/index.module.css
@@ -0,0 +1,189 @@
+.postCenterContainer {
+ min-height: 100vh;
+ background-color: #f5f5f5;
+}
+
+.headerNav {
+ background: white;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ padding: 0 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 64px;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.categoryMenu {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.categoryButton {
+ border: none;
+ box-shadow: none;
+ font-weight: 500;
+ padding: 8px 16px;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+}
+
+.categoryButton:hover {
+ background-color: #f0f0f0;
+}
+
+.searchContainer {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ max-width: 400px;
+ margin: 0 24px;
+}
+
+.userCenter {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.carouselContainer {
+ margin: 24px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.carouselSlide {
+ position: relative;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+ min-height: 300px;
+}
+
+.carouselOverlay {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
+ color: white;
+ padding: 40px 40px 24px;
+ text-align: left;
+}
+
+.carouselTitle {
+ font-size: 28px;
+ font-weight: bold;
+ margin: 0 0 12px 0;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+.carouselSummary {
+ font-size: 16px;
+ margin: 0 0 16px 0;
+ opacity: 0.9;
+ line-height: 1.5;
+}
+
+.carouselMeta {
+ display: flex;
+ gap: 24px;
+ font-size: 14px;
+ opacity: 0.8;
+}
+
+.postsSection {
+ padding: 24px;
+}
+
+.categoryTitle {
+ margin-bottom: 24px;
+ text-align: center;
+}
+
+.categoryTitle h2 {
+ font-size: 24px;
+ margin: 0 0 8px 0;
+ color: #1890ff;
+}
+
+.categoryTitle p {
+ margin: 0;
+ color: #666;
+ font-size: 14px;
+}
+
+.postsRow {
+ margin-bottom: 32px;
+}
+
+.postCol {
+ display: flex;
+ height: 480px; /* 固定卡片高度 */
+}
+
+.postCol > * {
+ width: 100%;
+}
+
+.emptyState {
+ text-align: center;
+ padding: 60px 0;
+ color: #999;
+ font-size: 16px;
+}
+
+.paginationContainer {
+ display: flex;
+ justify-content: center;
+ margin-top: 32px;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .headerNav {
+ flex-direction: column;
+ height: auto;
+ padding: 12px;
+ gap: 12px;
+ }
+
+ .categoryMenu {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ .searchContainer {
+ margin: 0;
+ max-width: 100%;
+ }
+
+ .carouselContainer {
+ margin: 12px;
+ }
+
+ .carouselOverlay {
+ padding: 20px;
+ }
+
+ .carouselTitle {
+ font-size: 20px;
+ }
+
+ .carouselSummary {
+ font-size: 14px;
+ }
+
+ .carouselMeta {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .postsSection {
+ padding: 12px;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/PostCenter/index.tsx b/src/pages/PostCenter/index.tsx
new file mode 100644
index 0000000..b81efb0
--- /dev/null
+++ b/src/pages/PostCenter/index.tsx
@@ -0,0 +1,301 @@
+import React, { useEffect, useState } from 'react';
+import { Row, Col, Pagination, Input, Carousel, Menu, Card, message, Button } from 'antd';
+import { SearchOutlined, AuditOutlined } from '@ant-design/icons';
+import { getPostList, getPromotionPosts } from '@/services/post';
+import PostCard from '../PostCenter/PostCard';
+import styles from './index.module.css';
+import { Post } from '../PostCenter/types';
+import { useNavigate } from 'react-router-dom';
+import { useModel } from 'umi';
+
+const { Search } = Input;
+
+const PostCenter: React.FC = () => {
+ const [posts, setPosts] = useState<Post[]>([]);
+ const [promotionPosts, setPromotionPosts] = useState<Post[]>([]);
+ const [total, setTotal] = useState<number>(0);
+ const [page, setPage] = useState<number>(1);
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedCategory, setSelectedCategory] = useState<string>('all');
+ const [searchKeyword, setSearchKeyword] = useState<string>('');
+ const pageSize = 12;
+ const navigate = useNavigate();
+ const { initialState } = useModel('@@initialState');
+
+ // 检查是否为管理员 - 用户名包含admin
+ const isAdmin = initialState?.currentUser?.userName?.toLowerCase().includes('admin') || false;
+
+ const fetchPosts = async (current: number = 1, category?: string, searchTitle?: string) => {
+ try {
+ setLoading(true);
+ const params: any = {
+ pageNum: current,
+ pageSize: pageSize,
+ status: '1', // 只查询正常状态的帖子
+ };
+
+ // 根据分类筛选
+ if (category && category !== 'all') {
+ params.tags = category;
+ }
+
+ // 搜索关键词
+ if (searchTitle) {
+ params.title = searchTitle;
+ }
+
+ const response = await getPostList(params);
+
+ if (response.code === 200) {
+ // 确保返回的数据符合 Post 类型
+ const formattedPosts = (response.rows || []).map((post: API.Post.PostInfo) => ({
+ ...post,
+ id: post.postId, // 确保id字段映射正确
+ tags: post.tags ? post.tags.split(',') : [],
+ views: post.views || 0,
+ comments: post.comments || 0,
+ favorites: post.favorites || 0,
+ likes: post.likes || 0,
+ coverImage: post.coverImage || '/images/404.png', // 使用本地默认图片
+ isPromoted: post.promotionPlanId != null && post.promotionPlanId > 0, // 添加推广标识
+ }));
+ setPosts(formattedPosts);
+ setTotal(response.total || 0);
+ } else {
+ message.error(response.msg || '获取帖子列表失败');
+ setPosts([]);
+ setTotal(0);
+ }
+ } catch (error) {
+ console.error('获取帖子失败:', error);
+ message.error('获取帖子列表失败,请稍后重试');
+ setPosts([]);
+ setTotal(0);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchPromotionPosts = async () => {
+ try {
+ const response = await getPromotionPosts();
+ if (response.code === 200) {
+ const formattedPosts = (response.data || []).map((post: API.Post.PostInfo) => ({
+ ...post,
+ id: post.postId,
+ tags: post.tags ? post.tags.split(',') : [],
+ views: post.views || 0,
+ comments: post.comments || 0,
+ favorites: post.favorites || 0,
+ likes: post.likes || 0,
+ coverImage: post.coverImage || '/images/404.png',
+ isPromoted: post.promotionPlanId != null && post.promotionPlanId > 0, // 添加推广标识
+ }));
+ setPromotionPosts(formattedPosts);
+ }
+ } catch (error) {
+ console.error('获取推广帖子失败:', error);
+ }
+ };
+
+ useEffect(() => {
+ fetchPosts(page, selectedCategory, searchKeyword);
+ fetchPromotionPosts();
+ }, [page, selectedCategory]);
+
+ const handleSearch = (value: string) => {
+ console.log('搜索:', value);
+ setSearchKeyword(value);
+ setPage(1); // 重置页码
+ fetchPosts(1, selectedCategory, value);
+ };
+
+ const handlePageChange = (newPage: number) => {
+ setPage(newPage);
+ fetchPosts(newPage, selectedCategory, searchKeyword);
+ };
+
+ const handleCategoryChange = (category: string) => {
+ setSelectedCategory(category);
+ setPage(1); // 重置页码
+ setSearchKeyword(''); // 清空搜索
+ fetchPosts(1, category, '');
+ };
+
+ return (
+ <div className={styles.postCenterContainer}>
+ {/* 顶部导航 */}
+ <div className={styles.headerNav}>
+ <div className={styles.categoryMenu}>
+ <Button
+ type={selectedCategory === 'all' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('all')}
+ className={styles.categoryButton}
+ >
+ 首页
+ </Button>
+ <Button
+ type={selectedCategory === '日剧' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('日剧')}
+ className={styles.categoryButton}
+ >
+ 日剧
+ </Button>
+ <Button
+ type={selectedCategory === '电影' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('电影')}
+ className={styles.categoryButton}
+ >
+ 电影
+ </Button>
+ <Button
+ type={selectedCategory === '音乐' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('音乐')}
+ className={styles.categoryButton}
+ >
+ 音乐
+ </Button>
+ <Button
+ type={selectedCategory === '合集' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('合集')}
+ className={styles.categoryButton}
+ >
+ 合集
+ </Button>
+ <Button
+ type={selectedCategory === '动漫' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('动漫')}
+ className={styles.categoryButton}
+ >
+ 动漫
+ </Button>
+ <Button
+ type={selectedCategory === '游戏' ? 'primary' : 'text'}
+ onClick={() => handleCategoryChange('游戏')}
+ className={styles.categoryButton}
+ >
+ 游戏
+ </Button>
+ </div>
+
+ <div className={styles.searchContainer}>
+ <Search
+ placeholder="搜索帖子..."
+ onSearch={handleSearch}
+ style={{ width: 300 }}
+ enterButton={<SearchOutlined />}
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ />
+ </div>
+
+ <div className={styles.userCenter}>
+ {isAdmin && (
+ <Button
+ icon={<AuditOutlined />}
+ onClick={() => navigate('/post-review')}
+ style={{ marginRight: 16 }}
+ >
+ 帖子审核
+ </Button>
+ )}
+ <Button
+ type="primary"
+ onClick={() => navigate('/user-center')}
+ >
+ 个人中心
+ </Button>
+ </div>
+ </div>
+
+ {/* 轮播推荐图 */}
+ <div className={styles.carouselContainer}>
+ <Carousel autoplay>
+ {promotionPosts.length > 0 ? (
+ promotionPosts.map((post) => (
+ <div key={post.id} onClick={() => navigate(`/post-detail/${post.id}`)}>
+ <div
+ className={styles.carouselSlide}
+ style={{
+ backgroundImage: `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url(${post.coverImage})`,
+ backgroundSize: 'cover',
+ backgroundPosition: 'center',
+ height: '300px',
+ position: 'relative',
+ cursor: 'pointer'
+ }}
+ >
+ <div className={styles.carouselOverlay}>
+ <h2 className={styles.carouselTitle}>{post.title}</h2>
+ <p className={styles.carouselSummary}>{post.summary}</p>
+ <div className={styles.carouselMeta}>
+ <span>作者: {post.author}</span>
+ <span>浏览: {post.views}</span>
+ <span>点赞: {post.likes}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ ))
+ ) : (
+ // 默认轮播图
+ <>
+ <div>
+ <div
+ className={styles.carouselSlide}
+ style={{
+ backgroundImage: `url(/images/flower.jpg)`,
+ backgroundSize: 'cover',
+ backgroundPosition: 'center',
+ height: '300px',
+ }}
+ >
+ <div className={styles.carouselOverlay}>
+ <h2 className={styles.carouselTitle}>欢迎来到ThunderHub</h2>
+ <p className={styles.carouselSummary}>发现精彩内容,分享美好时光</p>
+ </div>
+ </div>
+ </div>
+ </>
+ )}
+ </Carousel>
+ </div>
+
+ {/* 卡片帖子区 */}
+ <div className={styles.postsSection}>
+ {selectedCategory !== 'all' && (
+ <div className={styles.categoryTitle}>
+ <h2>{selectedCategory} 分类</h2>
+ <p>共找到 {total} 篇相关帖子</p>
+ </div>
+ )}
+
+ <Row gutter={[24, 24]} className={styles.postsRow}>
+ {posts.map((post) => (
+ <Col xs={24} sm={12} lg={8} xl={6} key={post.id} className={styles.postCol}>
+ <PostCard post={post} />
+ </Col>
+ ))}
+ </Row>
+
+ {posts.length === 0 && !loading && (
+ <div className={styles.emptyState}>
+ <p>暂无相关帖子</p>
+ </div>
+ )}
+
+ <div className={styles.paginationContainer}>
+ <Pagination
+ current={page}
+ pageSize={pageSize}
+ total={total}
+ onChange={handlePageChange}
+ showTotal={(total) => `共 ${total} 条帖子`}
+ />
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default PostCenter;
\ No newline at end of file
diff --git a/src/pages/PostCenter/types.ts b/src/pages/PostCenter/types.ts
new file mode 100644
index 0000000..4cfe237
--- /dev/null
+++ b/src/pages/PostCenter/types.ts
@@ -0,0 +1,55 @@
+export interface Post {
+ postId?: number;
+ id: string | number;
+ title: string;
+ author: string;
+ publishTime: string;
+ tags: string[];
+ views: number;
+ comments?: number;
+ favorites?: number;
+ likes?: number;
+ coverImage?: string;
+ summary?: string;
+ content?: string;
+ status?: string;
+ authorId?: number;
+ createTime?: string;
+ updateTime?: string;
+ promotionPlanId?: number;
+ isPromoted?: boolean;
+}
+
+export interface CommentType {
+ commentId?: number;
+ id: number;
+ author: string;
+ avatar: string;
+ content: string;
+ datetime: string;
+ replies: CommentType[];
+ likes?: number;
+ parentId?: number;
+ postId?: number;
+ userId?: number;
+ userName?: string;
+ userAvatar?: string;
+ status?: string;
+}
+
+export interface PostTag {
+ tagId: number;
+ tagName: string;
+ tagColor?: string;
+ postCount: number;
+ status?: string;
+}
+
+export interface PostFavorite {
+ favoriteId: number;
+ postId: number;
+ userId: number;
+ postTitle?: string;
+ postCover?: string;
+ status: string;
+}
\ No newline at end of file
diff --git a/src/pages/PostReview/ReportManagement.tsx b/src/pages/PostReview/ReportManagement.tsx
new file mode 100644
index 0000000..f5c0e99
--- /dev/null
+++ b/src/pages/PostReview/ReportManagement.tsx
@@ -0,0 +1,325 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Card,
+ Table,
+ Button,
+ Modal,
+ Tag,
+ Space,
+ message,
+ Typography,
+ Input
+} from 'antd';
+import {
+ EyeOutlined,
+ CheckOutlined,
+ CloseOutlined
+} from '@ant-design/icons';
+import { getReportList, handleReport } from '@/services/post';
+import styles from './index.module.css';
+
+const { Title, Paragraph } = Typography;
+const { TextArea } = Input;
+
+interface ReportInfo {
+ reportId: number;
+ postId: number;
+ postTitle: string;
+ reportUserId: number;
+ reportUserName: string;
+ reportReason: string;
+ status: string;
+ handleResult?: string;
+ handleTime?: string;
+ handleBy?: string;
+ createTime: string;
+}
+
+const ReportManagement: React.FC = () => {
+ const [reports, setReports] = useState<ReportInfo[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [detailModalVisible, setDetailModalVisible] = useState(false);
+ const [handleModalVisible, setHandleModalVisible] = useState(false);
+ const [currentReport, setCurrentReport] = useState<ReportInfo | null>(null);
+ const [currentAction, setCurrentAction] = useState<'approve' | 'reject' | null>(null);
+ const [handleReason, setHandleReason] = useState('');
+
+ useEffect(() => {
+ fetchReports();
+ }, []);
+
+ const fetchReports = async () => {
+ setLoading(true);
+ try {
+ const response = await getReportList({
+ pageNum: 1,
+ pageSize: 100
+ });
+
+ if (response.code === 200) {
+ setReports(response.rows || []);
+ } else {
+ message.error(response.msg || '获取举报列表失败');
+ }
+ } catch (error) {
+ message.error('获取举报列表失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleReportAction = async (report: ReportInfo, action: 'approve' | 'reject') => {
+ setCurrentReport(report);
+ setCurrentAction(action);
+ setHandleReason('');
+ setHandleModalVisible(true);
+ };
+
+ const submitHandle = async () => {
+ if (!currentReport || !currentAction) return;
+
+ try {
+ const response = await handleReport(
+ currentReport.reportId,
+ currentAction,
+ currentReport.postId,
+ handleReason
+ );
+
+ if (response.code === 200) {
+ message.success(currentAction === 'approve' ? '举报处理成功,帖子已下架' : '举报已驳回');
+ fetchReports();
+ } else {
+ message.error(response.msg || '处理失败');
+ }
+ } catch (error) {
+ message.error('处理失败');
+ }
+
+ setHandleModalVisible(false);
+ setCurrentReport(null);
+ setCurrentAction(null);
+ setHandleReason('');
+ };
+
+ const showDetail = (report: ReportInfo) => {
+ setCurrentReport(report);
+ setDetailModalVisible(true);
+ };
+
+ const columns = [
+ {
+ title: '举报帖子',
+ dataIndex: 'postTitle',
+ key: 'postTitle',
+ width: 180,
+ render: (text: string, record: ReportInfo) => (
+ <a onClick={() => showDetail(record)} style={{ color: '#1890ff' }}>
+ {text}
+ </a>
+ ),
+ },
+ {
+ title: '举报人',
+ dataIndex: 'reportUserName',
+ key: 'reportUserName',
+ width: 100,
+ },
+ {
+ title: '举报时间',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ width: 140,
+ },
+ {
+ title: '举报理由',
+ dataIndex: 'reportReason',
+ key: 'reportReason',
+ width: 150,
+ ellipsis: true,
+ render: (text: string) => (
+ <span title={text}>{text}</span>
+ ),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 80,
+ render: (status: string) => {
+ const statusMap: Record<string, { color: string; text: string }> = {
+ '0': { color: 'orange', text: '待处理' },
+ '1': { color: 'green', text: '已处理' },
+ '2': { color: 'red', text: '已驳回' }
+ };
+ const statusInfo = statusMap[status] || { color: 'gray', text: '未知' };
+ return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
+ },
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 300,
+ fixed: 'right' as const,
+ render: (text: any, record: ReportInfo) => (
+ <Space size="small">
+ <Button
+ type="link"
+ icon={<EyeOutlined />}
+ onClick={() => showDetail(record)}
+ size="small"
+ >
+ 查看
+ </Button>
+ {record.status === '0' && (
+ <>
+ <Button
+ type="link"
+ icon={<CheckOutlined />}
+ style={{ color: 'green' }}
+ onClick={() => handleReportAction(record, 'approve')}
+ size="small"
+ >
+ 确认下架
+ </Button>
+ <Button
+ type="link"
+ danger
+ icon={<CloseOutlined />}
+ onClick={() => handleReportAction(record, 'reject')}
+ size="small"
+ >
+ 驳回举报
+ </Button>
+ </>
+ )}
+ </Space>
+ ),
+ },
+ ];
+
+ return (
+ <div className={styles.postReviewContainer}>
+ <Card title="帖子举报管理">
+ <Table
+ columns={columns}
+ dataSource={reports}
+ loading={loading}
+ rowKey="reportId"
+ scroll={{ x: 880 }}
+ pagination={{
+ pageSize: 10,
+ showTotal: (total) => `共 ${total} 条记录`,
+ showSizeChanger: true,
+ showQuickJumper: true,
+ }}
+ />
+ </Card>
+
+ {/* 举报详情弹窗 */}
+ <Modal
+ title="举报详情"
+ open={detailModalVisible}
+ onCancel={() => {
+ setDetailModalVisible(false);
+ setCurrentReport(null);
+ }}
+ footer={null}
+ width={600}
+ >
+ {currentReport && (
+ <div>
+ <div style={{ marginBottom: 16 }}>
+ <strong>举报帖子:</strong>{currentReport.postTitle}
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <strong>举报人:</strong>{currentReport.reportUserName}
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <strong>举报时间:</strong>{currentReport.createTime}
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <strong>举报理由:</strong>
+ <Paragraph>{currentReport.reportReason}</Paragraph>
+ </div>
+ {currentReport.status !== '0' && (
+ <>
+ <div style={{ marginBottom: 16 }}>
+ <strong>处理结果:</strong>{currentReport.handleResult}
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <strong>处理时间:</strong>{currentReport.handleTime}
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <strong>处理人:</strong>{currentReport.handleBy}
+ </div>
+ </>
+ )}
+ {currentReport.status === '0' && (
+ <div style={{ marginTop: 16 }}>
+ <Space>
+ <Button
+ type="primary"
+ icon={<CheckOutlined />}
+ onClick={() => {
+ handleReportAction(currentReport, 'approve');
+ setDetailModalVisible(false);
+ }}
+ >
+ 确认下架
+ </Button>
+ <Button
+ danger
+ icon={<CloseOutlined />}
+ onClick={() => {
+ handleReportAction(currentReport, 'reject');
+ setDetailModalVisible(false);
+ }}
+ >
+ 驳回举报
+ </Button>
+ </Space>
+ </div>
+ )}
+ </div>
+ )}
+ </Modal>
+
+ {/* 处理举报弹窗 */}
+ <Modal
+ title={currentAction === 'approve' ? '确认下架帖子' : '驳回举报'}
+ open={handleModalVisible}
+ onOk={submitHandle}
+ onCancel={() => {
+ setHandleModalVisible(false);
+ setCurrentReport(null);
+ setCurrentAction(null);
+ setHandleReason('');
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+ <div style={{ marginBottom: 16 }}>
+ <strong>帖子:</strong>{currentReport?.postTitle}
+ </div>
+ <div>
+ <strong>
+ {currentAction === 'approve' ? '下架理由' : '驳回理由'}
+ (可选):
+ </strong>
+ <TextArea
+ value={handleReason}
+ onChange={(e) => setHandleReason(e.target.value)}
+ placeholder={
+ currentAction === 'approve' ? '请输入下架理由...' : '请输入驳回理由...'
+ }
+ rows={4}
+ style={{ marginTop: 8 }}
+ />
+ </div>
+ </Modal>
+ </div>
+ );
+};
+
+export default ReportManagement;
\ No newline at end of file
diff --git a/src/pages/PostReview/index.module.css b/src/pages/PostReview/index.module.css
new file mode 100644
index 0000000..887b367
--- /dev/null
+++ b/src/pages/PostReview/index.module.css
@@ -0,0 +1,50 @@
+.postReviewContainer {
+ padding: 24px;
+}
+
+.postDetail {
+ padding: 16px 0;
+}
+
+.postMeta {
+ margin: 16px 0;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+}
+
+.postMeta p {
+ margin: 8px 0;
+}
+
+.postCover {
+ margin: 16px 0;
+ text-align: center;
+}
+
+.postTags {
+ margin: 16px 0;
+}
+
+.postSummary {
+ margin: 16px 0;
+ padding: 16px;
+ background: #f9f9f9;
+ border-radius: 8px;
+}
+
+.postContent {
+ margin: 16px 0;
+ padding: 16px;
+ border: 1px solid #e8e8e8;
+ border-radius: 8px;
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.postActions {
+ margin-top: 24px;
+ text-align: center;
+ padding-top: 16px;
+ border-top: 1px solid #e8e8e8;
+}
\ No newline at end of file
diff --git a/src/pages/PostReview/index.tsx b/src/pages/PostReview/index.tsx
new file mode 100644
index 0000000..d3d6036
--- /dev/null
+++ b/src/pages/PostReview/index.tsx
@@ -0,0 +1,394 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Card,
+ Table,
+ Button,
+ Modal,
+ Tag,
+ Space,
+ message,
+ Popconfirm,
+ Typography,
+ Image,
+ Row,
+ Col,
+ Input,
+ Tabs
+} from 'antd';
+import {
+ EyeOutlined,
+ CheckOutlined,
+ CloseOutlined,
+ DeleteOutlined
+} from '@ant-design/icons';
+import { getReviewPosts, reviewPost, takeDownPost } from '@/services/post';
+import ReportManagement from './ReportManagement';
+import styles from './index.module.css';
+
+const { Title, Paragraph } = Typography;
+const { TextArea } = Input;
+const { TabPane } = Tabs;
+
+interface PostReviewProps {}
+
+const PostReview: React.FC<PostReviewProps> = () => {
+ const [activeTab, setActiveTab] = useState('review');
+ const [posts, setPosts] = useState<API.Post.PostInfo[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [detailModalVisible, setDetailModalVisible] = useState(false);
+ const [reasonModalVisible, setReasonModalVisible] = useState(false);
+ const [currentPost, setCurrentPost] = useState<API.Post.PostInfo | null>(null);
+ const [currentAction, setCurrentAction] = useState<'approve' | 'reject' | 'takedown' | null>(null);
+ const [reason, setReason] = useState('');
+
+ useEffect(() => {
+ if (activeTab === 'review') {
+ fetchPendingPosts();
+ }
+ }, [activeTab]);
+
+ const fetchPendingPosts = async () => {
+ setLoading(true);
+ try {
+ const response = await getReviewPosts({
+ pageNum: 1,
+ pageSize: 100,
+ status: '0' // 只查询待审核的帖子
+ });
+
+ if (response.code === 200) {
+ setPosts(response.rows || []);
+ } else {
+ message.error(response.msg || '获取待审核帖子失败');
+ }
+ } catch (error) {
+ message.error('获取待审核帖子失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAction = async (postId: number, action: 'approve' | 'reject', reason?: string) => {
+ try {
+ const response = await reviewPost(postId, action, reason);
+
+ if (response.code === 200) {
+ message.success(action === 'approve' ? '帖子审核通过' : '帖子已拒绝');
+ fetchPendingPosts();
+ } else {
+ message.error(response.msg || '操作失败');
+ }
+ } catch (error) {
+ message.error('操作失败');
+ }
+ };
+
+ const handleTakeDown = async (postId: number, reason?: string) => {
+ try {
+ const response = await takeDownPost(postId, reason);
+
+ if (response.code === 200) {
+ message.success('帖子已下架');
+ fetchPendingPosts();
+ } else {
+ message.error(response.msg || '操作失败');
+ }
+ } catch (error) {
+ message.error('操作失败');
+ }
+ };
+
+ const showReasonModal = (post: API.Post.PostInfo, action: 'approve' | 'reject' | 'takedown') => {
+ setCurrentPost(post);
+ setCurrentAction(action);
+ setReason('');
+ setReasonModalVisible(true);
+ };
+
+ const handleReasonSubmit = () => {
+ if (!currentPost || !currentAction) return;
+
+ if (currentAction === 'takedown') {
+ handleTakeDown(currentPost.postId || 0, reason);
+ } else {
+ handleAction(currentPost.postId || 0, currentAction, reason);
+ }
+
+ setReasonModalVisible(false);
+ setCurrentPost(null);
+ setCurrentAction(null);
+ setReason('');
+ };
+
+ const handleViewDetail = (post: API.Post.PostInfo) => {
+ setCurrentPost(post);
+ setDetailModalVisible(true);
+ };
+
+ const columns = [
+ {
+ title: '帖子标题',
+ dataIndex: 'title',
+ key: 'title',
+ width: 200,
+ render: (text: string, record: API.Post.PostInfo) => (
+ <a onClick={() => handleViewDetail(record)}>{text}</a>
+ ),
+ },
+ {
+ title: '作者',
+ dataIndex: 'author',
+ key: 'author',
+ width: 100,
+ },
+ {
+ title: '发布时间',
+ dataIndex: 'publishTime',
+ key: 'publishTime',
+ width: 150,
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 100,
+ render: (status: string) => {
+ const statusMap: Record<string, { color: string; text: string }> = {
+ '0': { color: 'orange', text: '待审核' },
+ '1': { color: 'green', text: '已发布' },
+ '2': { color: 'red', text: '已拒绝' },
+ '3': { color: 'gray', text: '已下架' }
+ };
+ const statusInfo = statusMap[status] || { color: 'gray', text: '未知' };
+ return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
+ },
+ },
+ {
+ title: '标签',
+ dataIndex: 'tags',
+ key: 'tags',
+ width: 150,
+ render: (tags: string) => {
+ if (!tags) return '-';
+ return tags.split(',').map(tag => (
+ <Tag key={tag} color="blue">{tag}</Tag>
+ ));
+ },
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 250,
+ render: (text: any, record: API.Post.PostInfo) => (
+ <Space size="small">
+ <Button
+ type="link"
+ icon={<EyeOutlined />}
+ onClick={() => handleViewDetail(record)}
+ >
+ 查看
+ </Button>
+ {record.status === '0' && (
+ <>
+ <Button
+ type="link"
+ icon={<CheckOutlined />}
+ style={{ color: 'green' }}
+ onClick={() => showReasonModal(record, 'approve')}
+ >
+ 通过
+ </Button>
+ <Button
+ type="link"
+ danger
+ icon={<CloseOutlined />}
+ onClick={() => showReasonModal(record, 'reject')}
+ >
+ 拒绝
+ </Button>
+ </>
+ )}
+ {record.status === '1' && (
+ <Button
+ type="link"
+ danger
+ icon={<DeleteOutlined />}
+ onClick={() => showReasonModal(record, 'takedown')}
+ >
+ 下架
+ </Button>
+ )}
+ </Space>
+ ),
+ },
+ ];
+
+ return (
+ <div className={styles.postReviewContainer}>
+ <Card title="帖子审核管理">
+ <Tabs activeKey={activeTab} onChange={setActiveTab}>
+ <TabPane tab="帖子发布管理" key="review">
+ <Table
+ columns={columns}
+ dataSource={posts}
+ loading={loading}
+ rowKey="postId"
+ pagination={{
+ pageSize: 10,
+ showTotal: (total) => `共 ${total} 条记录`,
+ }}
+ />
+ </TabPane>
+
+ <TabPane tab="帖子举报管理" key="report">
+ <ReportManagement />
+ </TabPane>
+ </Tabs>
+ </Card>
+
+ {/* 帖子详情弹窗 */}
+ <Modal
+ title="帖子详情"
+ open={detailModalVisible}
+ onCancel={() => {
+ setDetailModalVisible(false);
+ setCurrentPost(null);
+ }}
+ footer={null}
+ width={800}
+ >
+ {currentPost && (
+ <div className={styles.postDetail}>
+ <Title level={3}>{currentPost.title}</Title>
+
+ <div className={styles.postMeta}>
+ <Row gutter={16}>
+ <Col span={12}>
+ <p><strong>作者:</strong>{currentPost.author}</p>
+ <p><strong>发布时间:</strong>{currentPost.publishTime}</p>
+ </Col>
+ <Col span={12}>
+ <p><strong>浏览量:</strong>{currentPost.views || 0}</p>
+ <p><strong>点赞数:</strong>{currentPost.likes || 0}</p>
+ </Col>
+ </Row>
+ </div>
+
+ {currentPost.coverImage && (
+ <div className={styles.postCover}>
+ <Image
+ src={currentPost.coverImage}
+ alt="封面图片"
+ style={{ maxWidth: '100%', maxHeight: '200px' }}
+ />
+ </div>
+ )}
+
+ <div className={styles.postTags}>
+ <strong>标签:</strong>
+ {currentPost.tags ? (
+ currentPost.tags.split(',').map(tag => (
+ <Tag key={tag} color="blue">{tag}</Tag>
+ ))
+ ) : (
+ <span>无标签</span>
+ )}
+ </div>
+
+ <div className={styles.postSummary}>
+ <strong>摘要:</strong>
+ <Paragraph>{currentPost.summary}</Paragraph>
+ </div>
+
+ <div className={styles.postContent}>
+ <strong>内容:</strong>
+ <div dangerouslySetInnerHTML={{ __html: currentPost.content || '' }} />
+ </div>
+
+ <div className={styles.postActions}>
+ <Space>
+ {currentPost.status === '0' && (
+ <>
+ <Button
+ type="primary"
+ icon={<CheckOutlined />}
+ onClick={() => {
+ showReasonModal(currentPost, 'approve');
+ setDetailModalVisible(false);
+ }}
+ >
+ 通过审核
+ </Button>
+ <Button
+ danger
+ icon={<CloseOutlined />}
+ onClick={() => {
+ showReasonModal(currentPost, 'reject');
+ setDetailModalVisible(false);
+ }}
+ >
+ 拒绝审核
+ </Button>
+ </>
+ )}
+ {currentPost.status === '1' && (
+ <Button
+ danger
+ icon={<DeleteOutlined />}
+ onClick={() => {
+ showReasonModal(currentPost, 'takedown');
+ setDetailModalVisible(false);
+ }}
+ >
+ 强制下架
+ </Button>
+ )}
+ </Space>
+ </div>
+ </div>
+ )}
+ </Modal>
+
+ {/* 审核理由弹窗 */}
+ <Modal
+ title={
+ currentAction === 'approve' ? '审核通过' :
+ currentAction === 'reject' ? '审核拒绝' : '强制下架'
+ }
+ open={reasonModalVisible}
+ onOk={handleReasonSubmit}
+ onCancel={() => {
+ setReasonModalVisible(false);
+ setCurrentPost(null);
+ setCurrentAction(null);
+ setReason('');
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+ <div style={{ marginBottom: 16 }}>
+ <strong>帖子:</strong>{currentPost?.title}
+ </div>
+ <div>
+ <strong>
+ {currentAction === 'approve' ? '通过理由' :
+ currentAction === 'reject' ? '拒绝理由' : '下架理由'}
+ (可选):
+ </strong>
+ <TextArea
+ value={reason}
+ onChange={(e) => setReason(e.target.value)}
+ placeholder={
+ currentAction === 'approve' ? '请输入审核通过的理由...' :
+ currentAction === 'reject' ? '请输入审核拒绝的理由...' : '请输入强制下架的理由...'
+ }
+ rows={4}
+ style={{ marginTop: 8 }}
+ />
+ </div>
+ </Modal>
+ </div>
+ );
+};
+
+export default PostReview;
\ No newline at end of file
diff --git a/src/pages/UserCenter/index.module.css b/src/pages/UserCenter/index.module.css
new file mode 100644
index 0000000..3728909
--- /dev/null
+++ b/src/pages/UserCenter/index.module.css
@@ -0,0 +1,90 @@
+.userCenterContainer {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 24px;
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+
+.userCenterCard {
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.tabContent {
+ padding: 16px 0;
+}
+
+.tabHeader {
+ margin-bottom: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.emptyState {
+ text-align: center;
+ padding: 60px 0;
+ color: #999;
+}
+
+.emptyState p {
+ margin-top: 16px;
+ font-size: 16px;
+}
+
+.paymentModal {
+ text-align: center;
+}
+
+.paymentInfo {
+ margin-bottom: 24px;
+ padding: 16px;
+ background: #f9f9f9;
+ border-radius: 8px;
+}
+
+.paymentInfo h3 {
+ margin: 0 0 8px 0;
+ color: #1890ff;
+}
+
+.paymentInfo p {
+ margin: 4px 0;
+ color: #666;
+}
+
+.qrCode {
+ margin: 24px 0;
+}
+
+.qrCodePlaceholder {
+ padding: 24px;
+ border: 2px dashed #d9d9d9;
+ border-radius: 8px;
+ background: #fafafa;
+}
+
+.mockQrCode {
+ width: 120px;
+ height: 120px;
+ margin: 16px auto;
+ border: 1px solid #d9d9d9;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ background: #fff;
+}
+
+.mockQrCode p {
+ margin: 4px 0;
+ font-size: 12px;
+ color: #666;
+}
+
+.paymentActions {
+ margin-top: 24px;
+}
\ No newline at end of file
diff --git a/src/pages/UserCenter/index.tsx b/src/pages/UserCenter/index.tsx
new file mode 100644
index 0000000..13de6ad
--- /dev/null
+++ b/src/pages/UserCenter/index.tsx
@@ -0,0 +1,968 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Card,
+ Tabs,
+ Button,
+ Table,
+ Modal,
+ Form,
+ Input,
+ Select,
+ Upload,
+ message,
+ Tag,
+ Space,
+ Popconfirm,
+ Row,
+ Col,
+ Radio,
+ InputNumber,
+ Image
+} from 'antd';
+import {
+ PlusOutlined,
+ EditOutlined,
+ DeleteOutlined,
+ EyeOutlined,
+ UploadOutlined,
+ HeartOutlined,
+ LoadingOutlined
+} from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+import {
+ getMyPosts,
+ getMyFavorites,
+ publishPost,
+ updatePost,
+ deletePost,
+ getAvailableTags,
+ uploadImage,
+ deleteImage,
+ getPromotionPlans,
+ createPayment,
+ getPromotionStatus,
+ confirmPayment,
+ cancelPayment
+} from '@/services/post';
+import PostCard from '../PostCenter/PostCard';
+import styles from './index.module.css';
+
+const { TabPane } = Tabs;
+const { TextArea } = Input;
+const { Option } = Select;
+
+interface PostFormData {
+ title: string;
+ content: string;
+ summary: string;
+ tags: string[] | string;
+ promotionPlan?: number;
+ coverImage?: string;
+}
+
+interface PromotionPlan {
+ id: number;
+ name: string;
+ description: string;
+ price: number;
+ duration: number;
+}
+
+interface PaymentRecord {
+ paymentId: number;
+ postId: number;
+ planId: number;
+ userId: number;
+ amount: number;
+ paymentStatus: string;
+ paymentTime: string;
+}
+
+const UserCenter: React.FC = () => {
+ const navigate = useNavigate();
+ const [activeTab, setActiveTab] = useState('myPosts');
+ const [publishModalVisible, setPublishModalVisible] = useState(false);
+ const [editModalVisible, setEditModalVisible] = useState(false);
+ const [paymentModalVisible, setPaymentModalVisible] = useState(false);
+ const [myPosts, setMyPosts] = useState<API.Post.PostInfo[]>([]);
+ const [favorites, setFavorites] = useState<API.Post.PostInfo[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [form] = Form.useForm();
+ const [editForm] = Form.useForm();
+ const [selectedPromotion, setSelectedPromotion] = useState<PromotionPlan | null>(null);
+ const [currentEditPost, setCurrentEditPost] = useState<API.Post.PostInfo | null>(null);
+ const [availableTags, setAvailableTags] = useState<API.Post.PostTag[]>([]);
+ const [promotionPlans, setPromotionPlans] = useState<PromotionPlan[]>([]);
+ const [uploadLoading, setUploadLoading] = useState(false);
+ const [editUploadLoading, setEditUploadLoading] = useState(false);
+ const [coverImageUrl, setCoverImageUrl] = useState<string>('');
+ const [editCoverImageUrl, setEditCoverImageUrl] = useState<string>('');
+ const [currentPayment, setCurrentPayment] = useState<PaymentRecord | null>(null);
+ const [isEditingPromotion, setIsEditingPromotion] = useState(false);
+
+ useEffect(() => {
+ if (activeTab === 'myPosts') {
+ fetchMyPosts();
+ } else if (activeTab === 'favorites') {
+ fetchFavorites();
+ }
+ }, [activeTab]);
+
+ useEffect(() => {
+ fetchAvailableTags();
+ fetchPromotionPlans();
+ }, []);
+
+ const fetchMyPosts = async () => {
+ setLoading(true);
+ try {
+ const response = await getMyPosts({ pageNum: 1, pageSize: 100 });
+ if (response.code === 200) {
+ setMyPosts(response.rows || []);
+ } else {
+ message.error(response.msg || '获取我的帖子失败');
+ }
+ } catch (error) {
+ message.error('获取我的帖子失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchFavorites = async () => {
+ setLoading(true);
+ try {
+ const response = await getMyFavorites({ pageNum: 1, pageSize: 100 });
+ if (response.code === 200) {
+ setFavorites(response.rows || []);
+ } else {
+ message.error(response.msg || '获取收藏列表失败');
+ }
+ } catch (error) {
+ message.error('获取收藏列表失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchAvailableTags = async () => {
+ try {
+ const response = await getAvailableTags();
+ if (response.code === 200) {
+ setAvailableTags(response.data || []);
+ } else {
+ message.error(response.msg || '获取可用标签失败');
+ }
+ } catch (error) {
+ message.error('获取可用标签失败');
+ }
+ };
+
+ const fetchPromotionPlans = async () => {
+ try {
+ const response = await getPromotionPlans();
+ if (response.code === 200) {
+ setPromotionPlans(response.data || []);
+ }
+ } catch (error) {
+ console.error('获取推广计划失败:', error);
+ }
+ };
+
+ const handlePublishPost = async (values: PostFormData) => {
+ try {
+ if (values.promotionPlan && selectedPromotion) {
+ // 如果选择了推广,创建支付记录
+ const paymentResponse = await createPayment({
+ postId: 0, // 新帖子,暂时设为0,后端会处理
+ planId: selectedPromotion.id,
+ amount: selectedPromotion.price
+ });
+
+ if (paymentResponse.code === 200) {
+ setCurrentPayment(paymentResponse.data);
+ setPaymentModalVisible(true);
+ return;
+ } else {
+ message.error(paymentResponse.msg || '创建支付记录失败');
+ return;
+ }
+ }
+
+ // 直接发布帖子
+ await submitPost(values);
+ } catch (error) {
+ message.error('发布帖子失败');
+ }
+ };
+
+ const submitPost = async (values: PostFormData) => {
+ try {
+ // 处理标签格式
+ const tagsString = Array.isArray(values.tags) ? values.tags.join(',') : values.tags;
+
+ const postData = {
+ title: values.title,
+ content: values.content,
+ summary: values.summary,
+ tags: tagsString,
+ promotionPlan: values.promotionPlan,
+ coverImage: coverImageUrl || undefined
+ };
+
+ const response = await publishPost(postData);
+ if (response.code === 200) {
+ message.success('帖子发布成功');
+ setPublishModalVisible(false);
+ form.resetFields();
+ setSelectedPromotion(null);
+ setCoverImageUrl('');
+ fetchMyPosts();
+ } else {
+ message.error(response.msg || '发布帖子失败');
+ }
+ } catch (error) {
+ message.error('发布帖子失败');
+ }
+ };
+
+ const handleEditPost = async (post: API.Post.PostInfo) => {
+ setCurrentEditPost(post);
+ const tagsArray = post.tags ? (typeof post.tags === 'string' ? post.tags.split(',') : post.tags) : [];
+
+ // 检查推广状态
+ try {
+ const promotionResponse = await getPromotionStatus(post.postId || post.id || 0);
+ if (promotionResponse.code === 200) {
+ const { hasPromotion, promotionPlanId } = promotionResponse.data;
+ setIsEditingPromotion(hasPromotion);
+
+ editForm.setFieldsValue({
+ title: post.title,
+ content: post.content,
+ summary: post.summary,
+ tags: tagsArray,
+ promotionPlan: hasPromotion ? promotionPlanId : undefined
+ });
+ }
+ } catch (error) {
+ console.error('获取推广状态失败:', error);
+ editForm.setFieldsValue({
+ title: post.title,
+ content: post.content,
+ summary: post.summary,
+ tags: tagsArray,
+ promotionPlan: post.promotionPlanId
+ });
+ }
+
+ setEditCoverImageUrl(post.coverImage || '');
+ setEditModalVisible(true);
+ };
+
+ const handleUpdatePost = async (values: any) => {
+ if (!currentEditPost) return;
+
+ try {
+ // 处理标签格式
+ const tagsString = Array.isArray(values.tags) ? values.tags.join(',') : values.tags;
+
+ // 检查是否选择了新的推广计划
+ const hasNewPromotion = values.promotionPlan && !isEditingPromotion;
+
+ if (hasNewPromotion) {
+ // 如果选择了新的推广计划,需要先创建支付记录
+ const selectedPlan = promotionPlans.find(p => p.id === values.promotionPlan);
+ if (selectedPlan) {
+ setSelectedPromotion(selectedPlan);
+
+ // 创建支付记录
+ const paymentResponse = await createPayment({
+ postId: currentEditPost.postId || currentEditPost.id || 0,
+ planId: selectedPlan.id,
+ amount: selectedPlan.price
+ });
+
+ if (paymentResponse.code === 200) {
+ setCurrentPayment(paymentResponse.data);
+ setPaymentModalVisible(true);
+ return; // 等待支付完成后再更新帖子
+ } else {
+ message.error(paymentResponse.msg || '创建支付记录失败');
+ return;
+ }
+ }
+ }
+
+ // 直接更新帖子(没有新推广或已有推广)
+ await updatePostDirectly(values, tagsString);
+ } catch (error) {
+ message.error('更新帖子失败');
+ }
+ };
+
+ const updatePostDirectly = async (values: any, tagsString: string) => {
+ if (!currentEditPost) return;
+
+ const updateData = {
+ ...currentEditPost,
+ title: values.title,
+ content: values.content,
+ summary: values.summary,
+ tags: tagsString,
+ coverImage: editCoverImageUrl || currentEditPost.coverImage,
+ promotionPlanId: values.promotionPlan
+ };
+
+ const response = await updatePost(updateData);
+ if (response.code === 200) {
+ message.success('帖子更新成功');
+ setEditModalVisible(false);
+ editForm.resetFields();
+ setCurrentEditPost(null);
+ setEditCoverImageUrl('');
+ setIsEditingPromotion(false);
+ fetchMyPosts();
+ } else {
+ message.error(response.msg || '更新帖子失败');
+ }
+ };
+
+ const handleDeletePost = async (postId: number) => {
+ try {
+ const response = await deletePost(postId);
+ if (response.code === 200) {
+ message.success('帖子删除成功');
+ fetchMyPosts();
+ } else {
+ message.error(response.msg || '删除帖子失败');
+ }
+ } catch (error) {
+ message.error('删除帖子失败');
+ }
+ };
+
+ const handleViewPost = (postId: number) => {
+ navigate(`/post-detail/${postId}`);
+ };
+
+ const handlePaymentConfirm = async () => {
+ if (!currentPayment) return;
+
+ try {
+ const response = await confirmPayment(currentPayment.paymentId);
+ if (response.code === 200) {
+ message.success('支付成功,推广已生效');
+ setPaymentModalVisible(false);
+ setCurrentPayment(null);
+
+ // 如果是编辑模式,完成帖子更新
+ if (editModalVisible && currentEditPost) {
+ const values = editForm.getFieldsValue();
+ const tagsString = Array.isArray(values.tags) ? values.tags.join(',') : values.tags;
+ await updatePostDirectly(values, tagsString);
+ } else {
+ // 如果是发布模式
+ setPublishModalVisible(false);
+ form.resetFields();
+ setSelectedPromotion(null);
+ setCoverImageUrl('');
+ fetchMyPosts();
+ }
+ } else {
+ message.error(response.msg || '支付确认失败');
+ }
+ } catch (error) {
+ message.error('支付确认失败');
+ }
+ };
+
+ const handlePaymentCancel = async () => {
+ if (!currentPayment) return;
+
+ try {
+ await cancelPayment(currentPayment.paymentId);
+ message.info('支付已取消');
+ setPaymentModalVisible(false);
+ setCurrentPayment(null);
+ setSelectedPromotion(null);
+ } catch (error) {
+ console.error('取消支付失败:', error);
+ setPaymentModalVisible(false);
+ setCurrentPayment(null);
+ setSelectedPromotion(null);
+ }
+ };
+
+ const handleImageUpload = async (file: any) => {
+ setUploadLoading(true);
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await uploadImage(formData);
+ if (response.code === 200 && response.data) {
+ setCoverImageUrl(response.data.url);
+ message.success('图片上传成功');
+ return false; // 阻止自动上传
+ } else {
+ message.error(response.msg || '图片上传失败');
+ }
+ } catch (error) {
+ message.error('图片上传失败');
+ } finally {
+ setUploadLoading(false);
+ }
+ return false;
+ };
+
+ const handleDeleteImage = async () => {
+ if (coverImageUrl) {
+ try {
+ const filename = coverImageUrl.split('/').pop();
+ if (filename) {
+ await deleteImage(filename);
+ }
+ setCoverImageUrl('');
+ message.success('图片删除成功');
+ } catch (error) {
+ message.error('图片删除失败');
+ }
+ }
+ };
+
+ const handleCancelPublish = async () => {
+ // 如果有上传的图片但没有发布帖子,删除图片
+ if (coverImageUrl) {
+ try {
+ const filename = coverImageUrl.split('/').pop();
+ if (filename) {
+ await deleteImage(filename);
+ }
+ } catch (error) {
+ console.error('删除图片失败:', error);
+ }
+ }
+
+ setPublishModalVisible(false);
+ form.resetFields();
+ setSelectedPromotion(null);
+ setCoverImageUrl('');
+ };
+
+ const uploadButton = (
+ <div>
+ {uploadLoading ? <LoadingOutlined /> : <PlusOutlined />}
+ <div style={{ marginTop: 8 }}>上传封面</div>
+ </div>
+ );
+
+ const handleEditImageUpload = async (file: any) => {
+ setEditUploadLoading(true);
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await uploadImage(formData);
+ if (response.code === 200 && response.data) {
+ // 如果有旧图片,删除它
+ if (editCoverImageUrl) {
+ const oldFilename = editCoverImageUrl.split('/').pop();
+ if (oldFilename) {
+ await deleteImage(oldFilename);
+ }
+ }
+
+ setEditCoverImageUrl(response.data.url);
+ message.success('图片上传成功');
+ return false;
+ } else {
+ message.error(response.msg || '图片上传失败');
+ }
+ } catch (error) {
+ message.error('图片上传失败');
+ } finally {
+ setEditUploadLoading(false);
+ }
+ return false;
+ };
+
+ const handleDeleteEditImage = async () => {
+ if (editCoverImageUrl) {
+ try {
+ const filename = editCoverImageUrl.split('/').pop();
+ if (filename) {
+ await deleteImage(filename);
+ }
+ setEditCoverImageUrl('');
+ message.success('图片删除成功');
+ } catch (error) {
+ message.error('图片删除失败');
+ }
+ }
+ };
+
+ const editUploadButton = (
+ <div>
+ {editUploadLoading ? <LoadingOutlined /> : <PlusOutlined />}
+ <div style={{ marginTop: 8 }}>上传封面</div>
+ </div>
+ );
+
+ const myPostsColumns = [
+ {
+ title: '标题',
+ dataIndex: 'title',
+ key: 'title',
+ render: (text: string, record: API.Post.PostInfo) => (
+ <a onClick={() => handleViewPost(record.postId || record.id || 0)}>{text}</a>
+ ),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ render: (status: string) => {
+ const statusMap: Record<string, { color: string; text: string }> = {
+ '0': { color: 'orange', text: '待审核' },
+ '1': { color: 'green', text: '已发布' },
+ '2': { color: 'red', text: '已拒绝' },
+ '3': { color: 'gray', text: '已下架' }
+ };
+ const statusInfo = statusMap[status] || { color: 'gray', text: '未知' };
+ return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
+ },
+ },
+ {
+ title: '浏览量',
+ dataIndex: 'views',
+ key: 'views',
+ },
+ {
+ title: '评论数',
+ dataIndex: 'comments',
+ key: 'comments',
+ },
+ {
+ title: '收藏数',
+ dataIndex: 'favorites',
+ key: 'favorites',
+ },
+ {
+ title: '点赞数',
+ dataIndex: 'likes',
+ key: 'likes',
+ },
+ {
+ title: '发布时间',
+ dataIndex: 'publishTime',
+ key: 'publishTime',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (text: any, record: API.Post.PostInfo) => (
+ <Space size="middle">
+ <Button
+ type="link"
+ icon={<EyeOutlined />}
+ onClick={() => handleViewPost(record.postId || record.id || 0)}
+ >
+ 查看
+ </Button>
+ <Button
+ type="link"
+ icon={<EditOutlined />}
+ onClick={() => handleEditPost(record)}
+ >
+ 编辑
+ </Button>
+ <Popconfirm
+ title="确定要删除这篇帖子吗?"
+ onConfirm={() => handleDeletePost(record.postId || record.id || 0)}
+ okText="确定"
+ cancelText="取消"
+ >
+ <Button type="link" danger icon={<DeleteOutlined />}>
+ 删除
+ </Button>
+ </Popconfirm>
+ </Space>
+ ),
+ },
+ ];
+
+ return (
+ <div className={styles.userCenterContainer}>
+ <Card title="个人中心" className={styles.userCenterCard}>
+ <Tabs activeKey={activeTab} onChange={setActiveTab}>
+ <TabPane tab="我的帖子" key="myPosts">
+ <div className={styles.tabContent}>
+ <div className={styles.tabHeader}>
+ <Button
+ type="primary"
+ icon={<PlusOutlined />}
+ onClick={() => setPublishModalVisible(true)}
+ >
+ 发布新帖子
+ </Button>
+ </div>
+ <Table
+ columns={myPostsColumns}
+ dataSource={myPosts}
+ loading={loading}
+ rowKey="id"
+ pagination={{
+ pageSize: 10,
+ showTotal: (total) => `共 ${total} 条记录`,
+ }}
+ />
+ </div>
+ </TabPane>
+
+ <TabPane tab="我的收藏" key="favorites">
+ <div className={styles.tabContent}>
+ <Row gutter={[24, 24]}>
+ {favorites.map((post: any) => {
+ // 确保post对象有正确的id字段
+ const formattedPost = {
+ ...post,
+ id: post.postId || post.id,
+ tags: post.tags ? (Array.isArray(post.tags) ? post.tags : post.tags.split(',')) : []
+ };
+ return (
+ <Col xs={24} sm={12} md={8} key={formattedPost.id}>
+ <PostCard post={formattedPost} />
+ </Col>
+ );
+ })}
+ </Row>
+ {favorites.length === 0 && !loading && (
+ <div className={styles.emptyState}>
+ <HeartOutlined style={{ fontSize: 48, color: '#ccc' }} />
+ <p>暂无收藏的帖子</p>
+ </div>
+ )}
+ </div>
+ </TabPane>
+ </Tabs>
+ </Card>
+
+ {/* 发布帖子弹窗 */}
+ <Modal
+ title="发布新帖子"
+ open={publishModalVisible}
+ onCancel={handleCancelPublish}
+ footer={null}
+ width={800}
+ >
+ <Form
+ form={form}
+ layout="vertical"
+ onFinish={handlePublishPost}
+ >
+ <Form.Item
+ name="title"
+ label="帖子标题"
+ rules={[{ required: true, message: '请输入帖子标题' }]}
+ >
+ <Input placeholder="请输入帖子标题" />
+ </Form.Item>
+
+ <Form.Item
+ name="summary"
+ label="帖子摘要"
+ rules={[{ required: true, message: '请输入帖子摘要' }]}
+ >
+ <TextArea rows={3} placeholder="请输入帖子摘要" />
+ </Form.Item>
+
+ <Form.Item
+ name="content"
+ label="帖子内容"
+ rules={[{ required: true, message: '请输入帖子内容' }]}
+ >
+ <TextArea rows={8} placeholder="请输入帖子内容" />
+ </Form.Item>
+
+ <Form.Item
+ name="coverImage"
+ label="封面图片(可选)"
+ >
+ <Upload
+ listType="picture-card"
+ showUploadList={false}
+ beforeUpload={handleImageUpload}
+ >
+ {coverImageUrl ? (
+ <Image
+ src={coverImageUrl}
+ alt="封面"
+ width="100%"
+ height="100%"
+ style={{ objectFit: 'cover' }}
+ />
+ ) : (
+ uploadButton
+ )}
+ </Upload>
+ {coverImageUrl && (
+ <Button
+ type="link"
+ onClick={handleDeleteImage}
+ style={{ padding: 0, marginTop: 8 }}
+ >
+ 删除图片
+ </Button>
+ )}
+ </Form.Item>
+
+ <Form.Item
+ name="tags"
+ label="标签"
+ rules={[{ required: true, message: '请选择标签' }]}
+ >
+ <Select
+ mode="multiple"
+ placeholder="请选择标签"
+ allowClear
+ style={{ width: '100%' }}
+ >
+ {availableTags.map(tag => (
+ <Select.Option key={tag.tagId} value={tag.tagName}>
+ <Tag color={tag.tagColor}>{tag.tagName}</Tag>
+ </Select.Option>
+ ))}
+ </Select>
+ </Form.Item>
+
+ <Form.Item
+ name="promotionPlan"
+ label="推广选项(可选)"
+ >
+ <Radio.Group>
+ <Space direction="vertical">
+ <Radio value={undefined}>不选择推广</Radio>
+ {promotionPlans.map(plan => (
+ <Radio key={plan.id} value={plan.id}>
+ <div>
+ <strong>{plan.name}</strong> - ¥{plan.price} ({plan.duration}天)
+ <br />
+ <span style={{ color: '#666', fontSize: '12px' }}>
+ {plan.description}
+ </span>
+ </div>
+ </Radio>
+ ))}
+ </Space>
+ </Radio.Group>
+ </Form.Item>
+
+ <Form.Item>
+ <Space>
+ <Button type="primary" htmlType="submit">
+ {selectedPromotion ? '选择支付方式' : '发布帖子'}
+ </Button>
+ <Button onClick={handleCancelPublish}>
+ 取消
+ </Button>
+ </Space>
+ </Form.Item>
+ </Form>
+ </Modal>
+
+ {/* 编辑帖子弹窗 */}
+ <Modal
+ title="编辑帖子"
+ open={editModalVisible}
+ onCancel={() => {
+ setEditModalVisible(false);
+ editForm.resetFields();
+ setCurrentEditPost(null);
+ setEditCoverImageUrl('');
+ setIsEditingPromotion(false);
+ }}
+ footer={null}
+ width={800}
+ >
+ <Form
+ form={editForm}
+ layout="vertical"
+ onFinish={handleUpdatePost}
+ >
+ <Form.Item
+ name="title"
+ label="帖子标题"
+ rules={[{ required: true, message: '请输入帖子标题' }]}
+ >
+ <Input placeholder="请输入帖子标题" />
+ </Form.Item>
+
+ <Form.Item
+ name="summary"
+ label="帖子摘要"
+ rules={[{ required: true, message: '请输入帖子摘要' }]}
+ >
+ <TextArea rows={3} placeholder="请输入帖子摘要" />
+ </Form.Item>
+
+ <Form.Item
+ name="content"
+ label="帖子内容"
+ rules={[{ required: true, message: '请输入帖子内容' }]}
+ >
+ <TextArea rows={8} placeholder="请输入帖子内容" />
+ </Form.Item>
+
+ <Form.Item
+ name="coverImage"
+ label="封面图片"
+ >
+ <Upload
+ listType="picture-card"
+ showUploadList={false}
+ beforeUpload={handleEditImageUpload}
+ >
+ {editCoverImageUrl ? (
+ <Image
+ src={editCoverImageUrl}
+ alt="封面"
+ width="100%"
+ height="100%"
+ style={{ objectFit: 'cover' }}
+ />
+ ) : (
+ editUploadButton
+ )}
+ </Upload>
+ {editCoverImageUrl && (
+ <Button
+ type="link"
+ onClick={handleDeleteEditImage}
+ style={{ padding: 0, marginTop: 8 }}
+ >
+ 删除图片
+ </Button>
+ )}
+ </Form.Item>
+
+ <Form.Item
+ name="tags"
+ label="标签"
+ rules={[{ required: true, message: '请选择标签' }]}
+ >
+ <Select
+ mode="multiple"
+ placeholder="请选择标签"
+ allowClear
+ style={{ width: '100%' }}
+ >
+ {availableTags.map(tag => (
+ <Select.Option key={tag.tagId} value={tag.tagName}>
+ <Tag color={tag.tagColor}>{tag.tagName}</Tag>
+ </Select.Option>
+ ))}
+ </Select>
+ </Form.Item>
+
+ <Form.Item
+ name="promotionPlan"
+ label="推广选项(可选)"
+ >
+ <Radio.Group disabled={isEditingPromotion}>
+ <Space direction="vertical">
+ <Radio value={undefined}>不选择推广</Radio>
+ {isEditingPromotion && (
+ <div style={{ color: '#ff4d4f', fontSize: '12px', marginBottom: 8 }}>
+ 该帖子已购买推广,无法更改推广选项
+ </div>
+ )}
+ {promotionPlans.map(plan => (
+ <Radio key={plan.id} value={plan.id} disabled={isEditingPromotion}>
+ <div>
+ <strong>{plan.name}</strong> - ¥{plan.price} ({plan.duration}天)
+ <br />
+ <span style={{ color: '#666', fontSize: '12px' }}>
+ {plan.description}
+ </span>
+ </div>
+ </Radio>
+ ))}
+ </Space>
+ </Radio.Group>
+ </Form.Item>
+
+ <Form.Item>
+ <Space>
+ <Button type="primary" htmlType="submit">
+ 更新帖子
+ </Button>
+ <Button onClick={() => {
+ setEditModalVisible(false);
+ editForm.resetFields();
+ setCurrentEditPost(null);
+ setEditCoverImageUrl('');
+ setIsEditingPromotion(false);
+ }}>
+ 取消
+ </Button>
+ </Space>
+ </Form.Item>
+ </Form>
+ </Modal>
+
+ {/* 支付弹窗 */}
+ <Modal
+ title="支付推广费用"
+ open={paymentModalVisible}
+ onCancel={handlePaymentCancel}
+ footer={null}
+ width={400}
+ >
+ <div className={styles.paymentModal}>
+ {selectedPromotion && (
+ <>
+ <div className={styles.paymentInfo}>
+ <h3>{selectedPromotion.name}</h3>
+ <p>{selectedPromotion.description}</p>
+ <p>费用: <strong>¥{selectedPromotion.price}</strong></p>
+ <p>时长: {selectedPromotion.duration}天</p>
+ </div>
+
+ <div className={styles.qrCode}>
+ <div className={styles.qrCodePlaceholder}>
+ <p>支付二维码</p>
+ <p style={{ fontSize: '12px', color: '#666' }}>
+ 请使用支付宝扫描二维码支付
+ </p>
+ <div className={styles.mockQrCode}>
+ <p>模拟二维码</p>
+ <p>¥{selectedPromotion.price}</p>
+ </div>
+ </div>
+ </div>
+
+ <div className={styles.paymentActions}>
+ <Button
+ type="primary"
+ onClick={handlePaymentConfirm}
+ style={{ width: '100%', marginBottom: 8 }}
+ >
+ 我已完成支付
+ </Button>
+ <Button
+ onClick={handlePaymentCancel}
+ style={{ width: '100%' }}
+ >
+ 取消支付
+ </Button>
+ </div>
+ </>
+ )}
+ </div>
+ </Modal>
+ </div>
+ );
+};
+
+export default UserCenter;
\ No newline at end of file