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