blob: b0ca84f6a0d125bd6376a11c436ea9660afc5f2f [file] [log] [blame]
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
ArrowLeft,
ThumbsUp,
MessageCircle,
Share2,
BookmarkPlus,
Heart,
Eye,
} from 'lucide-react'
import { searchAPI } from '../api/search_jwlll'
import { getUserInfo } from '../utils/auth'
import FollowButton from './FollowButton'
import postsAPI from '../api/posts_api' // ⇦ 包含 hasLikedPost、likePost、unlikePost
import MediaPreview from './MediaPreview'
import '../style/PostDetail.css'
import dayjs from 'dayjs'
export default function PostDetail() {
const { id } = useParams()
const navigate = useNavigate()
// ──────────────── 状态 ────────────────
const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [liked, setLiked] = useState(false)
const [bookmarked, setBookmarked] = useState(false)
const [likeCount, setLikeCount] = useState(0)
const [comments, setComments] = useState([])
const [newComment, setNewComment] = useState('')
const [showComments, setShowComments] = useState(false)
const [isFollowing, setIsFollowing] = useState(false)
const [authorInfo, setAuthorInfo] = useState(null)
const [previewImg, setPreviewImg] = useState(null)
const [commentUserMap, setCommentUserMap] = useState({}) // user_id → username
const [commentUserAvatarMap, setCommentUserAvatarMap] = useState({}) // user_id → avatar
// 当前登录用户 ID(memo 化,组件整个生命周期只算一次)
const currentUserId = useMemo(() => {
const ui = getUserInfo()
return ui?.id || 'null' // 未登录就给个默认值 3
}, [])
// ──────────────── 拉取帖子详情 ────────────────
const fetchPostDetail = useCallback(async () => {
setLoading(true)
setError(null)
try {
const data = await searchAPI.getPostDetail(id)
setPost(data)
setLikeCount(data.heat || 0)
} catch (err) {
console.error('获取帖子详情失败:', err)
setError('帖子不存在或已被删除')
} finally {
setLoading(false)
}
}, [id])
// ──────────────── 拉取评论 ────────────────
const fetchComments = useCallback(async () => {
try {
const data = await searchAPI.getComments(id)
setComments(data.comments || [])
} catch (err) {
console.error('获取评论失败:', err)
}
}, [id])
// ──────────────── 组件挂载:帖子详情 + 评论 + 点赞状态 ────────────────
useEffect(() => {
fetchPostDetail()
fetchComments()
// 检查我是否点过赞
if (currentUserId) {
;(async () => {
try {
const res = await postsAPI.hasLikedPost(id, currentUserId)
setLiked(!!res.liked)
} catch (err) {
console.error('检查点赞状态失败:', err)
}
})()
}
}, [fetchPostDetail, fetchComments, id, currentUserId])
// ──────────────── 检查是否关注作者 ────────────────
useEffect(() => {
if (post && post.user_id) {
const checkFollow = async () => {
try {
if (!currentUserId) return
const res = await postsAPI.getUserFollowing(currentUserId)
if (Array.isArray(res)) {
setIsFollowing(res.some((u) => u.id === post.user_id))
} else if (Array.isArray(res.following)) {
setIsFollowing(res.following.some((u) => u.id === post.user_id))
}
} catch {}
}
checkFollow()
}
}, [post, currentUserId])
// ──────────────── 作者信息 ────────────────
useEffect(() => {
if (post && post.user_id) {
postsAPI
.getUser(post.user_id)
.then((res) => setAuthorInfo(res || {}))
.catch(() => setAuthorInfo({}))
}
}, [post])
// ──────────────── 拉取评论用户昵称 / 头像 ────────────────
const fetchCommentUserNames = async (userIds) => {
const map = {}
await Promise.all(
userIds.map(async (uid) => {
try {
const user = await postsAPI.getUser(uid)
map[uid] = user.username || user.nickname || `用户${uid}`
} catch {
map[uid] = `用户${uid}`
}
})
)
setCommentUserMap(map)
}
const fetchCommentUserAvatars = async (userIds) => {
const map = {}
await Promise.all(
userIds.map(async (uid) => {
try {
const user = await postsAPI.getUser(uid)
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}`
} catch {
map[uid] = `https://i.pravatar.cc/40?img=${uid}`
}
})
)
setCommentUserAvatarMap(map)
}
useEffect(() => {
if (comments.length > 0) {
const uidSet = new Set(comments.map((c) => c.user_id).filter(Boolean))
const ids = [...uidSet]
fetchCommentUserNames(ids)
fetchCommentUserAvatars(ids)
}
}, [comments])
// ──────────────── 交互 handlers ────────────────
const handleBack = () => navigate(-1)
const handleLike = async () => {
if (!currentUserId) {
alert('请先登录~')
return
}
try {
if (liked) {
// 取消点赞
await postsAPI.unlikePost(id, currentUserId)
} else {
// 点赞
await postsAPI.likePost(id, currentUserId)
}
// —— 关键:操作成功后重新拉一次帖子详情(里面会 setLikeCount) ——
await fetchPostDetail()
// —— 同步一下 liked 状态(可选,因为你直接用 fetchPostDetail 重置 likeCount,也可以重新查一下状态) ——
const { liked: has } = await postsAPI.hasLikedPost(id, currentUserId)
setLiked(!!has)
} catch (err) {
console.error('点赞操作失败:', err)
}
}
const handleBookmark = () => {
setBookmarked(!bookmarked)
// TODO: 调后端保存收藏状态
}
const handleShare = () => {
if (navigator.share) {
navigator.share({
title: post?.title,
text: post?.content,
url: window.location.href,
})
} else {
navigator.clipboard.writeText(window.location.href)
alert('链接已复制到剪贴板')
}
}
const handleAddComment = async (e) => {
e.preventDefault()
if (!newComment.trim()) return
try {
await searchAPI.addComment(id, currentUserId, newComment)
setNewComment('')
fetchComments()
} catch (err) {
console.error('添加评论失败:', err)
alert('评论失败,请重试')
}
}
const handleFollowChange = (followed) => setIsFollowing(followed)
// ──────────────── 渲染逻辑 ────────────────
if (loading) {
return (
<div className="post-detail">
<div className="loading-container">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="post-detail">
<div className="error-container">
<h2>😔 出错了</h2>
<p>{error}</p>
<button onClick={handleBack} className="back-btn">
<ArrowLeft size={20} />
返回
</button>
</div>
</div>
)
}
if (post === null || post === undefined) {
return (
<div className="post-detail">
<div className="error-container">
<h2>😔 帖子不存在或已被删除</h2>
<p>该帖子可能已被删除或不存在</p>
<button onClick={handleBack} className="back-btn">
<ArrowLeft size={20} />
返回
</button>
</div>
</div>
)
}
return (
<div className="post-detail">
{/* 顶部导航栏 */}
<header className="post-header">
<button onClick={handleBack} className="back-btn">
<ArrowLeft size={20} />
返回
</button>
<div className="header-actions">
<button onClick={handleShare} className="action-btn">
<Share2 size={20} />
</button>
<button
onClick={handleBookmark}
className={`action-btn ${bookmarked ? 'active' : ''}`}
>
<BookmarkPlus size={20} />
</button>
</div>
</header>
{/* 主要内容区 */}
<main className="post-content">
{/* 标题 */}
<h1 className="post-title">{post.title}</h1>
{/* 作者 & 元数据 */}
<div className="post-meta">
<div className="author-info">
<div className="avatar">
{authorInfo?.avatar && authorInfo.avatar.startsWith('http') ? (
<img
className="avatar"
src={authorInfo.avatar}
alt={
authorInfo.username ||
authorInfo.nickname ||
post.author ||
'用户'
}
/>
) : (
<img
className="avatar"
src={`https://i.pravatar.cc/40?img=${post.user_id}`}
alt={
authorInfo?.username ||
authorInfo?.nickname ||
post.author ||
'用户'
}
/>
)}
</div>
<div className="author-details">
<span className="author-name">
{authorInfo?.username ||
authorInfo?.nickname ||
post.author ||
'匿名用户'}
</span>
<span className="post-date">
{post.created_at
? dayjs(post.created_at).format('YYYY-MM-DD HH:mm:ss')
: '未知时间'}
</span>
{post.user_id && (
<FollowButton
userId={post.user_id}
isFollowing={isFollowing}
onFollowChange={handleFollowChange}
style={{ marginLeft: 12 }}
/>
)}
</div>
</div>
<div className="post-stats">
<span className="stat-item">
<Eye size={16} />
{post.views || 0}
</span>
<span className="stat-item">
<Heart size={16} />
{likeCount}
</span>
</div>
</div>
{/* 标签 */}
{post.tags?.length > 0 && (
<div className="post-tags">
{post.tags.map((tag, idx) => (
<span key={idx} className="tag">
{tag}
</span>
))}
</div>
)}
{/* 媒体 */}
{Array.isArray(post.media_urls) && post.media_urls.length > 0 && (
<div
className="post-media"
style={{
display: 'flex',
gap: 8,
marginBottom: 16,
flexWrap: 'wrap',
}}
>
{post.media_urls.map((url, idx) => (
<MediaPreview
key={idx}
url={url}
alt={`媒体${idx + 1}`}
onClick={(mediaUrl) => {
// 图片点击预览
const lower = mediaUrl.toLowerCase()
if (
!lower.includes('video') &&
!lower.endsWith('.mp4') &&
!lower.endsWith('.webm')
) {
setPreviewImg(mediaUrl)
}
}}
style={{ cursor: 'pointer' }}
maxWidth={320}
maxHeight={320}
/>
))}
</div>
)}
{previewImg && (
<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)}
>
<img
src={previewImg}
alt="大图预览"
style={{
maxWidth: '90vw',
maxHeight: '90vh',
borderRadius: 12,
boxShadow: '0 4px 24px #0008',
}}
/>
</div>
)}
{/* 正文 */}
<div className="post-body">
<p>{post.content}</p>
</div>
{/* 分类 / 类型 */}
{(post.category || post.type) && (
<div className="post-category">
{post.category && (
<>
<span className="category-label">分类:</span>
<span className="category-name">{post.category}</span>
</>
)}
{post.type && (
<>
<span className="category-label" style={{ marginLeft: '1em' }}>
类型:
</span>
<span className="category-name">{post.type}</span>
</>
)}
</div>
)}
{/* 评论区 */}
<div className="comments-section">
<div className="comments-header">
<button
onClick={() => setShowComments(!showComments)}
className="comments-toggle"
>
<MessageCircle size={20} />
评论 ({comments.length})
</button>
</div>
{showComments && (
<div className="comments-content">
{/* 新评论 */}
<form onSubmit={handleAddComment} className="comment-form">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="写下你的评论..."
className="comment-input"
rows={3}
/>
<button type="submit" className="comment-submit">
发布评论
</button>
</form>
{/* 评论列表 */}
<div className="comments-list">
{comments.length === 0 ? (
<p className="no-comments">暂无评论</p>
) : (
comments.map((comment, idx) => (
<div key={idx} className="comment-item">
<div className="comment-author">
<div className="comment-avatar">
<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 ||
'用户'
}
/>
</div>
<span className="comment-name">
{commentUserMap[comment.user_id] ||
comment.user_name ||
'匿名用户'}
</span>
<span className="comment-time">
{comment.create_time
? new Date(comment.create_time).toLocaleString(
'zh-CN'
)
: ''}
</span>
</div>
<div className="comment-content">{comment.content}</div>
</div>
))
)}
</div>
</div>
)}
</div>
</main>
{/* 底部操作栏 */}
<footer className="post-footer">
<div className="action-bar">
<button
onClick={handleLike}
className={`action-button ${liked ? 'liked' : ''}`}
>
<ThumbsUp size={20} />
<span>{likeCount}</span>
</button>
<button
onClick={() => setShowComments(!showComments)}
className="action-button"
>
<MessageCircle size={20} />
<span>评论</span>
</button>
<button onClick={handleShare} className="action-button">
<Share2 size={20} />
<span>分享</span>
</button>
<button
onClick={handleBookmark}
className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
>
<BookmarkPlus size={20} />
<span>收藏</span>
</button>
</div>
</footer>
</div>
)
}