blob: bf0f0627d1d3666a5d570ee6e649996d49e44401 [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';
22301009207e2db2025-06-09 00:27:28 +08008import AuthButton from '../../../components/AuthButton';
Krishya7ec1dd02025-04-19 15:29:03 +08009
22301009df48f962025-06-05 13:40:44 +080010const 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/
223010094158f3a2025-06-06 19:59:10 +080018 return `http://localhost:5011/uploads/post/${url.slice('/images/'.length)}`;
22301009df48f962025-06-05 13:40:44 +080019 }
20
21 // 其它情况默认直接拼接,不加斜杠
223010094158f3a2025-06-06 19:59:10 +080022 return `http://localhost:5011${url.startsWith('/') ? '' : '/'}${url}`;
22301009df48f962025-06-05 13:40:44 +080023};
24
25
26// 头像地址格式化,处理 avatarUrl 字段
27export function formatAvatarUrlNoDefault(avatarUrl) {
28 if (!avatarUrl) return '';
29 if (avatarUrl.startsWith('http')) return avatarUrl;
223010094158f3a2025-06-06 19:59:10 +080030 return `http://localhost:5011${avatarUrl}`;
22301009df48f962025-06-05 13:40:44 +080031}
32
Krishya7ec1dd02025-04-19 15:29:03 +080033const PostDetailPage = () => {
22301009df48f962025-06-05 13:40:44 +080034 const { postId } = useParams();
35 const { user } = useContext(UserContext);
36
22301009237217b2025-04-20 15:15:25 +080037 const [postDetail, setPostDetail] = useState(null);
38 const [comments, setComments] = useState([]);
39 const [loading, setLoading] = useState(true);
40 const [errorMsg, setErrorMsg] = useState('');
22301009df48f962025-06-05 13:40:44 +080041 const [newComment, setNewComment] = useState('');
42 const [isLiked, setIsLiked] = useState(false);
43 const [isCollected, setIsCollected] = useState(false);
44 const [replyToCommentId, setReplyToCommentId] = useState(null);
Krishya2283d882025-05-27 22:25:19 +080045 const [replyToUsername, setReplyToUsername] = useState(null);
Krishya7ec1dd02025-04-19 15:29:03 +080046
22301009237217b2025-04-20 15:15:25 +080047 useEffect(() => {
22301009df48f962025-06-05 13:40:44 +080048 const fetchData = async () => {
22301009237217b2025-04-20 15:15:25 +080049 setLoading(true);
50 setErrorMsg('');
51 try {
22301009237217b2025-04-20 15:15:25 +080052 const postData = await getPostDetail(postId);
53 setPostDetail(postData);
22301009237217b2025-04-20 15:15:25 +080054 const commentsData = await getPostComments(postId);
55 setComments(commentsData);
Krishya7ec1dd02025-04-19 15:29:03 +080056
22301009df48f962025-06-05 13:40:44 +080057 setIsLiked(!!postData.likedByUser);
58 setIsCollected(!!postData.collectedByUser);
22301009237217b2025-04-20 15:15:25 +080059 } catch (err) {
22301009df48f962025-06-05 13:40:44 +080060 console.error(err);
22301009237217b2025-04-20 15:15:25 +080061 setErrorMsg('加载失败,请稍后重试');
62 } finally {
63 setLoading(false);
64 }
Krishya7ec1dd02025-04-19 15:29:03 +080065 };
22301009df48f962025-06-05 13:40:44 +080066 fetchData();
22301009237217b2025-04-20 15:15:25 +080067 }, [postId]);
Krishya7ec1dd02025-04-19 15:29:03 +080068
22301009df48f962025-06-05 13:40:44 +080069 // 点赞
22301009237217b2025-04-20 15:15:25 +080070 const toggleLike = async () => {
22301009df48f962025-06-05 13:40:44 +080071 if (!user) return alert('请先登录');
22301009237217b2025-04-20 15:15:25 +080072 try {
73 if (isLiked) {
Krishya8f2fec82025-06-04 21:54:46 +080074 await unlikePost(postId, user.userId);
22301009237217b2025-04-20 15:15:25 +080075 setIsLiked(false);
22301009df48f962025-06-05 13:40:44 +080076 setPostDetail(prev => ({
22301009237217b2025-04-20 15:15:25 +080077 ...prev,
78 postLikeNum: prev.postLikeNum - 1,
79 }));
80 } else {
Krishya8f2fec82025-06-04 21:54:46 +080081 await likePost(postId, user.userId);
22301009237217b2025-04-20 15:15:25 +080082 setIsLiked(true);
22301009df48f962025-06-05 13:40:44 +080083 setPostDetail(prev => ({
22301009237217b2025-04-20 15:15:25 +080084 ...prev,
85 postLikeNum: prev.postLikeNum + 1,
86 }));
87 }
22301009df48f962025-06-05 13:40:44 +080088 } catch {
22301009237217b2025-04-20 15:15:25 +080089 alert('点赞失败,请稍后再试');
90 }
91 };
Krishya7ec1dd02025-04-19 15:29:03 +080092
22301009df48f962025-06-05 13:40:44 +080093 // 收藏
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('收藏失败,请稍后再试');
22301009237217b2025-04-20 15:15:25 +0800114 }
22301009df48f962025-06-05 13:40:44 +0800115 };
116
117 // 添加评论
118 const handleAddComment = async () => {
119 if (!user || !user.userId) return alert('请先登录后再评论');
120 if (!newComment.trim()) return alert('评论内容不能为空');
Krishya7ec1dd02025-04-19 15:29:03 +0800121
22301009237217b2025-04-20 15:15:25 +0800122 try {
22301009df48f962025-06-05 13:40:44 +0800123 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 || '评论失败,请稍后再试');
22301009237217b2025-04-20 15:15:25 +0800148 }
22301009df48f962025-06-05 13:40:44 +0800149 };
22301009237217b2025-04-20 15:15:25 +0800150
22301009df48f962025-06-05 13:40:44 +0800151 // 回复按钮点击
22301009237217b2025-04-20 15:15:25 +0800152 const handleReply = (commentId) => {
22301009df48f962025-06-05 13:40:44 +0800153 setReplyToCommentId(commentId);
154 const comment = comments.find(c => c.commentId === commentId);
155 setReplyToUsername(comment?.username || comment?.userId || '未知用户');
156 };
Krishya2283d882025-05-27 22:25:19 +0800157
22301009df48f962025-06-05 13:40:44 +0800158 // 查找回复的用户名
159 const findUsernameByCommentId = (id) => {
160 const comment = comments.find(c => c.commentId === id);
161 return comment ? (comment.username || comment.userId || '未知用户') : '未知用户';
162 };
Krishya2283d882025-05-27 22:25:19 +0800163
22301009df48f962025-06-05 13:40:44 +0800164 // 帖子图片处理,imgUrl 是单字符串,包装成数组
165 const getPostImages = () => {
166 if (!postDetail) return [];
167 if (postDetail.imgUrl) return [formatImageUrl(postDetail.imgUrl)];
168 return [];
169 };
22301009237217b2025-04-20 15:15:25 +0800170
171 return (
172 <div className="post-detail-page">
Krishyac0f7e9b2025-04-22 15:28:28 +0800173 <Header />
22301009237217b2025-04-20 15:15:25 +0800174 {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">
22301009df48f962025-06-05 13:40:44 +0800182 <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>
22301009237217b2025-04-20 15:15:25 +0800192 <span className="post-time">
22301009df48f962025-06-05 13:40:44 +0800193 发布时间:{new Date(postDetail.postTime).toLocaleString()}
22301009237217b2025-04-20 15:15:25 +0800194 </span>
22301009237217b2025-04-20 15:15:25 +0800195 </div>
22301009df48f962025-06-05 13:40:44 +0800196
22301009237217b2025-04-20 15:15:25 +0800197 <div className="post-content">
198 <p>{postDetail.postContent}</p>
22301009df48f962025-06-05 13:40:44 +0800199 <div className="post-images">
200 {getPostImages().map((url, idx) => (
201 <img key={idx} src={url} alt={`图片${idx + 1}`} />
202 ))}
203 </div>
22301009237217b2025-04-20 15:15:25 +0800204 </div>
205
22301009237217b2025-04-20 15:15:25 +0800206 <div className="post-actions">
22301009df48f962025-06-05 13:40:44 +0800207 <button className="icon-btn" onClick={toggleLike} title="点赞">
208 <GoodTwo theme="outline" size="20" fill={isLiked ? '#f00' : '#ccc'} />
22301009237217b2025-04-20 15:15:25 +0800209 <span>{postDetail.postLikeNum}</span>
210 </button>
22301009df48f962025-06-05 13:40:44 +0800211 <button className="icon-btn" onClick={toggleCollect} title="收藏">
212 <Star theme="outline" size="20" fill={isCollected ? '#ffd700' : '#ccc'} />
22301009237217b2025-04-20 15:15:25 +0800213 <span>{postDetail.postCollectNum}</span>
214 </button>
215 </div>
22301009df48f962025-06-05 13:40:44 +0800216
Krishya1300cad2025-04-20 22:16:45 +0800217 <hr className="divider" />
22301009df48f962025-06-05 13:40:44 +0800218
Krishya1300cad2025-04-20 22:16:45 +0800219 <h3>评论区</h3>
22301009df48f962025-06-05 13:40:44 +0800220 <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 || '用户头像'}
22301009237217b2025-04-20 15:15:25 +0800230 />
22301009df48f962025-06-05 13:40:44 +0800231 ) : null}
232 <span className="comment-username">{comment.username || '匿名用户'}</span>
233 </div>
234 <button className="reply-btn" onClick={() => handleReply(comment.commentId)}>回复</button>
22301009237217b2025-04-20 15:15:25 +0800235 </div>
22301009237217b2025-04-20 15:15:25 +0800236
22301009df48f962025-06-05 13:40:44 +0800237 <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>
Krishya2283d882025-05-27 22:25:19 +0800247
22301009df48f962025-06-05 13:40:44 +0800248 <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">
22301009207e2db2025-06-09 00:27:28 +0800263 <AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}>
264 发布回复
265 </AuthButton>
266
22301009df48f962025-06-05 13:40:44 +0800267 <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 )}
22301009237217b2025-04-20 15:15:25 +0800280 </div>
22301009df48f962025-06-05 13:40:44 +0800281 )) : <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">
22301009207e2db2025-06-09 00:27:28 +0800291 <AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}>
292 发布评论
293 </AuthButton>
294
22301009df48f962025-06-05 13:40:44 +0800295 </div>
296 </div>
297 )}
22301009237217b2025-04-20 15:15:25 +0800298 </div>
Krishya7ec1dd02025-04-19 15:29:03 +0800299 </div>
22301009237217b2025-04-20 15:15:25 +0800300 ) : (
301 <p>帖子不存在</p>
302 )}
303 </div>
304 );
Krishya7ec1dd02025-04-19 15:29:03 +0800305};
306
22301009df48f962025-06-05 13:40:44 +0800307export default PostDetailPage;