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