22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 1 | import React, { useContext, useEffect, useState } from 'react'; |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 2 | import { useParams } from 'wouter'; |
| 3 | import { GoodTwo, Star } from '@icon-park/react'; |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 4 | import { getPostDetail, getPostComments, likePost, unlikePost, addCommentToPost, collectPost } from './api'; // 你的 API 函数 |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 5 | import './PostDetailPage.css'; |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 6 | import { UserContext } from '../../../context/UserContext'; // 用户上下文 |
Krishya | c0f7e9b | 2025-04-22 15:28:28 +0800 | [diff] [blame] | 7 | import Header from '../../../components/Header'; |
22301009 | 207e2db | 2025-06-09 00:27:28 +0800 | [diff] [blame] | 8 | import AuthButton from '../../../components/AuthButton'; |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 9 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 10 | const formatImageUrl = (url) => { |
| 11 | if (!url) return ''; |
| 12 | |
| 13 | if (url.startsWith('http')) return url; |
| 14 | |
| 15 | // 如果是 /images/... ,替换成 /uploads/post/... |
| 16 | if (url.startsWith('/images/')) { |
| 17 | // 这里把 /images/ 替换成 /uploads/post/ |
22301009 | 4158f3a | 2025-06-06 19:59:10 +0800 | [diff] [blame] | 18 | return `http://localhost:5011/uploads/post/${url.slice('/images/'.length)}`; |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 19 | } |
| 20 | |
| 21 | // 其它情况默认直接拼接,不加斜杠 |
22301009 | 4158f3a | 2025-06-06 19:59:10 +0800 | [diff] [blame] | 22 | return `http://localhost:5011${url.startsWith('/') ? '' : '/'}${url}`; |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 23 | }; |
| 24 | |
| 25 | |
| 26 | // 头像地址格式化,处理 avatarUrl 字段 |
| 27 | export function formatAvatarUrlNoDefault(avatarUrl) { |
| 28 | if (!avatarUrl) return ''; |
| 29 | if (avatarUrl.startsWith('http')) return avatarUrl; |
22301009 | 4158f3a | 2025-06-06 19:59:10 +0800 | [diff] [blame] | 30 | return `http://localhost:5011${avatarUrl}`; |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 31 | } |
| 32 | |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 33 | const PostDetailPage = () => { |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 34 | const { postId } = useParams(); |
| 35 | const { user } = useContext(UserContext); |
| 36 | |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 37 | const [postDetail, setPostDetail] = useState(null); |
| 38 | const [comments, setComments] = useState([]); |
| 39 | const [loading, setLoading] = useState(true); |
| 40 | const [errorMsg, setErrorMsg] = useState(''); |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 41 | const [newComment, setNewComment] = useState(''); |
| 42 | const [isLiked, setIsLiked] = useState(false); |
| 43 | const [isCollected, setIsCollected] = useState(false); |
| 44 | const [replyToCommentId, setReplyToCommentId] = useState(null); |
Krishya | 2283d88 | 2025-05-27 22:25:19 +0800 | [diff] [blame] | 45 | const [replyToUsername, setReplyToUsername] = useState(null); |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 46 | |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 47 | useEffect(() => { |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 48 | const fetchData = async () => { |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 49 | setLoading(true); |
| 50 | setErrorMsg(''); |
| 51 | try { |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 52 | const postData = await getPostDetail(postId); |
| 53 | setPostDetail(postData); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 54 | const commentsData = await getPostComments(postId); |
| 55 | setComments(commentsData); |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 56 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 57 | setIsLiked(!!postData.likedByUser); |
| 58 | setIsCollected(!!postData.collectedByUser); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 59 | } catch (err) { |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 60 | console.error(err); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 61 | setErrorMsg('加载失败,请稍后重试'); |
| 62 | } finally { |
| 63 | setLoading(false); |
| 64 | } |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 65 | }; |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 66 | fetchData(); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 67 | }, [postId]); |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 68 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 69 | // 点赞 |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 70 | const toggleLike = async () => { |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 71 | if (!user) return alert('请先登录'); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 72 | try { |
| 73 | if (isLiked) { |
Krishya | 8f2fec8 | 2025-06-04 21:54:46 +0800 | [diff] [blame] | 74 | await unlikePost(postId, user.userId); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 75 | setIsLiked(false); |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 76 | setPostDetail(prev => ({ |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 77 | ...prev, |
| 78 | postLikeNum: prev.postLikeNum - 1, |
| 79 | })); |
| 80 | } else { |
Krishya | 8f2fec8 | 2025-06-04 21:54:46 +0800 | [diff] [blame] | 81 | await likePost(postId, user.userId); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 82 | setIsLiked(true); |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 83 | setPostDetail(prev => ({ |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 84 | ...prev, |
| 85 | postLikeNum: prev.postLikeNum + 1, |
| 86 | })); |
| 87 | } |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 88 | } catch { |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 89 | alert('点赞失败,请稍后再试'); |
| 90 | } |
| 91 | }; |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 92 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 93 | // 收藏 |
| 94 | const toggleCollect = async () => { |
| 95 | if (!user) return alert('请先登录'); |
| 96 | try { |
| 97 | if (isCollected) { |
| 98 | await collectPost(postId, user.userId, 'cancel'); |
| 99 | setIsCollected(false); |
| 100 | setPostDetail(prev => ({ |
| 101 | ...prev, |
| 102 | postCollectNum: prev.postCollectNum - 1, |
| 103 | })); |
| 104 | } else { |
| 105 | await collectPost(postId, user.userId, 'collect'); |
| 106 | setIsCollected(true); |
| 107 | setPostDetail(prev => ({ |
| 108 | ...prev, |
| 109 | postCollectNum: prev.postCollectNum + 1, |
| 110 | })); |
| 111 | } |
| 112 | } catch { |
| 113 | alert('收藏失败,请稍后再试'); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 114 | } |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 115 | }; |
| 116 | |
| 117 | // 添加评论 |
| 118 | const handleAddComment = async () => { |
| 119 | if (!user || !user.userId) return alert('请先登录后再评论'); |
| 120 | if (!newComment.trim()) return alert('评论内容不能为空'); |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 121 | |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 122 | try { |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 123 | const commentPayload = { |
| 124 | content: newComment, |
| 125 | userId: user.userId, |
| 126 | isAnonymous: false, |
| 127 | com_comment_id: replyToCommentId || null, |
| 128 | }; |
| 129 | const commentData = await addCommentToPost(postId, commentPayload); |
| 130 | |
| 131 | const newCommentItem = { |
| 132 | commentId: commentData?.commentId || Date.now(), |
| 133 | post_id: postId, |
| 134 | userId: user.userId, |
| 135 | username: user.username || '匿名', |
| 136 | content: newComment, |
| 137 | commentTime: new Date().toISOString(), |
| 138 | comCommentId: replyToCommentId, |
| 139 | userAvatar: user.avatar_url || '', |
| 140 | }; |
| 141 | |
| 142 | setComments(prev => [newCommentItem, ...prev]); |
| 143 | setNewComment(''); |
| 144 | setReplyToCommentId(null); |
| 145 | setReplyToUsername(null); |
| 146 | } catch (error) { |
| 147 | alert(error.response?.data?.message || '评论失败,请稍后再试'); |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 148 | } |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 149 | }; |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 150 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 151 | // 回复按钮点击 |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 152 | const handleReply = (commentId) => { |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 153 | setReplyToCommentId(commentId); |
| 154 | const comment = comments.find(c => c.commentId === commentId); |
| 155 | setReplyToUsername(comment?.username || comment?.userId || '未知用户'); |
| 156 | }; |
Krishya | 2283d88 | 2025-05-27 22:25:19 +0800 | [diff] [blame] | 157 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 158 | // 查找回复的用户名 |
| 159 | const findUsernameByCommentId = (id) => { |
| 160 | const comment = comments.find(c => c.commentId === id); |
| 161 | return comment ? (comment.username || comment.userId || '未知用户') : '未知用户'; |
| 162 | }; |
Krishya | 2283d88 | 2025-05-27 22:25:19 +0800 | [diff] [blame] | 163 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 164 | // 帖子图片处理,imgUrl 是单字符串,包装成数组 |
| 165 | const getPostImages = () => { |
| 166 | if (!postDetail) return []; |
| 167 | if (postDetail.imgUrl) return [formatImageUrl(postDetail.imgUrl)]; |
| 168 | return []; |
| 169 | }; |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 170 | |
| 171 | return ( |
| 172 | <div className="post-detail-page"> |
Krishya | c0f7e9b | 2025-04-22 15:28:28 +0800 | [diff] [blame] | 173 | <Header /> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 174 | {loading ? ( |
| 175 | <p>加载中...</p> |
| 176 | ) : errorMsg ? ( |
| 177 | <p className="error-text">{errorMsg}</p> |
| 178 | ) : postDetail ? ( |
| 179 | <div className="post-detail"> |
| 180 | <h1>{postDetail.title}</h1> |
| 181 | <div className="post-meta"> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 182 | <div className="post-user-info"> |
| 183 | {postDetail.avatarUrl ? ( |
| 184 | <img |
| 185 | className="avatar" |
| 186 | src={formatAvatarUrlNoDefault(postDetail.avatarUrl)} |
| 187 | alt={postDetail.username || '用户头像'} |
| 188 | /> |
| 189 | ) : null} |
| 190 | <span className="post-username">{postDetail.username || '匿名用户'}</span> |
| 191 | </div> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 192 | <span className="post-time"> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 193 | 发布时间:{new Date(postDetail.postTime).toLocaleString()} |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 194 | </span> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 195 | </div> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 196 | |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 197 | <div className="post-content"> |
| 198 | <p>{postDetail.postContent}</p> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 199 | <div className="post-images"> |
| 200 | {getPostImages().map((url, idx) => ( |
| 201 | <img key={idx} src={url} alt={`图片${idx + 1}`} /> |
| 202 | ))} |
| 203 | </div> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 204 | </div> |
| 205 | |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 206 | <div className="post-actions"> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 207 | <button className="icon-btn" onClick={toggleLike} title="点赞"> |
| 208 | <GoodTwo theme="outline" size="20" fill={isLiked ? '#f00' : '#ccc'} /> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 209 | <span>{postDetail.postLikeNum}</span> |
| 210 | </button> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 211 | <button className="icon-btn" onClick={toggleCollect} title="收藏"> |
| 212 | <Star theme="outline" size="20" fill={isCollected ? '#ffd700' : '#ccc'} /> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 213 | <span>{postDetail.postCollectNum}</span> |
| 214 | </button> |
| 215 | </div> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 216 | |
Krishya | 1300cad | 2025-04-20 22:16:45 +0800 | [diff] [blame] | 217 | <hr className="divider" /> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 218 | |
Krishya | 1300cad | 2025-04-20 22:16:45 +0800 | [diff] [blame] | 219 | <h3>评论区</h3> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 220 | <div className="comments-section"> |
| 221 | {comments.length ? comments.map(comment => ( |
| 222 | <div key={comment.commentId} className="comment"> |
| 223 | <div className="comment-header"> |
| 224 | <div className="comment-user-info"> |
| 225 | {comment.userAvatar ? ( |
| 226 | <img |
| 227 | className="avatar-small" |
| 228 | src={formatAvatarUrlNoDefault(comment.userAvatar)} |
| 229 | alt={comment.username || '用户头像'} |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 230 | /> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 231 | ) : null} |
| 232 | <span className="comment-username">{comment.username || '匿名用户'}</span> |
| 233 | </div> |
| 234 | <button className="reply-btn" onClick={() => handleReply(comment.commentId)}>回复</button> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 235 | </div> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 236 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 237 | <p className="comment-content"> |
| 238 | {comment.comCommentId ? ( |
| 239 | <> |
| 240 | <span className="reply-to">回复 {findUsernameByCommentId(comment.comCommentId)}:</span> |
| 241 | {comment.content} |
| 242 | </> |
| 243 | ) : ( |
| 244 | comment.content |
| 245 | )} |
| 246 | </p> |
Krishya | 2283d88 | 2025-05-27 22:25:19 +0800 | [diff] [blame] | 247 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 248 | <div className="comment-time"> |
| 249 | {new Date(comment.commentTime).toLocaleString()} |
| 250 | </div> |
| 251 | |
| 252 | {replyToCommentId === comment.commentId && ( |
| 253 | <div className="reply-form"> |
| 254 | <div className="replying-to"> |
| 255 | 回复 <strong>{replyToUsername}</strong>: |
| 256 | </div> |
| 257 | <textarea |
| 258 | placeholder="输入你的回复..." |
| 259 | value={newComment} |
| 260 | onChange={(e) => setNewComment(e.target.value)} |
| 261 | /> |
| 262 | <div className="comment-options"> |
22301009 | 207e2db | 2025-06-09 00:27:28 +0800 | [diff] [blame] | 263 | <AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}> |
| 264 | 发布回复 |
| 265 | </AuthButton> |
| 266 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 267 | <button |
| 268 | onClick={() => { |
| 269 | setReplyToCommentId(null); |
| 270 | setReplyToUsername(null); |
| 271 | setNewComment(''); |
| 272 | }} |
| 273 | style={{ marginLeft: '8px' }} |
| 274 | > |
| 275 | 取消 |
| 276 | </button> |
| 277 | </div> |
| 278 | </div> |
| 279 | )} |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 280 | </div> |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 281 | )) : <p>暂无评论</p>} |
| 282 | |
| 283 | {!replyToCommentId && ( |
| 284 | <div className="add-comment-form"> |
| 285 | <textarea |
| 286 | placeholder="输入你的评论..." |
| 287 | value={newComment} |
| 288 | onChange={(e) => setNewComment(e.target.value)} |
| 289 | /> |
| 290 | <div className="comment-options"> |
22301009 | 207e2db | 2025-06-09 00:27:28 +0800 | [diff] [blame] | 291 | <AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}> |
| 292 | 发布评论 |
| 293 | </AuthButton> |
| 294 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 295 | </div> |
| 296 | </div> |
| 297 | )} |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 298 | </div> |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 299 | </div> |
22301009 | 237217b | 2025-04-20 15:15:25 +0800 | [diff] [blame] | 300 | ) : ( |
| 301 | <p>帖子不存在</p> |
| 302 | )} |
| 303 | </div> |
| 304 | ); |
Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame] | 305 | }; |
| 306 | |
22301009 | df48f96 | 2025-06-05 13:40:44 +0800 | [diff] [blame] | 307 | export default PostDetailPage; |