| 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; |