blob: 2745d79ff34aaca39e53594ee9f6c33b6a6625b4 [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)
22301008ba662fe2025-06-20 18:10:20 +080026 // 获取当前用户ID
27 const getCurrentUserId = () => {
28 const userInfo = getUserInfo()
29 return userInfo?.id || '3' // 如果未登录或无用户信息,使用默认值3
30 }
95630366980c1f272025-06-20 14:08:54 +080031
22301008ba662fe2025-06-20 18:10:20 +080032 const fetchPostDetail = useCallback(async () => {
95630366980c1f272025-06-20 14:08:54 +080033 setLoading(true)
34 setError(null)
35 try {
36 const data = await searchAPI.getPostDetail(id)
37 setPost(data)
38 setLikeCount(data.heat || 0)
39 } catch (error) {
40 console.error('获取帖子详情失败:', error)
41 setError('帖子不存在或已被删除')
42 } finally {
43 setLoading(false)
44 }
22301008ba662fe2025-06-20 18:10:20 +080045 }, [id])
95630366980c1f272025-06-20 14:08:54 +080046
22301008ba662fe2025-06-20 18:10:20 +080047 const fetchComments = useCallback(async () => {
95630366980c1f272025-06-20 14:08:54 +080048 try {
49 const data = await searchAPI.getComments(id)
50 setComments(data.comments || [])
51 } catch (error) {
52 console.error('获取评论失败:', error)
53 }
22301008ba662fe2025-06-20 18:10:20 +080054 }, [id])
55
22301008e25b4b02025-06-20 22:15:31 +080056 // 检查当前用户是否已关注发帖人
57 useEffect(() => {
58 if (post && post.user_id) {
59 // 这里假设有API postsAPI.getUserFollowing
60 const checkFollow = async () => {
61 try {
62 const userInfo = getUserInfo()
63 if (!userInfo?.id) return
64 const res = await postsAPI.getUserFollowing(userInfo.id)
65 if (Array.isArray(res)) {
66 setIsFollowing(res.some(u => u.id === post.user_id))
67 } else if (Array.isArray(res.following)) {
68 setIsFollowing(res.following.some(u => u.id === post.user_id))
69 }
70 } catch {}
71 }
72 checkFollow()
73 }
74 }, [post])
75
76 // 拉取发帖人信息
77 useEffect(() => {
78 if (post && post.user_id) {
79 postsAPI.getUser(post.user_id).then(res => setAuthorInfo(res || {})).catch(() => setAuthorInfo({}))
80 }
81 }, [post])
82
22301008ba662fe2025-06-20 18:10:20 +080083 useEffect(() => {
84 fetchPostDetail()
85 fetchComments()
86 }, [fetchPostDetail, fetchComments])
95630366980c1f272025-06-20 14:08:54 +080087
88 const handleBack = () => {
89 navigate(-1)
90 }
95630366980c1f272025-06-20 14:08:54 +080091 const handleLike = async () => {
92 try {
22301008ba662fe2025-06-20 18:10:20 +080093 const currentUserId = getCurrentUserId()
95630366980c1f272025-06-20 14:08:54 +080094 const newLiked = !liked
95 if (newLiked) {
22301008ba662fe2025-06-20 18:10:20 +080096 await searchAPI.likePost(id, currentUserId)
95630366980c1f272025-06-20 14:08:54 +080097 } else {
22301008ba662fe2025-06-20 18:10:20 +080098 await searchAPI.unlikePost(id, currentUserId)
95630366980c1f272025-06-20 14:08:54 +080099 }
100 setLiked(newLiked)
101 setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
102 } catch (error) {
103 console.error('点赞失败:', error)
104 // 回滚状态
105 setLiked(!liked)
106 setLikeCount(prev => liked ? prev + 1 : prev - 1)
107 }
108 }
109
110 const handleBookmark = () => {
111 setBookmarked(!bookmarked)
112 // 实际项目中这里应该调用后端API保存收藏状态
113 }
114
115 const handleShare = () => {
116 // 分享功能
117 if (navigator.share) {
118 navigator.share({
119 title: post?.title,
120 text: post?.content,
121 url: window.location.href,
122 })
123 } else {
124 // 复制链接到剪贴板
125 navigator.clipboard.writeText(window.location.href)
126 alert('链接已复制到剪贴板')
127 }
128 }
95630366980c1f272025-06-20 14:08:54 +0800129 const handleAddComment = async (e) => {
130 e.preventDefault()
131 if (!newComment.trim()) return
132
133 try {
22301008ba662fe2025-06-20 18:10:20 +0800134 const currentUserId = getCurrentUserId()
135 await searchAPI.addComment(id, currentUserId, newComment)
95630366980c1f272025-06-20 14:08:54 +0800136 setNewComment('')
137 fetchComments() // 刷新评论列表
138 } catch (error) {
139 console.error('添加评论失败:', error)
140 alert('评论失败,请重试')
141 }
142 }
143
22301008e25b4b02025-06-20 22:15:31 +0800144 // 关注后刷新关注状态
145 const handleFollowChange = async (followed) => {
146 setIsFollowing(followed)
147 // 关注/取关后重新拉取一次关注状态,保证和数据库同步
148 if (post && post.user_id) {
149 try {
150 const userInfo = getUserInfo()
151 if (!userInfo?.id) return
152 const res = await postsAPI.getUserFollowing(userInfo.id)
153 if (Array.isArray(res)) {
154 setIsFollowing(res.some(u => u.id === post.user_id))
155 } else if (Array.isArray(res.following)) {
156 setIsFollowing(res.following.some(u => u.id === post.user_id))
157 }
158 } catch {}
159 }
160 }
161
95630366980c1f272025-06-20 14:08:54 +0800162 if (loading) {
163 return (
164 <div className="post-detail">
165 <div className="loading-container">
166 <div className="loading-spinner"></div>
167 <p>加载中...</p>
168 </div>
169 </div>
170 )
171 }
172
22301008e25b4b02025-06-20 22:15:31 +0800173 // 优化错误和不存在的判断逻辑
95630366980c1f272025-06-20 14:08:54 +0800174 if (error) {
175 return (
176 <div className="post-detail">
177 <div className="error-container">
178 <h2>😔 出错了</h2>
179 <p>{error}</p>
180 <button onClick={handleBack} className="back-btn">
181 <ArrowLeft size={20} />
182 返回
183 </button>
184 </div>
185 </div>
186 )
187 }
188
22301008e25b4b02025-06-20 22:15:31 +0800189 // 只有明确为 null 或 undefined 时才显示不存在
190 if (post === null || post === undefined) {
95630366980c1f272025-06-20 14:08:54 +0800191 return (
192 <div className="post-detail">
193 <div className="error-container">
22301008e25b4b02025-06-20 22:15:31 +0800194 <h2>😔 帖子不存在或已被删除</h2>
95630366980c1f272025-06-20 14:08:54 +0800195 <p>该帖子可能已被删除或不存在</p>
196 <button onClick={handleBack} className="back-btn">
197 <ArrowLeft size={20} />
198 返回
199 </button>
200 </div>
201 </div>
202 )
203 }
204
205 return (
206 <div className="post-detail">
207 {/* 顶部导航栏 */}
208 <header className="post-header">
209 <button onClick={handleBack} className="back-btn">
210 <ArrowLeft size={20} />
211 返回
212 </button>
213 <div className="header-actions">
214 <button onClick={handleShare} className="action-btn">
215 <Share2 size={20} />
216 </button>
217 <button
218 onClick={handleBookmark}
219 className={`action-btn ${bookmarked ? 'active' : ''}`}
220 >
221 <BookmarkPlus size={20} />
222 </button>
223 </div>
224 </header>
225
226 {/* 主要内容区 */}
227 <main className="post-content">
228 {/* 帖子标题 */}
229 <h1 className="post-title">{post.title}</h1>
230
231 {/* 作者信息和元数据 */}
232 <div className="post-meta">
233 <div className="author-info">
234 <div className="avatar">
22301008e25b4b02025-06-20 22:15:31 +0800235 {authorInfo && authorInfo.avatar && authorInfo.avatar.startsWith('http') ? (
236 <img className="avatar" src={authorInfo.avatar} alt={authorInfo.username || authorInfo.nickname || post.author || '用户'} />
237 ) : (
238 <img className="avatar" src={`https://i.pravatar.cc/40?img=${post.user_id}`} alt={authorInfo?.username || authorInfo?.nickname || post.author || '用户'} />
239 )}
95630366980c1f272025-06-20 14:08:54 +0800240 </div>
241 <div className="author-details">
22301008e25b4b02025-06-20 22:15:31 +0800242 <span className="author-name">{authorInfo?.username || authorInfo?.nickname || post.author || '匿名用户'}</span>
95630366980c1f272025-06-20 14:08:54 +0800243 <span className="post-date">
223010083c35e492025-06-20 22:24:22 +0800244 {post.created_at ? dayjs(post.created_at).format('YYYY-MM-DD HH:mm:ss') : '未知时间'}
95630366980c1f272025-06-20 14:08:54 +0800245 </span>
22301008e25b4b02025-06-20 22:15:31 +0800246 {/* 关注按钮 */}
247 {post.user_id && (
248 <FollowButton
249 userId={post.user_id}
250 isFollowing={isFollowing}
251 onFollowChange={handleFollowChange}
252 style={{marginLeft: 12}}
253 />
254 )}
95630366980c1f272025-06-20 14:08:54 +0800255 </div>
256 </div>
257 <div className="post-stats">
258 <span className="stat-item">
259 <Eye size={16} />
260 {post.views || 0}
261 </span>
262 <span className="stat-item">
263 <Heart size={16} />
264 {likeCount}
265 </span>
266 </div>
267 </div>
268
269 {/* 标签 */}
270 {post.tags && post.tags.length > 0 && (
271 <div className="post-tags">
272 {post.tags.map((tag, index) => (
273 <span key={index} className="tag">{tag}</span>
274 ))}
275 </div>
276 )}
277
22301008c9805072025-06-20 22:38:02 +0800278 {/* 帖子图片(支持多图) */}
279 {Array.isArray(post.media_urls) && post.media_urls.length > 0 && (
280 <div className="post-images" style={{display:'flex',gap:8,marginBottom:16}}>
281 {post.media_urls.map((url, idx) => (
282 <img key={idx} src={url} alt={`图片${idx+1}`} style={{maxWidth:220,maxHeight:220,borderRadius:8,objectFit:'cover',cursor:'pointer'}} onClick={() => setPreviewImg(url)} />
283 ))}
284 </div>
285 )}
286 {/* 大图预览弹窗 */}
287 {previewImg && (
288 <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)}>
289 <img src={previewImg} alt="大图预览" style={{maxWidth:'90vw',maxHeight:'90vh',borderRadius:12,boxShadow:'0 4px 24px #0008'}} />
290 </div>
291 )}
292
95630366980c1f272025-06-20 14:08:54 +0800293 {/* 帖子正文 */}
294 <div className="post-body">
295 <p>{post.content}</p>
296 </div>
297
298 {/* 类别信息 */}
299 {(post.category || post.type) && (
300 <div className="post-category">
301 {post.category && (
302 <>
303 <span className="category-label">分类:</span>
304 <span className="category-name">{post.category}</span>
305 </>
306 )}
307 {post.type && (
308 <>
309 <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
310 <span className="category-name">{post.type}</span>
311 </>
312 )}
313 </div>
314 )}
315
316 {/* 评论区 */}
317 <div className="comments-section">
318 <div className="comments-header">
319 <button
320 onClick={() => setShowComments(!showComments)}
321 className="comments-toggle"
322 >
323 <MessageCircle size={20} />
324 评论 ({comments.length})
325 </button>
326 </div>
327
328 {showComments && (
329 <div className="comments-content">
330 {/* 添加评论 */}
331 <form onSubmit={handleAddComment} className="comment-form">
332 <textarea
333 value={newComment}
334 onChange={(e) => setNewComment(e.target.value)}
335 placeholder="写下你的评论..."
336 className="comment-input"
337 rows={3}
338 />
339 <button type="submit" className="comment-submit">
340 发布评论
341 </button>
342 </form>
343
344 {/* 评论列表 */}
345 <div className="comments-list">
346 {comments.length === 0 ? (
347 <p className="no-comments">暂无评论</p>
348 ) : (
349 comments.map((comment, index) => (
350 <div key={index} className="comment-item">
351 <div className="comment-author">
352 <div className="comment-avatar">
353 {comment.user_name ? comment.user_name.charAt(0).toUpperCase() : 'U'}
354 </div>
355 <span className="comment-name">{comment.user_name || '匿名用户'}</span>
356 <span className="comment-time">
357 {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
358 </span>
359 </div>
360 <div className="comment-content">
361 {comment.content}
362 </div>
363 </div>
364 ))
365 )}
366 </div>
367 </div>
368 )}
369 </div>
370 </main>
371
372 {/* 底部操作栏 */}
373 <footer className="post-footer">
374 <div className="action-bar">
375 <button
376 onClick={handleLike}
377 className={`action-button ${liked ? 'liked' : ''}`}
378 >
379 <ThumbsUp size={20} />
380 <span>{likeCount}</span>
381 </button>
382
383 <button
384 onClick={() => setShowComments(!showComments)}
385 className="action-button"
386 >
387 <MessageCircle size={20} />
388 <span>评论</span>
389 </button>
390
391 <button onClick={handleShare} className="action-button">
392 <Share2 size={20} />
393 <span>分享</span>
394 </button>
395
396 <button
397 onClick={handleBookmark}
398 className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
399 >
400 <BookmarkPlus size={20} />
401 <span>收藏</span>
402 </button>
403 </div>
404 </footer>
405 </div>
406 )
407}