Krishya | 7ec1dd0 | 2025-04-19 15:29:03 +0800 | [diff] [blame^] | 1 | import React, { useState, useEffect } from 'react'; |
| 2 | import { useRoute } from 'wouter'; |
| 3 | import { |
| 4 | getPostDetail, |
| 5 | getPostComments, |
| 6 | likePost, |
| 7 | unlikePost, |
| 8 | addCommentToPost, |
| 9 | replyToComment, |
| 10 | likeComment, |
| 11 | unlikeComment, |
| 12 | getUserInfo |
| 13 | } from './api'; |
| 14 | import './PostDetailPage.css'; |
| 15 | import axios from 'axios'; |
| 16 | |
| 17 | const API_BASE = process.env.REACT_APP_API_BASE; |
| 18 | |
| 19 | const PostHeader = ({ post }) => { |
| 20 | const anonymousAvatar = '/assets/img/anonymous.jpg'; |
| 21 | return ( |
| 22 | <div className="post-header"> |
| 23 | <div className="author-info"> |
| 24 | <img className="avatar" src={post.isAnonymous? anonymousAvatar : post.userProfile.avatar_url} alt="头像" /> |
| 25 | <span className="author-name">{post.isAnonymous? '某同学' : post.userProfile.nickname}</span> |
| 26 | </div> |
| 27 | <h1 className="post-title">{post.title}</h1> |
| 28 | </div> |
| 29 | ); |
| 30 | }; |
| 31 | |
| 32 | const PostContent = ({ content }) => { |
| 33 | return ( |
| 34 | <div className="post-content" dangerouslySetInnerHTML={{ __html: content }} /> |
| 35 | ); |
| 36 | }; |
| 37 | |
| 38 | const PostActions = ({ post, onLike, onFavorite }) => { |
| 39 | return ( |
| 40 | <div className="post-actions"> |
| 41 | <div className="action-item" onClick={onLike}> |
| 42 | <i className={post.liked? 'liked' : 'unliked'} /> |
| 43 | <span>{post.likeCount || 0}</span> |
| 44 | </div> |
| 45 | <div className="action-item" onClick={onFavorite}> |
| 46 | <i className={post.favorited? 'favorited' : 'unfavorited'} /> |
| 47 | <span>{post.favorites || 0}</span> |
| 48 | </div> |
| 49 | </div> |
| 50 | ); |
| 51 | }; |
| 52 | |
| 53 | const CommentInput = ({ onSubmitComment, isFlag }) => { |
| 54 | const [content, setContent] = useState(''); |
| 55 | |
| 56 | useEffect(() => { |
| 57 | if (isFlag) { |
| 58 | setContent(''); |
| 59 | } |
| 60 | }, [isFlag]); |
| 61 | |
| 62 | const handleSubmit = () => { |
| 63 | if (content) { |
| 64 | onSubmitComment(content); |
| 65 | } |
| 66 | }; |
| 67 | |
| 68 | return ( |
| 69 | <div className="comment-input"> |
| 70 | <textarea |
| 71 | value={content} |
| 72 | onChange={(e) => setContent(e.target.value)} |
| 73 | placeholder="写下你的评论..." |
| 74 | className="comment-textarea" |
| 75 | /> |
| 76 | <div className="button-container"> |
| 77 | <button |
| 78 | type="button" |
| 79 | onClick={handleSubmit} |
| 80 | disabled={!content} |
| 81 | className="submit-button" |
| 82 | > |
| 83 | 发布评论 |
| 84 | </button> |
| 85 | </div> |
| 86 | </div> |
| 87 | ); |
| 88 | }; |
| 89 | |
| 90 | const CommentItem = ({ comment, onLikeComment, onReplyComment }) => { |
| 91 | const [showReplyInput, setShowReplyInput] = useState(false); |
| 92 | const [replyContent, setReplyContent] = useState(''); |
| 93 | |
| 94 | const queryUserInfo = async (id) => { |
| 95 | if (!id) { |
| 96 | return; |
| 97 | } |
| 98 | try { |
| 99 | const userData = await getUserInfo(id); |
| 100 | console.log(userData); |
| 101 | // 这里可以添加跳转逻辑等,比如根据用户ID跳转到对应个人信息页面 |
| 102 | } catch (error) { |
| 103 | console.error('获取用户信息失败:', error); |
| 104 | } |
| 105 | }; |
| 106 | |
| 107 | const handleLike = () => { |
| 108 | onLikeComment(comment); |
| 109 | }; |
| 110 | |
| 111 | const handleReply = () => { |
| 112 | setShowReplyInput(!showReplyInput); |
| 113 | }; |
| 114 | |
| 115 | const handleSubmitReply = () => { |
| 116 | if (replyContent) { |
| 117 | onReplyComment(comment, replyContent); |
| 118 | setReplyContent(''); |
| 119 | setShowReplyInput(false); |
| 120 | } |
| 121 | }; |
| 122 | |
| 123 | return ( |
| 124 | <div className="comment-item"> |
| 125 | <img className="avatar" src={comment.author.avatar_url} alt="头像" onClick={() => queryUserInfo(comment.author.userId)} /> |
| 126 | <div className="comment-content"> |
| 127 | <div className="comment-author">{comment.author.nickname}</div> |
| 128 | <div className="comment-text">{comment.content}</div> |
| 129 | <div className="comment-actions"> |
| 130 | <div className="action-item" onClick={handleLike}> |
| 131 | <i className={comment.liked? 'liked' : 'unliked'} /> |
| 132 | <span>{comment.likeCount || 0}</span> |
| 133 | </div> |
| 134 | <button type="button" onClick={handleReply}>回复</button> |
| 135 | </div> |
| 136 | {showReplyInput && ( |
| 137 | <div className="reply-input"> |
| 138 | <textarea |
| 139 | value={replyContent} |
| 140 | onChange={(e) => setReplyContent(e.target.value)} |
| 141 | placeholder="写下你的回复..." |
| 142 | className="reply-textarea" |
| 143 | /> |
| 144 | <button |
| 145 | type="button" |
| 146 | onClick={handleSubmitReply} |
| 147 | disabled={!replyContent} |
| 148 | className="submit-button" |
| 149 | > |
| 150 | 发布回复 |
| 151 | </button> |
| 152 | </div> |
| 153 | )} |
| 154 | {comment.replies && comment.replies.length > 0 && ( |
| 155 | <div className="reply-list"> |
| 156 | {comment.replies.map(reply => ( |
| 157 | <CommentItem |
| 158 | key={reply.id} |
| 159 | comment={reply} |
| 160 | onLikeComment={onLikeComment} |
| 161 | onReplyComment={onReplyComment} |
| 162 | /> |
| 163 | ))} |
| 164 | </div> |
| 165 | )} |
| 166 | </div> |
| 167 | </div> |
| 168 | ); |
| 169 | }; |
| 170 | |
| 171 | const CommentsList = ({ comments, onLikeComment, onReplyComment }) => { |
| 172 | return ( |
| 173 | <div className="comments-list"> |
| 174 | <h2>评论</h2> |
| 175 | {comments.map(comment => ( |
| 176 | <CommentItem |
| 177 | key={comment.id} |
| 178 | comment={comment} |
| 179 | onLikeComment={onLikeComment} |
| 180 | onReplyComment={onReplyComment} |
| 181 | /> |
| 182 | ))} |
| 183 | </div> |
| 184 | ); |
| 185 | }; |
| 186 | |
| 187 | const PostDetailPage = () => { |
| 188 | const [post, setPost] = useState(null); |
| 189 | const [loading, setLoading] = useState(true); |
| 190 | const [errorMsg, setErrorMsg] = useState(''); |
| 191 | const [comments, setComments] = useState([]); |
| 192 | const [isFlag, setIsFlag] = useState(false); |
| 193 | |
| 194 | const { params } = useRoute('/forum/post/:postId'); |
| 195 | const postId = params?.postId; |
| 196 | |
| 197 | useEffect(() => { |
| 198 | const fetchPostDetail = async () => { |
| 199 | setLoading(true); |
| 200 | setErrorMsg(''); |
| 201 | try { |
| 202 | const postDetail = await getPostDetail(postId); |
| 203 | const postComments = await getPostComments(postId); |
| 204 | setPost(postDetail); |
| 205 | setComments(postComments); |
| 206 | } catch (error) { |
| 207 | console.error('获取帖子详情失败:', error); |
| 208 | setErrorMsg('加载失败,请稍后重试'); |
| 209 | } finally { |
| 210 | setLoading(false); |
| 211 | } |
| 212 | }; |
| 213 | |
| 214 | if (postId) { |
| 215 | fetchPostDetail(); |
| 216 | } |
| 217 | }, [postId]); |
| 218 | |
| 219 | const handleLike = async () => { |
| 220 | if (!post.liked) { |
| 221 | try { |
| 222 | await likePost(postId); |
| 223 | const newPost = { ...post, liked: true, likeCount: post.likeCount + 1 }; |
| 224 | setPost(newPost); |
| 225 | } catch (err) { |
| 226 | console.error('点赞失败:', err); |
| 227 | } |
| 228 | } else { |
| 229 | try { |
| 230 | await unlikePost(postId); |
| 231 | const newPost = { ...post, liked: false, likeCount: post.likeCount - 1 }; |
| 232 | setPost(newPost); |
| 233 | } catch (err) { |
| 234 | console.error('取消点赞失败:', err); |
| 235 | } |
| 236 | } |
| 237 | }; |
| 238 | |
| 239 | const handleFavorite = () => { |
| 240 | const newPost = { ...post, favorited: !post.favorited, favorites: post.favorited? post.favorites - 1 : post.favorites + 1 }; |
| 241 | setPost(newPost); |
| 242 | if (newPost.favorited) { |
| 243 | axios.post(`${API_BASE}/echo/forum/posts/${postId}/favorite`).catch(err => { |
| 244 | console.error('收藏失败:', err); |
| 245 | }); |
| 246 | } else { |
| 247 | axios.delete(`${API_BASE}/echo/forum/posts/${postId}/unfavorite`).catch(err => { |
| 248 | console.error('取消收藏失败:', err); |
| 249 | }); |
| 250 | } |
| 251 | }; |
| 252 | |
| 253 | const likeCommentAction = async (comment) => { |
| 254 | if (!comment.liked) { |
| 255 | try { |
| 256 | await likeComment(comment.id); |
| 257 | const newComment = { ...comment, liked: true, likeCount: comment.likeCount + 1 }; |
| 258 | setComments(comments.map(c => c.id === comment.id? newComment : c)); |
| 259 | } catch (err) { |
| 260 | console.error('点赞评论失败:', err); |
| 261 | } |
| 262 | } else { |
| 263 | try { |
| 264 | await unlikeComment(comment.id); |
| 265 | const newComment = { ...comment, liked: false, likeCount: comment.likeCount - 1 }; |
| 266 | setComments(comments.map(c => c.id === comment.id? newComment : c)); |
| 267 | } catch (err) { |
| 268 | console.error('取消点赞评论失败:', err); |
| 269 | } |
| 270 | } |
| 271 | }; |
| 272 | |
| 273 | const replyCommentAction = async (comment, replyContent) => { |
| 274 | try { |
| 275 | await replyToComment(comment.id, replyContent); |
| 276 | setIsFlag(true); |
| 277 | const commentResponse = await getPostComments(postId); |
| 278 | setComments(commentResponse.data); |
| 279 | } catch (error) { |
| 280 | console.error('回复评论失败:', error); |
| 281 | } |
| 282 | }; |
| 283 | |
| 284 | const addCommentAction = async (content) => { |
| 285 | try { |
| 286 | await addCommentToPost(postId, content); |
| 287 | setIsFlag(true); |
| 288 | const commentResponse = await getPostComments(postId); |
| 289 | setComments(commentResponse.data); |
| 290 | } catch (error) { |
| 291 | console.error('添加评论失败:', error); |
| 292 | } |
| 293 | }; |
| 294 | |
| 295 | if (loading) return <p>加载中...</p>; |
| 296 | if (errorMsg) return <p className="error-text">{errorMsg}</p>; |
| 297 | if (!post) return <p>没有找到该帖子。</p>; |
| 298 | |
| 299 | return ( |
| 300 | <div className="post-detail-page"> |
| 301 | <PostHeader post={post} /> |
| 302 | <PostContent content={post.content} /> |
| 303 | <PostActions post={post} onLike={handleLike} onFavorite={handleFavorite} /> |
| 304 | <CommentInput onSubmitComment={addCommentAction} isFlag={isFlag} /> |
| 305 | <CommentsList comments={comments} onLikeComment={likeCommentAction} onReplyComment={replyCommentAction} /> |
| 306 | </div> |
| 307 | ); |
| 308 | }; |
| 309 | |
| 310 | export default PostDetailPage; |