blob: 2e4874afbde82436cce442671ac6fd60b00662ad [file] [log] [blame]
22301009df48f962025-06-05 13:40:44 +08001import React, { useContext, useEffect, useState } from 'react';
22301009237217b2025-04-20 15:15:25 +08002import { useParams } from 'wouter';
3import { GoodTwo, Star } from '@icon-park/react';
22301009df48f962025-06-05 13:40:44 +08004import { getPostDetail, getPostComments, likePost, unlikePost, addCommentToPost, collectPost } from './api'; // 你的 API 函数
Krishya7ec1dd02025-04-19 15:29:03 +08005import './PostDetailPage.css';
22301009df48f962025-06-05 13:40:44 +08006import { UserContext } from '../../../context/UserContext'; // 用户上下文
Krishyac0f7e9b2025-04-22 15:28:28 +08007import Header from '../../../components/Header';
Krishya7ec1dd02025-04-19 15:29:03 +08008
22301009df48f962025-06-05 13:40:44 +08009const 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/
223010094158f3a2025-06-06 19:59:10 +080017 return `http://localhost:5011/uploads/post/${url.slice('/images/'.length)}`;
22301009df48f962025-06-05 13:40:44 +080018 }
19
20 // 其它情况默认直接拼接,不加斜杠
223010094158f3a2025-06-06 19:59:10 +080021 return `http://localhost:5011${url.startsWith('/') ? '' : '/'}${url}`;
22301009df48f962025-06-05 13:40:44 +080022};
23
24
25// 头像地址格式化,处理 avatarUrl 字段
26export function formatAvatarUrlNoDefault(avatarUrl) {
27 if (!avatarUrl) return '';
28 if (avatarUrl.startsWith('http')) return avatarUrl;
223010094158f3a2025-06-06 19:59:10 +080029 return `http://localhost:5011${avatarUrl}`;
22301009df48f962025-06-05 13:40:44 +080030}
31
Krishya7ec1dd02025-04-19 15:29:03 +080032const PostDetailPage = () => {
22301009df48f962025-06-05 13:40:44 +080033 const { postId } = useParams();
34 const { user } = useContext(UserContext);
35
22301009237217b2025-04-20 15:15:25 +080036 const [postDetail, setPostDetail] = useState(null);
37 const [comments, setComments] = useState([]);
38 const [loading, setLoading] = useState(true);
39 const [errorMsg, setErrorMsg] = useState('');
22301009df48f962025-06-05 13:40:44 +080040 const [newComment, setNewComment] = useState('');
41 const [isLiked, setIsLiked] = useState(false);
42 const [isCollected, setIsCollected] = useState(false);
43 const [replyToCommentId, setReplyToCommentId] = useState(null);
Krishya2283d882025-05-27 22:25:19 +080044 const [replyToUsername, setReplyToUsername] = useState(null);
Krishya7ec1dd02025-04-19 15:29:03 +080045
22301009237217b2025-04-20 15:15:25 +080046 useEffect(() => {
22301009df48f962025-06-05 13:40:44 +080047 const fetchData = async () => {
22301009237217b2025-04-20 15:15:25 +080048 setLoading(true);
49 setErrorMsg('');
50 try {
22301009237217b2025-04-20 15:15:25 +080051 const postData = await getPostDetail(postId);
52 setPostDetail(postData);
22301009237217b2025-04-20 15:15:25 +080053 const commentsData = await getPostComments(postId);
54 setComments(commentsData);
Krishya7ec1dd02025-04-19 15:29:03 +080055
22301009df48f962025-06-05 13:40:44 +080056 setIsLiked(!!postData.likedByUser);
57 setIsCollected(!!postData.collectedByUser);
22301009237217b2025-04-20 15:15:25 +080058 } catch (err) {
22301009df48f962025-06-05 13:40:44 +080059 console.error(err);
22301009237217b2025-04-20 15:15:25 +080060 setErrorMsg('加载失败,请稍后重试');
61 } finally {
62 setLoading(false);
63 }
Krishya7ec1dd02025-04-19 15:29:03 +080064 };
22301009df48f962025-06-05 13:40:44 +080065 fetchData();
22301009237217b2025-04-20 15:15:25 +080066 }, [postId]);
Krishya7ec1dd02025-04-19 15:29:03 +080067
22301009df48f962025-06-05 13:40:44 +080068 // 点赞
22301009237217b2025-04-20 15:15:25 +080069 const toggleLike = async () => {
22301009df48f962025-06-05 13:40:44 +080070 if (!user) return alert('请先登录');
22301009237217b2025-04-20 15:15:25 +080071 try {
72 if (isLiked) {
Krishya8f2fec82025-06-04 21:54:46 +080073 await unlikePost(postId, user.userId);
22301009237217b2025-04-20 15:15:25 +080074 setIsLiked(false);
22301009df48f962025-06-05 13:40:44 +080075 setPostDetail(prev => ({
22301009237217b2025-04-20 15:15:25 +080076 ...prev,
77 postLikeNum: prev.postLikeNum - 1,
78 }));
79 } else {
Krishya8f2fec82025-06-04 21:54:46 +080080 await likePost(postId, user.userId);
22301009237217b2025-04-20 15:15:25 +080081 setIsLiked(true);
22301009df48f962025-06-05 13:40:44 +080082 setPostDetail(prev => ({
22301009237217b2025-04-20 15:15:25 +080083 ...prev,
84 postLikeNum: prev.postLikeNum + 1,
85 }));
86 }
22301009df48f962025-06-05 13:40:44 +080087 } catch {
22301009237217b2025-04-20 15:15:25 +080088 alert('点赞失败,请稍后再试');
89 }
90 };
Krishya7ec1dd02025-04-19 15:29:03 +080091
22301009df48f962025-06-05 13:40:44 +080092 // 收藏
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('收藏失败,请稍后再试');
22301009237217b2025-04-20 15:15:25 +0800113 }
22301009df48f962025-06-05 13:40:44 +0800114 };
115
116 // 添加评论
117 const handleAddComment = async () => {
118 if (!user || !user.userId) return alert('请先登录后再评论');
119 if (!newComment.trim()) return alert('评论内容不能为空');
Krishya7ec1dd02025-04-19 15:29:03 +0800120
22301009237217b2025-04-20 15:15:25 +0800121 try {
22301009df48f962025-06-05 13:40:44 +0800122 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 || '评论失败,请稍后再试');
22301009237217b2025-04-20 15:15:25 +0800147 }
22301009df48f962025-06-05 13:40:44 +0800148 };
22301009237217b2025-04-20 15:15:25 +0800149
22301009df48f962025-06-05 13:40:44 +0800150 // 回复按钮点击
22301009237217b2025-04-20 15:15:25 +0800151 const handleReply = (commentId) => {
22301009df48f962025-06-05 13:40:44 +0800152 setReplyToCommentId(commentId);
153 const comment = comments.find(c => c.commentId === commentId);
154 setReplyToUsername(comment?.username || comment?.userId || '未知用户');
155 };
Krishya2283d882025-05-27 22:25:19 +0800156
22301009df48f962025-06-05 13:40:44 +0800157 // 查找回复的用户名
158 const findUsernameByCommentId = (id) => {
159 const comment = comments.find(c => c.commentId === id);
160 return comment ? (comment.username || comment.userId || '未知用户') : '未知用户';
161 };
Krishya2283d882025-05-27 22:25:19 +0800162
22301009df48f962025-06-05 13:40:44 +0800163 // 帖子图片处理,imgUrl 是单字符串,包装成数组
164 const getPostImages = () => {
165 if (!postDetail) return [];
166 if (postDetail.imgUrl) return [formatImageUrl(postDetail.imgUrl)];
167 return [];
168 };
22301009237217b2025-04-20 15:15:25 +0800169
170 return (
171 <div className="post-detail-page">
Krishyac0f7e9b2025-04-22 15:28:28 +0800172 <Header />
22301009237217b2025-04-20 15:15:25 +0800173 {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">
22301009df48f962025-06-05 13:40:44 +0800181 <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>
22301009237217b2025-04-20 15:15:25 +0800191 <span className="post-time">
22301009df48f962025-06-05 13:40:44 +0800192 发布时间:{new Date(postDetail.postTime).toLocaleString()}
22301009237217b2025-04-20 15:15:25 +0800193 </span>
22301009237217b2025-04-20 15:15:25 +0800194 </div>
22301009df48f962025-06-05 13:40:44 +0800195
22301009237217b2025-04-20 15:15:25 +0800196 <div className="post-content">
197 <p>{postDetail.postContent}</p>
22301009df48f962025-06-05 13:40:44 +0800198 <div className="post-images">
199 {getPostImages().map((url, idx) => (
200 <img key={idx} src={url} alt={`图片${idx + 1}`} />
201 ))}
202 </div>
22301009237217b2025-04-20 15:15:25 +0800203 </div>
204
22301009237217b2025-04-20 15:15:25 +0800205 <div className="post-actions">
22301009df48f962025-06-05 13:40:44 +0800206 <button className="icon-btn" onClick={toggleLike} title="点赞">
207 <GoodTwo theme="outline" size="20" fill={isLiked ? '#f00' : '#ccc'} />
22301009237217b2025-04-20 15:15:25 +0800208 <span>{postDetail.postLikeNum}</span>
209 </button>
22301009df48f962025-06-05 13:40:44 +0800210 <button className="icon-btn" onClick={toggleCollect} title="收藏">
211 <Star theme="outline" size="20" fill={isCollected ? '#ffd700' : '#ccc'} />
22301009237217b2025-04-20 15:15:25 +0800212 <span>{postDetail.postCollectNum}</span>
213 </button>
214 </div>
22301009df48f962025-06-05 13:40:44 +0800215
Krishya1300cad2025-04-20 22:16:45 +0800216 <hr className="divider" />
22301009df48f962025-06-05 13:40:44 +0800217
Krishya1300cad2025-04-20 22:16:45 +0800218 <h3>评论区</h3>
22301009df48f962025-06-05 13:40:44 +0800219 <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 || '用户头像'}
22301009237217b2025-04-20 15:15:25 +0800229 />
22301009df48f962025-06-05 13:40:44 +0800230 ) : null}
231 <span className="comment-username">{comment.username || '匿名用户'}</span>
232 </div>
233 <button className="reply-btn" onClick={() => handleReply(comment.commentId)}>回复</button>
22301009237217b2025-04-20 15:15:25 +0800234 </div>
22301009237217b2025-04-20 15:15:25 +0800235
22301009df48f962025-06-05 13:40:44 +0800236 <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>
Krishya2283d882025-05-27 22:25:19 +0800246
22301009df48f962025-06-05 13:40:44 +0800247 <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 )}
22301009237217b2025-04-20 15:15:25 +0800276 </div>
22301009df48f962025-06-05 13:40:44 +0800277 )) : <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 )}
22301009237217b2025-04-20 15:15:25 +0800291 </div>
Krishya7ec1dd02025-04-19 15:29:03 +0800292 </div>
22301009237217b2025-04-20 15:15:25 +0800293 ) : (
294 <p>帖子不存在</p>
295 )}
296 </div>
297 );
Krishya7ec1dd02025-04-19 15:29:03 +0800298};
299
22301009df48f962025-06-05 13:40:44 +0800300export default PostDetailPage;