创作中心模块包含首页展示、个人中心、帖子审核。
“首页展示”支持广告轮播展示、推广帖子优先展示、分页显示所有帖子、导航栏便捷标签筛选帖子、全局标题模糊搜索帖子、点击帖子“查看更多”进入帖子详情页。帖子详情页展示帖子封面图片、作者时间、详细内容(可以插入种子链接对种子进行介绍与推广)等基本信息、对帖子点赞收藏举报评论回复、查看相关推荐帖子。相关推荐会推荐当前帖子作者的其他帖子(最多推荐5篇),还会推荐具有相似标签的其他帖子,两者总共最多推荐9篇帖子。
“个人中心”包含“我的中心”和“我的收藏”。
“我的中心”中可以管理已经成功发布的帖子(编辑、删除帖子),还可以发布新帖子。发布新帖子时除了填写帖子基本信息以外,帖子标签支持下拉多项选择,用户还可以选择帖子推广项目并进行支付。设置了多种推广项目,包含广告轮播推广、帖子置顶展示、限时优先展示、分类页首条展示。系统后台执行自动定时任务,每小时对帖子的推广时效性进行检查,如超出推广时限,则取消帖子的推广显示特权。用户点击发布帖子后帖子处于待审核状态,需要管理员审核通过才能正常发布在首页展示页面。编辑帖子时用户可以追加帖子推广,但如果帖子处于推广状态,则禁止修改推广项目。
“我的收藏”中可以便捷查看所有已收藏的帖子。
“帖子审核”包含“帖子发布管理”和“帖子举报管理”。“帖子审核”板块具有权限管理,只有管理员界面能够进入。
“帖子发布管理”对所有待审核帖子进行处理,支持预览待审核帖子详细内容,批准通过和拒绝通过选项。
“帖子举报管理”对所有用户的举报请求进行人工审核,如果举报内容属实,则将帖子下架处理,如果举报内容不属实,驳回举报请求。所有举报请求的处理结果均留存显示,方便后续再次审查。
Change-Id: If822351183e9d55a5a56ff5cf1e13b313fdbe231
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