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