blob: 417105a4441f5f8758de5e528b3446c4a3b7bfa7 [file] [log] [blame]
22301008ba662fe2025-06-20 18:10:20 +08001import React, { useState, useEffect, useCallback } from 'react'
95630366980c1f272025-06-20 14:08:54 +08002import { useParams, useNavigate } from 'react-router-dom'
3import { ArrowLeft, ThumbsUp, MessageCircle, Share2, BookmarkPlus, Heart, Eye } from 'lucide-react'
4import { searchAPI } from '../api/search_jwlll'
22301008ba662fe2025-06-20 18:10:20 +08005import { getUserInfo } from '../utils/auth'
22301008e25b4b02025-06-20 22:15:31 +08006import FollowButton from './FollowButton'
7import postsAPI from '../api/posts_api'
9563036699de8c092025-06-21 16:41:18 +08008import MediaPreview from './MediaPreview'
95630366980c1f272025-06-20 14:08:54 +08009import '../style/PostDetail.css'
223010083c35e492025-06-20 22:24:22 +080010import dayjs from 'dayjs'
223010696c849b42025-06-26 18:27:52 +080011import { followUser, unfollowUser } from '../api/api_ljc'
95630366980c1f272025-06-20 14:08:54 +080012
13export default function PostDetail() {
14 const { id } = useParams()
15 const navigate = useNavigate()
16 const [post, setPost] = useState(null)
17 const [loading, setLoading] = useState(true)
18 const [error, setError] = useState(null)
19 const [liked, setLiked] = useState(false)
20 const [bookmarked, setBookmarked] = useState(false)
21 const [likeCount, setLikeCount] = useState(0)
22 const [comments, setComments] = useState([])
23 const [newComment, setNewComment] = useState('')
24 const [showComments, setShowComments] = useState(false)
22301008e25b4b02025-06-20 22:15:31 +080025 const [isFollowing, setIsFollowing] = useState(false)
223010696c849b42025-06-26 18:27:52 +080026 const [followLoading, setFollowLoading] = useState(false)
22301008e25b4b02025-06-20 22:15:31 +080027 const [authorInfo, setAuthorInfo] = useState(null)
22301008c9805072025-06-20 22:38:02 +080028 const [previewImg, setPreviewImg] = useState(null)
22301008082d2ab2025-06-20 22:45:17 +080029 const [commentUserMap, setCommentUserMap] = useState({}) // user_id: username
223010696c849b42025-06-26 18:27:52 +080030 const [commentUserAvatarMap, setCommentUserAvatarMap] = useState({}) // user_id: avatar // 获取当前用户ID
22301008ba662fe2025-06-20 18:10:20 +080031 const getCurrentUserId = () => {
32 const userInfo = getUserInfo()
33 return userInfo?.id || '3' // 如果未登录或无用户信息,使用默认值3
34 }
22301008ba662fe2025-06-20 18:10:20 +080035 const fetchPostDetail = useCallback(async () => {
95630366980c1f272025-06-20 14:08:54 +080036 setLoading(true)
37 setError(null)
38 try {
39 const data = await searchAPI.getPostDetail(id)
40 setPost(data)
41 setLikeCount(data.heat || 0)
223010696c849b42025-06-26 18:27:52 +080042
43 // 检查当前用户是否已点赞
44 const currentUserId = getCurrentUserId()
45 try {
46 const hasLiked = await searchAPI.hasLiked(id, currentUserId)
47 setLiked(hasLiked)
48 } catch (error) {
49 console.error('检查点赞状态失败:', error)
50 setLiked(false) // 如果检查失败,默认为未点赞
51 }
95630366980c1f272025-06-20 14:08:54 +080052 } catch (error) {
53 console.error('获取帖子详情失败:', error)
54 setError('帖子不存在或已被删除')
55 } finally {
56 setLoading(false)
57 }
22301008ba662fe2025-06-20 18:10:20 +080058 }, [id])
95630366980c1f272025-06-20 14:08:54 +080059
22301008ba662fe2025-06-20 18:10:20 +080060 const fetchComments = useCallback(async () => {
95630366980c1f272025-06-20 14:08:54 +080061 try {
62 const data = await searchAPI.getComments(id)
63 setComments(data.comments || [])
64 } catch (error) {
65 console.error('获取评论失败:', error)
66 }
22301008ba662fe2025-06-20 18:10:20 +080067 }, [id])
68
22301008e25b4b02025-06-20 22:15:31 +080069 // 检查当前用户是否已关注发帖人
70 useEffect(() => {
71 if (post && post.user_id) {
72 // 这里假设有API postsAPI.getUserFollowing
73 const checkFollow = async () => {
74 try {
75 const userInfo = getUserInfo()
76 if (!userInfo?.id) return
77 const res = await postsAPI.getUserFollowing(userInfo.id)
78 if (Array.isArray(res)) {
79 setIsFollowing(res.some(u => u.id === post.user_id))
80 } else if (Array.isArray(res.following)) {
81 setIsFollowing(res.following.some(u => u.id === post.user_id))
82 }
83 } catch {}
84 }
85 checkFollow()
86 }
87 }, [post])
88
89 // 拉取发帖人信息
90 useEffect(() => {
91 if (post && post.user_id) {
92 postsAPI.getUser(post.user_id).then(res => setAuthorInfo(res || {})).catch(() => setAuthorInfo({}))
93 }
94 }, [post])
95
22301008082d2ab2025-06-20 22:45:17 +080096 // 拉取所有评论用户昵称
97 const fetchCommentUserNames = async (userIds) => {
98 const map = {}
99 await Promise.all(userIds.map(async uid => {
100 try {
101 const user = await postsAPI.getUser(uid)
102 map[uid] = user.username || user.nickname || `用户${uid}`
103 } catch {
104 map[uid] = `用户${uid}`
105 }
106 }))
107 setCommentUserMap(map)
108 }
109
110 // 拉取所有评论用户头像
111 const fetchCommentUserAvatars = async (userIds) => {
112 const map = {}
113 await Promise.all(userIds.map(async uid => {
114 try {
115 const user = await postsAPI.getUser(uid)
116 map[uid] = user.avatar && user.avatar.startsWith('http') ? user.avatar : (user.avatar ? `http://10.126.59.25:5715/static/${user.avatar}` : `https://i.pravatar.cc/40?img=${uid}`)
117 } catch {
118 map[uid] = `https://i.pravatar.cc/40?img=${uid}`
119 }
120 }))
121 setCommentUserAvatarMap(map)
122 }
123
22301008ba662fe2025-06-20 18:10:20 +0800124 useEffect(() => {
125 fetchPostDetail()
126 fetchComments()
127 }, [fetchPostDetail, fetchComments])
95630366980c1f272025-06-20 14:08:54 +0800128
22301008082d2ab2025-06-20 22:45:17 +0800129 // 评论区用户昵称和头像拉取
130 useEffect(() => {
131 if (comments.length > 0) {
132 const userIds = [...new Set(comments.map(c => c.user_id).filter(Boolean))]
133 fetchCommentUserNames(userIds)
134 fetchCommentUserAvatars(userIds)
135 }
136 }, [comments])
95630366980c1f272025-06-20 14:08:54 +0800137 const handleBack = () => {
138 navigate(-1)
139 }
95630366980c1f272025-06-20 14:08:54 +0800140 const handleLike = async () => {
223010696c849b42025-06-26 18:27:52 +0800141 const currentUserId = getCurrentUserId()
142 const originalLiked = liked
143 const originalLikeCount = likeCount
144
95630366980c1f272025-06-20 14:08:54 +0800145 try {
223010696c849b42025-06-26 18:27:52 +0800146 // 先乐观更新UI,提供即时反馈
95630366980c1f272025-06-20 14:08:54 +0800147 const newLiked = !liked
223010696c849b42025-06-26 18:27:52 +0800148 setLiked(newLiked)
149 setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
150
151 // 调用后端API
95630366980c1f272025-06-20 14:08:54 +0800152 if (newLiked) {
22301008ba662fe2025-06-20 18:10:20 +0800153 await searchAPI.likePost(id, currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800154 } else {
22301008ba662fe2025-06-20 18:10:20 +0800155 await searchAPI.unlikePost(id, currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800156 }
223010696c849b42025-06-26 18:27:52 +0800157
158 // API调用成功,状态已经更新,无需再次设置
95630366980c1f272025-06-20 14:08:54 +0800159 } catch (error) {
223010696c849b42025-06-26 18:27:52 +0800160 console.error('点赞操作失败:', error)
161
162 // 检查是否是可以忽略的错误
163 const errorMessage = error.message || error.toString()
164 const isIgnorableError = errorMessage.includes('already liked') ||
165 errorMessage.includes('not liked yet') ||
166 errorMessage.includes('already favorited') ||
167 errorMessage.includes('not favorited yet')
168
169 if (isIgnorableError) {
170 // 这些错误可以忽略,因为最终状态是正确的
171 console.log('忽略重复操作错误:', errorMessage)
172 return
173 }
174
175 // 发生真正的错误时回滚到原始状态
176 setLiked(originalLiked)
177 setLikeCount(originalLikeCount)
178 alert('操作失败,请重试')
95630366980c1f272025-06-20 14:08:54 +0800179 }
180 }
181
182 const handleBookmark = () => {
183 setBookmarked(!bookmarked)
184 // 实际项目中这里应该调用后端API保存收藏状态
185 }
186
187 const handleShare = () => {
188 // 分享功能
189 if (navigator.share) {
190 navigator.share({
191 title: post?.title,
192 text: post?.content,
193 url: window.location.href,
194 })
195 } else {
196 // 复制链接到剪贴板
197 navigator.clipboard.writeText(window.location.href)
198 alert('链接已复制到剪贴板')
199 }
200 }
95630366980c1f272025-06-20 14:08:54 +0800201 const handleAddComment = async (e) => {
202 e.preventDefault()
203 if (!newComment.trim()) return
204
205 try {
22301008ba662fe2025-06-20 18:10:20 +0800206 const currentUserId = getCurrentUserId()
207 await searchAPI.addComment(id, currentUserId, newComment)
95630366980c1f272025-06-20 14:08:54 +0800208 setNewComment('')
209 fetchComments() // 刷新评论列表
210 } catch (error) {
211 console.error('添加评论失败:', error)
212 alert('评论失败,请重试')
213 }
214 }
215
22301008e25b4b02025-06-20 22:15:31 +0800216 // 关注后刷新关注状态
223010696c849b42025-06-26 18:27:52 +0800217 const handleFollowAction = async () => {
218 // 添加了加载状态和错误处理
219 setFollowLoading(true);
220 const currentUserId = getCurrentUserId()
221 try {
222 if (isFollowing) {
223 await unfollowUser(currentUserId, post.user_id);
224 } else {
225 await followUser(currentUserId, post.user_id);
22301008e25b4b02025-06-20 22:15:31 +0800226 }
223010696c849b42025-06-26 18:27:52 +0800227 setIsFollowing(!isFollowing);
228 } catch (error) {
229 console.error(isFollowing ? '取消关注失败' : '关注失败', error);
230 alert(`操作失败: ${error.message || '请重试'}`);
231 } finally {
232 setFollowLoading(false);
22301008e25b4b02025-06-20 22:15:31 +0800233 }
223010696c849b42025-06-26 18:27:52 +0800234};
22301008e25b4b02025-06-20 22:15:31 +0800235
95630366980c1f272025-06-20 14:08:54 +0800236 if (loading) {
237 return (
238 <div className="post-detail">
239 <div className="loading-container">
240 <div className="loading-spinner"></div>
241 <p>加载中...</p>
242 </div>
243 </div>
244 )
245 }
246
22301008e25b4b02025-06-20 22:15:31 +0800247 // 优化错误和不存在的判断逻辑
95630366980c1f272025-06-20 14:08:54 +0800248 if (error) {
249 return (
250 <div className="post-detail">
251 <div className="error-container">
252 <h2>😔 出错了</h2>
253 <p>{error}</p>
254 <button onClick={handleBack} className="back-btn">
255 <ArrowLeft size={20} />
256 返回
257 </button>
258 </div>
259 </div>
260 )
261 }
262
22301008e25b4b02025-06-20 22:15:31 +0800263 // 只有明确为 null 或 undefined 时才显示不存在
264 if (post === null || post === undefined) {
95630366980c1f272025-06-20 14:08:54 +0800265 return (
266 <div className="post-detail">
267 <div className="error-container">
22301008e25b4b02025-06-20 22:15:31 +0800268 <h2>😔 帖子不存在或已被删除</h2>
95630366980c1f272025-06-20 14:08:54 +0800269 <p>该帖子可能已被删除或不存在</p>
270 <button onClick={handleBack} className="back-btn">
271 <ArrowLeft size={20} />
272 返回
273 </button>
274 </div>
275 </div>
276 )
277 }
278
279 return (
280 <div className="post-detail">
281 {/* 顶部导航栏 */}
282 <header className="post-header">
283 <button onClick={handleBack} className="back-btn">
284 <ArrowLeft size={20} />
285 返回
286 </button>
287 <div className="header-actions">
288 <button onClick={handleShare} className="action-btn">
289 <Share2 size={20} />
290 </button>
291 <button
292 onClick={handleBookmark}
293 className={`action-btn ${bookmarked ? 'active' : ''}`}
294 >
295 <BookmarkPlus size={20} />
296 </button>
297 </div>
298 </header>
299
300 {/* 主要内容区 */}
301 <main className="post-content">
302 {/* 帖子标题 */}
303 <h1 className="post-title">{post.title}</h1>
304
305 {/* 作者信息和元数据 */}
306 <div className="post-meta">
307 <div className="author-info">
308 <div className="avatar">
22301008e25b4b02025-06-20 22:15:31 +0800309 {authorInfo && authorInfo.avatar && authorInfo.avatar.startsWith('http') ? (
310 <img className="avatar" src={authorInfo.avatar} alt={authorInfo.username || authorInfo.nickname || post.author || '用户'} />
311 ) : (
312 <img className="avatar" src={`https://i.pravatar.cc/40?img=${post.user_id}`} alt={authorInfo?.username || authorInfo?.nickname || post.author || '用户'} />
313 )}
95630366980c1f272025-06-20 14:08:54 +0800314 </div>
315 <div className="author-details">
22301008e25b4b02025-06-20 22:15:31 +0800316 <span className="author-name">{authorInfo?.username || authorInfo?.nickname || post.author || '匿名用户'}</span>
95630366980c1f272025-06-20 14:08:54 +0800317 <span className="post-date">
223010083c35e492025-06-20 22:24:22 +0800318 {post.created_at ? dayjs(post.created_at).format('YYYY-MM-DD HH:mm:ss') : '未知时间'}
95630366980c1f272025-06-20 14:08:54 +0800319 </span>
22301008e25b4b02025-06-20 22:15:31 +0800320 {/* 关注按钮 */}
321 {post.user_id && (
223010696c849b42025-06-26 18:27:52 +0800322 <button
323 className={`follow-btn ${isFollowing ? 'following' : ''}`}
324 onClick={handleFollowAction}
325 disabled={followLoading}
326 style={{
327 marginLeft: '12px',
328 padding: '4px 12px',
329 borderRadius: '20px',
330 border: '1px solid #ccc',
331 background: isFollowing ? '#f0f0f0' : '#007bff',
332 color: isFollowing ? '#333' : 'white',
333 cursor: 'pointer',
334 fontSize: '14px'
335 }}
336 >
337 {followLoading ? '处理中...' : (isFollowing ? '已关注' : '关注')}
338 </button>
22301008e25b4b02025-06-20 22:15:31 +0800339 )}
95630366980c1f272025-06-20 14:08:54 +0800340 </div>
341 </div>
342 <div className="post-stats">
343 <span className="stat-item">
344 <Eye size={16} />
345 {post.views || 0}
346 </span>
347 <span className="stat-item">
348 <Heart size={16} />
349 {likeCount}
350 </span>
351 </div>
352 </div>
353
354 {/* 标签 */}
355 {post.tags && post.tags.length > 0 && (
356 <div className="post-tags">
357 {post.tags.map((tag, index) => (
358 <span key={index} className="tag">{tag}</span>
359 ))}
360 </div>
361 )}
362
9563036699de8c092025-06-21 16:41:18 +0800363 {/* 帖子媒体(支持多图/多视频) */}
22301008c9805072025-06-20 22:38:02 +0800364 {Array.isArray(post.media_urls) && post.media_urls.length > 0 && (
9563036699de8c092025-06-21 16:41:18 +0800365 <div className="post-media" style={{display:'flex',gap:8,marginBottom:16,flexWrap:'wrap'}}>
22301008c9805072025-06-20 22:38:02 +0800366 {post.media_urls.map((url, idx) => (
9563036699de8c092025-06-21 16:41:18 +0800367 <MediaPreview
368 key={idx}
369 url={url}
370 alt={`媒体${idx+1}`}
371 onClick={(mediaUrl) => {
372 // 对于图片,显示预览
373 if (!mediaUrl.toLowerCase().includes('video') && !mediaUrl.includes('.mp4') && !mediaUrl.includes('.webm')) {
374 setPreviewImg(mediaUrl)
375 }
376 }}
377 style={{ cursor: 'pointer' }}
378 maxWidth={320}
379 maxHeight={320}
380 />
22301008c9805072025-06-20 22:38:02 +0800381 ))}
382 </div>
383 )}
384 {/* 大图预览弹窗 */}
385 {previewImg && (
386 <div className="img-preview-mask" style={{position:'fixed',zIndex:9999,top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.7)',display:'flex',alignItems:'center',justifyContent:'center'}} onClick={()=>setPreviewImg(null)}>
387 <img src={previewImg} alt="大图预览" style={{maxWidth:'90vw',maxHeight:'90vh',borderRadius:12,boxShadow:'0 4px 24px #0008'}} />
388 </div>
389 )}
390
95630366980c1f272025-06-20 14:08:54 +0800391 {/* 帖子正文 */}
392 <div className="post-body">
393 <p>{post.content}</p>
394 </div>
395
396 {/* 类别信息 */}
397 {(post.category || post.type) && (
398 <div className="post-category">
399 {post.category && (
400 <>
401 <span className="category-label">分类:</span>
402 <span className="category-name">{post.category}</span>
403 </>
404 )}
405 {post.type && (
406 <>
407 <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
408 <span className="category-name">{post.type}</span>
409 </>
410 )}
411 </div>
412 )}
413
414 {/* 评论区 */}
415 <div className="comments-section">
416 <div className="comments-header">
417 <button
418 onClick={() => setShowComments(!showComments)}
419 className="comments-toggle"
420 >
421 <MessageCircle size={20} />
422 评论 ({comments.length})
423 </button>
424 </div>
425
426 {showComments && (
427 <div className="comments-content">
428 {/* 添加评论 */}
429 <form onSubmit={handleAddComment} className="comment-form">
430 <textarea
431 value={newComment}
432 onChange={(e) => setNewComment(e.target.value)}
433 placeholder="写下你的评论..."
434 className="comment-input"
435 rows={3}
436 />
437 <button type="submit" className="comment-submit">
438 发布评论
439 </button>
440 </form>
441
442 {/* 评论列表 */}
443 <div className="comments-list">
444 {comments.length === 0 ? (
445 <p className="no-comments">暂无评论</p>
446 ) : (
447 comments.map((comment, index) => (
448 <div key={index} className="comment-item">
449 <div className="comment-author">
450 <div className="comment-avatar">
22301008082d2ab2025-06-20 22:45:17 +0800451 <img className="avatar" src={commentUserAvatarMap[comment.user_id] || `https://i.pravatar.cc/40?img=${comment.user_id}`} alt={commentUserMap[comment.user_id] || comment.user_name || '用户'} />
95630366980c1f272025-06-20 14:08:54 +0800452 </div>
22301008082d2ab2025-06-20 22:45:17 +0800453 <span className="comment-name">{commentUserMap[comment.user_id] || comment.user_name || '匿名用户'}</span>
95630366980c1f272025-06-20 14:08:54 +0800454 <span className="comment-time">
455 {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
456 </span>
457 </div>
458 <div className="comment-content">
459 {comment.content}
460 </div>
461 </div>
462 ))
463 )}
464 </div>
465 </div>
466 )}
467 </div>
468 </main>
469
470 {/* 底部操作栏 */}
471 <footer className="post-footer">
472 <div className="action-bar">
473 <button
474 onClick={handleLike}
475 className={`action-button ${liked ? 'liked' : ''}`}
476 >
477 <ThumbsUp size={20} />
478 <span>{likeCount}</span>
479 </button>
480
481 <button
482 onClick={() => setShowComments(!showComments)}
483 className="action-button"
484 >
485 <MessageCircle size={20} />
486 <span>评论</span>
487 </button>
488
489 <button onClick={handleShare} className="action-button">
490 <Share2 size={20} />
491 <span>分享</span>
492 </button>
493
494 <button
495 onClick={handleBookmark}
496 className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
497 >
498 <BookmarkPlus size={20} />
499 <span>收藏</span>
500 </button>
501 </div>
502 </footer>
503 </div>
504 )
505}