blob: bf0f0627d1d3666a5d570ee6e649996d49e44401 [file] [log] [blame]
import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'wouter';
import { GoodTwo, Star } from '@icon-park/react';
import { getPostDetail, getPostComments, likePost, unlikePost, addCommentToPost, collectPost } from './api'; // 你的 API 函数
import './PostDetailPage.css';
import { UserContext } from '../../../context/UserContext'; // 用户上下文
import Header from '../../../components/Header';
import AuthButton from '../../../components/AuthButton';
const formatImageUrl = (url) => {
if (!url) return '';
if (url.startsWith('http')) return url;
// 如果是 /images/... ,替换成 /uploads/post/...
if (url.startsWith('/images/')) {
// 这里把 /images/ 替换成 /uploads/post/
return `http://localhost:5011/uploads/post/${url.slice('/images/'.length)}`;
}
// 其它情况默认直接拼接,不加斜杠
return `http://localhost:5011${url.startsWith('/') ? '' : '/'}${url}`;
};
// 头像地址格式化,处理 avatarUrl 字段
export function formatAvatarUrlNoDefault(avatarUrl) {
if (!avatarUrl) return '';
if (avatarUrl.startsWith('http')) return avatarUrl;
return `http://localhost:5011${avatarUrl}`;
}
const PostDetailPage = () => {
const { postId } = useParams();
const { user } = useContext(UserContext);
const [postDetail, setPostDetail] = useState(null);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState('');
const [newComment, setNewComment] = useState('');
const [isLiked, setIsLiked] = useState(false);
const [isCollected, setIsCollected] = useState(false);
const [replyToCommentId, setReplyToCommentId] = useState(null);
const [replyToUsername, setReplyToUsername] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setErrorMsg('');
try {
const postData = await getPostDetail(postId);
setPostDetail(postData);
const commentsData = await getPostComments(postId);
setComments(commentsData);
setIsLiked(!!postData.likedByUser);
setIsCollected(!!postData.collectedByUser);
} catch (err) {
console.error(err);
setErrorMsg('加载失败,请稍后重试');
} finally {
setLoading(false);
}
};
fetchData();
}, [postId]);
// 点赞
const toggleLike = async () => {
if (!user) return alert('请先登录');
try {
if (isLiked) {
await unlikePost(postId, user.userId);
setIsLiked(false);
setPostDetail(prev => ({
...prev,
postLikeNum: prev.postLikeNum - 1,
}));
} else {
await likePost(postId, user.userId);
setIsLiked(true);
setPostDetail(prev => ({
...prev,
postLikeNum: prev.postLikeNum + 1,
}));
}
} catch {
alert('点赞失败,请稍后再试');
}
};
// 收藏
const toggleCollect = async () => {
if (!user) return alert('请先登录');
try {
if (isCollected) {
await collectPost(postId, user.userId, 'cancel');
setIsCollected(false);
setPostDetail(prev => ({
...prev,
postCollectNum: prev.postCollectNum - 1,
}));
} else {
await collectPost(postId, user.userId, 'collect');
setIsCollected(true);
setPostDetail(prev => ({
...prev,
postCollectNum: prev.postCollectNum + 1,
}));
}
} catch {
alert('收藏失败,请稍后再试');
}
};
// 添加评论
const handleAddComment = async () => {
if (!user || !user.userId) return alert('请先登录后再评论');
if (!newComment.trim()) return alert('评论内容不能为空');
try {
const commentPayload = {
content: newComment,
userId: user.userId,
isAnonymous: false,
com_comment_id: replyToCommentId || null,
};
const commentData = await addCommentToPost(postId, commentPayload);
const newCommentItem = {
commentId: commentData?.commentId || Date.now(),
post_id: postId,
userId: user.userId,
username: user.username || '匿名',
content: newComment,
commentTime: new Date().toISOString(),
comCommentId: replyToCommentId,
userAvatar: user.avatar_url || '',
};
setComments(prev => [newCommentItem, ...prev]);
setNewComment('');
setReplyToCommentId(null);
setReplyToUsername(null);
} catch (error) {
alert(error.response?.data?.message || '评论失败,请稍后再试');
}
};
// 回复按钮点击
const handleReply = (commentId) => {
setReplyToCommentId(commentId);
const comment = comments.find(c => c.commentId === commentId);
setReplyToUsername(comment?.username || comment?.userId || '未知用户');
};
// 查找回复的用户名
const findUsernameByCommentId = (id) => {
const comment = comments.find(c => c.commentId === id);
return comment ? (comment.username || comment.userId || '未知用户') : '未知用户';
};
// 帖子图片处理,imgUrl 是单字符串,包装成数组
const getPostImages = () => {
if (!postDetail) return [];
if (postDetail.imgUrl) return [formatImageUrl(postDetail.imgUrl)];
return [];
};
return (
<div className="post-detail-page">
<Header />
{loading ? (
<p>加载中...</p>
) : errorMsg ? (
<p className="error-text">{errorMsg}</p>
) : postDetail ? (
<div className="post-detail">
<h1>{postDetail.title}</h1>
<div className="post-meta">
<div className="post-user-info">
{postDetail.avatarUrl ? (
<img
className="avatar"
src={formatAvatarUrlNoDefault(postDetail.avatarUrl)}
alt={postDetail.username || '用户头像'}
/>
) : null}
<span className="post-username">{postDetail.username || '匿名用户'}</span>
</div>
<span className="post-time">
发布时间:{new Date(postDetail.postTime).toLocaleString()}
</span>
</div>
<div className="post-content">
<p>{postDetail.postContent}</p>
<div className="post-images">
{getPostImages().map((url, idx) => (
<img key={idx} src={url} alt={`图片${idx + 1}`} />
))}
</div>
</div>
<div className="post-actions">
<button className="icon-btn" onClick={toggleLike} title="点赞">
<GoodTwo theme="outline" size="20" fill={isLiked ? '#f00' : '#ccc'} />
<span>{postDetail.postLikeNum}</span>
</button>
<button className="icon-btn" onClick={toggleCollect} title="收藏">
<Star theme="outline" size="20" fill={isCollected ? '#ffd700' : '#ccc'} />
<span>{postDetail.postCollectNum}</span>
</button>
</div>
<hr className="divider" />
<h3>评论区</h3>
<div className="comments-section">
{comments.length ? comments.map(comment => (
<div key={comment.commentId} className="comment">
<div className="comment-header">
<div className="comment-user-info">
{comment.userAvatar ? (
<img
className="avatar-small"
src={formatAvatarUrlNoDefault(comment.userAvatar)}
alt={comment.username || '用户头像'}
/>
) : null}
<span className="comment-username">{comment.username || '匿名用户'}</span>
</div>
<button className="reply-btn" onClick={() => handleReply(comment.commentId)}>回复</button>
</div>
<p className="comment-content">
{comment.comCommentId ? (
<>
<span className="reply-to">回复 {findUsernameByCommentId(comment.comCommentId)}:</span>
{comment.content}
</>
) : (
comment.content
)}
</p>
<div className="comment-time">
{new Date(comment.commentTime).toLocaleString()}
</div>
{replyToCommentId === comment.commentId && (
<div className="reply-form">
<div className="replying-to">
回复 <strong>{replyToUsername}</strong>:
</div>
<textarea
placeholder="输入你的回复..."
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
/>
<div className="comment-options">
<AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}>
发布回复
</AuthButton>
<button
onClick={() => {
setReplyToCommentId(null);
setReplyToUsername(null);
setNewComment('');
}}
style={{ marginLeft: '8px' }}
>
取消
</button>
</div>
</div>
)}
</div>
)) : <p>暂无评论</p>}
{!replyToCommentId && (
<div className="add-comment-form">
<textarea
placeholder="输入你的评论..."
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
/>
<div className="comment-options">
<AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}>
发布评论
</AuthButton>
</div>
</div>
)}
</div>
</div>
) : (
<p>帖子不存在</p>
)}
</div>
);
};
export default PostDetailPage;