blob: d598a72db8a84fddece3eb9a6bffba8be476e4ee [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'
95630366980c1f272025-06-20 14:08:54 +08008import '../style/PostDetail.css'
223010083c35e492025-06-20 22:24:22 +08009import dayjs from 'dayjs'
95630366980c1f272025-06-20 14:08:54 +080010
11export default function PostDetail() {
12 const { id } = useParams()
13 const navigate = useNavigate()
14 const [post, setPost] = useState(null)
15 const [loading, setLoading] = useState(true)
16 const [error, setError] = useState(null)
17 const [liked, setLiked] = useState(false)
18 const [bookmarked, setBookmarked] = useState(false)
19 const [likeCount, setLikeCount] = useState(0)
20 const [comments, setComments] = useState([])
21 const [newComment, setNewComment] = useState('')
22 const [showComments, setShowComments] = useState(false)
22301008e25b4b02025-06-20 22:15:31 +080023 const [isFollowing, setIsFollowing] = useState(false)
24 const [authorInfo, setAuthorInfo] = useState(null)
22301008c9805072025-06-20 22:38:02 +080025 const [previewImg, setPreviewImg] = useState(null)
22301008082d2ab2025-06-20 22:45:17 +080026 const [commentUserMap, setCommentUserMap] = useState({}) // user_id: username
27 const [commentUserAvatarMap, setCommentUserAvatarMap] = useState({}) // user_id: avatar
22301008ba662fe2025-06-20 18:10:20 +080028 // 获取当前用户ID
29 const getCurrentUserId = () => {
30 const userInfo = getUserInfo()
31 return userInfo?.id || '3' // 如果未登录或无用户信息,使用默认值3
32 }
95630366980c1f272025-06-20 14:08:54 +080033
22301008ba662fe2025-06-20 18:10:20 +080034 const fetchPostDetail = useCallback(async () => {
95630366980c1f272025-06-20 14:08:54 +080035 setLoading(true)
36 setError(null)
37 try {
38 const data = await searchAPI.getPostDetail(id)
39 setPost(data)
40 setLikeCount(data.heat || 0)
41 } catch (error) {
42 console.error('获取帖子详情失败:', error)
43 setError('帖子不存在或已被删除')
44 } finally {
45 setLoading(false)
46 }
22301008ba662fe2025-06-20 18:10:20 +080047 }, [id])
95630366980c1f272025-06-20 14:08:54 +080048
22301008ba662fe2025-06-20 18:10:20 +080049 const fetchComments = useCallback(async () => {
95630366980c1f272025-06-20 14:08:54 +080050 try {
51 const data = await searchAPI.getComments(id)
52 setComments(data.comments || [])
53 } catch (error) {
54 console.error('获取评论失败:', error)
55 }
22301008ba662fe2025-06-20 18:10:20 +080056 }, [id])
57
22301008e25b4b02025-06-20 22:15:31 +080058 // 检查当前用户是否已关注发帖人
59 useEffect(() => {
60 if (post && post.user_id) {
61 // 这里假设有API postsAPI.getUserFollowing
62 const checkFollow = async () => {
63 try {
64 const userInfo = getUserInfo()
65 if (!userInfo?.id) return
66 const res = await postsAPI.getUserFollowing(userInfo.id)
67 if (Array.isArray(res)) {
68 setIsFollowing(res.some(u => u.id === post.user_id))
69 } else if (Array.isArray(res.following)) {
70 setIsFollowing(res.following.some(u => u.id === post.user_id))
71 }
72 } catch {}
73 }
74 checkFollow()
75 }
76 }, [post])
77
78 // 拉取发帖人信息
79 useEffect(() => {
80 if (post && post.user_id) {
81 postsAPI.getUser(post.user_id).then(res => setAuthorInfo(res || {})).catch(() => setAuthorInfo({}))
82 }
83 }, [post])
84
22301008082d2ab2025-06-20 22:45:17 +080085 // 拉取所有评论用户昵称
86 const fetchCommentUserNames = async (userIds) => {
87 const map = {}
88 await Promise.all(userIds.map(async uid => {
89 try {
90 const user = await postsAPI.getUser(uid)
91 map[uid] = user.username || user.nickname || `用户${uid}`
92 } catch {
93 map[uid] = `用户${uid}`
94 }
95 }))
96 setCommentUserMap(map)
97 }
98
99 // 拉取所有评论用户头像
100 const fetchCommentUserAvatars = async (userIds) => {
101 const map = {}
102 await Promise.all(userIds.map(async uid => {
103 try {
104 const user = await postsAPI.getUser(uid)
105 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}`)
106 } catch {
107 map[uid] = `https://i.pravatar.cc/40?img=${uid}`
108 }
109 }))
110 setCommentUserAvatarMap(map)
111 }
112
22301008ba662fe2025-06-20 18:10:20 +0800113 useEffect(() => {
114 fetchPostDetail()
115 fetchComments()
116 }, [fetchPostDetail, fetchComments])
95630366980c1f272025-06-20 14:08:54 +0800117
22301008082d2ab2025-06-20 22:45:17 +0800118 // 评论区用户昵称和头像拉取
119 useEffect(() => {
120 if (comments.length > 0) {
121 const userIds = [...new Set(comments.map(c => c.user_id).filter(Boolean))]
122 fetchCommentUserNames(userIds)
123 fetchCommentUserAvatars(userIds)
124 }
125 }, [comments])
126
95630366980c1f272025-06-20 14:08:54 +0800127 const handleBack = () => {
128 navigate(-1)
129 }
95630366980c1f272025-06-20 14:08:54 +0800130 const handleLike = async () => {
131 try {
22301008ba662fe2025-06-20 18:10:20 +0800132 const currentUserId = getCurrentUserId()
95630366980c1f272025-06-20 14:08:54 +0800133 const newLiked = !liked
134 if (newLiked) {
22301008ba662fe2025-06-20 18:10:20 +0800135 await searchAPI.likePost(id, currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800136 } else {
22301008ba662fe2025-06-20 18:10:20 +0800137 await searchAPI.unlikePost(id, currentUserId)
95630366980c1f272025-06-20 14:08:54 +0800138 }
139 setLiked(newLiked)
140 setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
141 } catch (error) {
142 console.error('点赞失败:', error)
143 // 回滚状态
144 setLiked(!liked)
145 setLikeCount(prev => liked ? prev + 1 : prev - 1)
146 }
147 }
148
149 const handleBookmark = () => {
150 setBookmarked(!bookmarked)
151 // 实际项目中这里应该调用后端API保存收藏状态
152 }
153
154 const handleShare = () => {
155 // 分享功能
156 if (navigator.share) {
157 navigator.share({
158 title: post?.title,
159 text: post?.content,
160 url: window.location.href,
161 })
162 } else {
163 // 复制链接到剪贴板
164 navigator.clipboard.writeText(window.location.href)
165 alert('链接已复制到剪贴板')
166 }
167 }
95630366980c1f272025-06-20 14:08:54 +0800168 const handleAddComment = async (e) => {
169 e.preventDefault()
170 if (!newComment.trim()) return
171
172 try {
22301008ba662fe2025-06-20 18:10:20 +0800173 const currentUserId = getCurrentUserId()
174 await searchAPI.addComment(id, currentUserId, newComment)
95630366980c1f272025-06-20 14:08:54 +0800175 setNewComment('')
176 fetchComments() // 刷新评论列表
177 } catch (error) {
178 console.error('添加评论失败:', error)
179 alert('评论失败,请重试')
180 }
181 }
182
22301008e25b4b02025-06-20 22:15:31 +0800183 // 关注后刷新关注状态
184 const handleFollowChange = async (followed) => {
185 setIsFollowing(followed)
186 // 关注/取关后重新拉取一次关注状态,保证和数据库同步
187 if (post && post.user_id) {
188 try {
189 const userInfo = getUserInfo()
190 if (!userInfo?.id) return
191 const res = await postsAPI.getUserFollowing(userInfo.id)
192 if (Array.isArray(res)) {
193 setIsFollowing(res.some(u => u.id === post.user_id))
194 } else if (Array.isArray(res.following)) {
195 setIsFollowing(res.following.some(u => u.id === post.user_id))
196 }
197 } catch {}
198 }
199 }
200
95630366980c1f272025-06-20 14:08:54 +0800201 if (loading) {
202 return (
203 <div className="post-detail">
204 <div className="loading-container">
205 <div className="loading-spinner"></div>
206 <p>加载中...</p>
207 </div>
208 </div>
209 )
210 }
211
22301008e25b4b02025-06-20 22:15:31 +0800212 // 优化错误和不存在的判断逻辑
95630366980c1f272025-06-20 14:08:54 +0800213 if (error) {
214 return (
215 <div className="post-detail">
216 <div className="error-container">
217 <h2>😔 出错了</h2>
218 <p>{error}</p>
219 <button onClick={handleBack} className="back-btn">
220 <ArrowLeft size={20} />
221 返回
222 </button>
223 </div>
224 </div>
225 )
226 }
227
22301008e25b4b02025-06-20 22:15:31 +0800228 // 只有明确为 null 或 undefined 时才显示不存在
229 if (post === null || post === undefined) {
95630366980c1f272025-06-20 14:08:54 +0800230 return (
231 <div className="post-detail">
232 <div className="error-container">
22301008e25b4b02025-06-20 22:15:31 +0800233 <h2>😔 帖子不存在或已被删除</h2>
95630366980c1f272025-06-20 14:08:54 +0800234 <p>该帖子可能已被删除或不存在</p>
235 <button onClick={handleBack} className="back-btn">
236 <ArrowLeft size={20} />
237 返回
238 </button>
239 </div>
240 </div>
241 )
242 }
243
244 return (
245 <div className="post-detail">
246 {/* 顶部导航栏 */}
247 <header className="post-header">
248 <button onClick={handleBack} className="back-btn">
249 <ArrowLeft size={20} />
250 返回
251 </button>
252 <div className="header-actions">
253 <button onClick={handleShare} className="action-btn">
254 <Share2 size={20} />
255 </button>
256 <button
257 onClick={handleBookmark}
258 className={`action-btn ${bookmarked ? 'active' : ''}`}
259 >
260 <BookmarkPlus size={20} />
261 </button>
262 </div>
263 </header>
264
265 {/* 主要内容区 */}
266 <main className="post-content">
267 {/* 帖子标题 */}
268 <h1 className="post-title">{post.title}</h1>
269
270 {/* 作者信息和元数据 */}
271 <div className="post-meta">
272 <div className="author-info">
273 <div className="avatar">
22301008e25b4b02025-06-20 22:15:31 +0800274 {authorInfo && authorInfo.avatar && authorInfo.avatar.startsWith('http') ? (
275 <img className="avatar" src={authorInfo.avatar} alt={authorInfo.username || authorInfo.nickname || post.author || '用户'} />
276 ) : (
277 <img className="avatar" src={`https://i.pravatar.cc/40?img=${post.user_id}`} alt={authorInfo?.username || authorInfo?.nickname || post.author || '用户'} />
278 )}
95630366980c1f272025-06-20 14:08:54 +0800279 </div>
280 <div className="author-details">
22301008e25b4b02025-06-20 22:15:31 +0800281 <span className="author-name">{authorInfo?.username || authorInfo?.nickname || post.author || '匿名用户'}</span>
95630366980c1f272025-06-20 14:08:54 +0800282 <span className="post-date">
223010083c35e492025-06-20 22:24:22 +0800283 {post.created_at ? dayjs(post.created_at).format('YYYY-MM-DD HH:mm:ss') : '未知时间'}
95630366980c1f272025-06-20 14:08:54 +0800284 </span>
22301008e25b4b02025-06-20 22:15:31 +0800285 {/* 关注按钮 */}
286 {post.user_id && (
287 <FollowButton
288 userId={post.user_id}
289 isFollowing={isFollowing}
290 onFollowChange={handleFollowChange}
291 style={{marginLeft: 12}}
292 />
293 )}
95630366980c1f272025-06-20 14:08:54 +0800294 </div>
295 </div>
296 <div className="post-stats">
297 <span className="stat-item">
298 <Eye size={16} />
299 {post.views || 0}
300 </span>
301 <span className="stat-item">
302 <Heart size={16} />
303 {likeCount}
304 </span>
305 </div>
306 </div>
307
308 {/* 标签 */}
309 {post.tags && post.tags.length > 0 && (
310 <div className="post-tags">
311 {post.tags.map((tag, index) => (
312 <span key={index} className="tag">{tag}</span>
313 ))}
314 </div>
315 )}
316
22301008c9805072025-06-20 22:38:02 +0800317 {/* 帖子图片(支持多图) */}
318 {Array.isArray(post.media_urls) && post.media_urls.length > 0 && (
319 <div className="post-images" style={{display:'flex',gap:8,marginBottom:16}}>
320 {post.media_urls.map((url, idx) => (
321 <img key={idx} src={url} alt={`图片${idx+1}`} style={{maxWidth:220,maxHeight:220,borderRadius:8,objectFit:'cover',cursor:'pointer'}} onClick={() => setPreviewImg(url)} />
322 ))}
323 </div>
324 )}
325 {/* 大图预览弹窗 */}
326 {previewImg && (
327 <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)}>
328 <img src={previewImg} alt="大图预览" style={{maxWidth:'90vw',maxHeight:'90vh',borderRadius:12,boxShadow:'0 4px 24px #0008'}} />
329 </div>
330 )}
331
95630366980c1f272025-06-20 14:08:54 +0800332 {/* 帖子正文 */}
333 <div className="post-body">
334 <p>{post.content}</p>
335 </div>
336
337 {/* 类别信息 */}
338 {(post.category || post.type) && (
339 <div className="post-category">
340 {post.category && (
341 <>
342 <span className="category-label">分类:</span>
343 <span className="category-name">{post.category}</span>
344 </>
345 )}
346 {post.type && (
347 <>
348 <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
349 <span className="category-name">{post.type}</span>
350 </>
351 )}
352 </div>
353 )}
354
355 {/* 评论区 */}
356 <div className="comments-section">
357 <div className="comments-header">
358 <button
359 onClick={() => setShowComments(!showComments)}
360 className="comments-toggle"
361 >
362 <MessageCircle size={20} />
363 评论 ({comments.length})
364 </button>
365 </div>
366
367 {showComments && (
368 <div className="comments-content">
369 {/* 添加评论 */}
370 <form onSubmit={handleAddComment} className="comment-form">
371 <textarea
372 value={newComment}
373 onChange={(e) => setNewComment(e.target.value)}
374 placeholder="写下你的评论..."
375 className="comment-input"
376 rows={3}
377 />
378 <button type="submit" className="comment-submit">
379 发布评论
380 </button>
381 </form>
382
383 {/* 评论列表 */}
384 <div className="comments-list">
385 {comments.length === 0 ? (
386 <p className="no-comments">暂无评论</p>
387 ) : (
388 comments.map((comment, index) => (
389 <div key={index} className="comment-item">
390 <div className="comment-author">
391 <div className="comment-avatar">
22301008082d2ab2025-06-20 22:45:17 +0800392 <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 +0800393 </div>
22301008082d2ab2025-06-20 22:45:17 +0800394 <span className="comment-name">{commentUserMap[comment.user_id] || comment.user_name || '匿名用户'}</span>
95630366980c1f272025-06-20 14:08:54 +0800395 <span className="comment-time">
396 {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
397 </span>
398 </div>
399 <div className="comment-content">
400 {comment.content}
401 </div>
402 </div>
403 ))
404 )}
405 </div>
406 </div>
407 )}
408 </div>
409 </main>
410
411 {/* 底部操作栏 */}
412 <footer className="post-footer">
413 <div className="action-bar">
414 <button
415 onClick={handleLike}
416 className={`action-button ${liked ? 'liked' : ''}`}
417 >
418 <ThumbsUp size={20} />
419 <span>{likeCount}</span>
420 </button>
421
422 <button
423 onClick={() => setShowComments(!showComments)}
424 className="action-button"
425 >
426 <MessageCircle size={20} />
427 <span>评论</span>
428 </button>
429
430 <button onClick={handleShare} className="action-button">
431 <Share2 size={20} />
432 <span>分享</span>
433 </button>
434
435 <button
436 onClick={handleBookmark}
437 className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
438 >
439 <BookmarkPlus size={20} />
440 <span>收藏</span>
441 </button>
442 </div>
443 </footer>
444 </div>
445 )
446}