blob: c6aedccfcedafeab8543bd191101806f285bed6a [file] [log] [blame]
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;