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