blob: c0f218f36109fcc71cb1c8d12d44633186a5992d [file] [log] [blame]
Krishya7ec1dd02025-04-19 15:29:03 +08001import React, { useState, useEffect } from 'react';
2import { useRoute } from 'wouter';
3import {
4 getPostDetail,
5 getPostComments,
6 likePost,
7 unlikePost,
8 addCommentToPost,
9 replyToComment,
10 likeComment,
11 unlikeComment,
12 getUserInfo
13} from './api';
14import './PostDetailPage.css';
15import axios from 'axios';
16
17const API_BASE = process.env.REACT_APP_API_BASE;
18
19const 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
32const PostContent = ({ content }) => {
33 return (
34 <div className="post-content" dangerouslySetInnerHTML={{ __html: content }} />
35 );
36};
37
38const 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
53const 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
90const 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
171const 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
187const 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
310export default PostDetailPage;