接点赞前后端接口
Change-Id: Idd6c270c72fddc7c50412a4c54a7429d998ebb25
diff --git a/Merge/front/src/api/posts_api.js b/Merge/front/src/api/posts_api.js
index 06cf2c7..d42ede2 100644
--- a/Merge/front/src/api/posts_api.js
+++ b/Merge/front/src/api/posts_api.js
@@ -130,7 +130,14 @@
// 获取收藏列表
getFavorites: async (userId) => {
return await request(`${LJC_BASE_URL}/api/user/${userId}/favorites`)
- }
+ },
+
+ // 获取点赞状态
+ // GET /posts/:postId/like/status?user_id=123
+ hasLikedPost: async (postId, userId) => {
+ const url = `${WZY_BASE_URL}/posts/${postId}/like/status?user_id=${userId}`
+ return await request(url)
+ },
}
export default postsAPI
diff --git a/Merge/front/src/components/PostDetailJWLLL.jsx b/Merge/front/src/components/PostDetailJWLLL.jsx
index 01f64b5..b0ca84f 100644
--- a/Merge/front/src/components/PostDetailJWLLL.jsx
+++ b/Merge/front/src/components/PostDetailJWLLL.jsx
@@ -1,10 +1,18 @@
-import React, { useState, useEffect, useCallback } from 'react'
+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 {
+ 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'
+import postsAPI from '../api/posts_api' // ⇦ 包含 hasLikedPost、likePost、unlikePost
import MediaPreview from './MediaPreview'
import '../style/PostDetail.css'
import dayjs from 'dayjs'
@@ -12,26 +20,34 @@
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
- const getCurrentUserId = () => {
- const userInfo = getUserInfo()
- return userInfo?.id || '3' // 如果未登录或无用户信息,使用默认值3
- }
+ 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)
@@ -39,121 +55,151 @@
const data = await searchAPI.getPostDetail(id)
setPost(data)
setLikeCount(data.heat || 0)
- } catch (error) {
- console.error('获取帖子详情失败:', error)
+ } 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 (error) {
- console.error('获取评论失败:', error)
+ } 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) {
- // 这里假设有API postsAPI.getUserFollowing
const checkFollow = async () => {
try {
- const userInfo = getUserInfo()
- if (!userInfo?.id) return
- const res = await postsAPI.getUserFollowing(userInfo.id)
+ if (!currentUserId) return
+ const res = await postsAPI.getUserFollowing(currentUserId)
if (Array.isArray(res)) {
- setIsFollowing(res.some(u => u.id === post.user_id))
+ 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))
+ setIsFollowing(res.following.some((u) => u.id === post.user_id))
}
} catch {}
}
checkFollow()
}
- }, [post])
+ }, [post, currentUserId])
- // 拉取发帖人信息
+ // ──────────────── 作者信息 ────────────────
useEffect(() => {
if (post && post.user_id) {
- postsAPI.getUser(post.user_id).then(res => setAuthorInfo(res || {})).catch(() => setAuthorInfo({}))
+ 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}`
- }
- }))
+ 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}`
- }
- }))
+ 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(() => {
- fetchPostDetail()
- fetchComments()
- }, [fetchPostDetail, fetchComments])
-
- // 评论区用户昵称和头像拉取
- useEffect(() => {
if (comments.length > 0) {
- const userIds = [...new Set(comments.map(c => c.user_id).filter(Boolean))]
- fetchCommentUserNames(userIds)
- fetchCommentUserAvatars(userIds)
+ const uidSet = new Set(comments.map((c) => c.user_id).filter(Boolean))
+ const ids = [...uidSet]
+ fetchCommentUserNames(ids)
+ fetchCommentUserAvatars(ids)
}
}, [comments])
- const handleBack = () => {
- navigate(-1)
- }
+ // ──────────────── 交互 handlers ────────────────
+ const handleBack = () => navigate(-1)
+
const handleLike = async () => {
+ if (!currentUserId) {
+ alert('请先登录~')
+ return
+ }
+
try {
- const currentUserId = getCurrentUserId()
- const newLiked = !liked
- if (newLiked) {
- await searchAPI.likePost(id, currentUserId)
+ if (liked) {
+ // 取消点赞
+ await postsAPI.unlikePost(id, currentUserId)
} else {
- await searchAPI.unlikePost(id, currentUserId)
+ // 点赞
+ await postsAPI.likePost(id, currentUserId)
}
- setLiked(newLiked)
- setLikeCount(prev => newLiked ? prev + 1 : prev - 1)
- } catch (error) {
- console.error('点赞失败:', error)
- // 回滚状态
- setLiked(!liked)
- setLikeCount(prev => liked ? prev + 1 : prev - 1)
+
+ // —— 关键:操作成功后重新拉一次帖子详情(里面会 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)
- // 实际项目中这里应该调用后端API保存收藏状态
+ // TODO: 调后端保存收藏状态
}
const handleShare = () => {
- // 分享功能
if (navigator.share) {
navigator.share({
title: post?.title,
@@ -161,44 +207,27 @@
url: window.location.href,
})
} else {
- // 复制链接到剪贴板
navigator.clipboard.writeText(window.location.href)
alert('链接已复制到剪贴板')
}
}
+
const handleAddComment = async (e) => {
e.preventDefault()
if (!newComment.trim()) return
-
try {
- const currentUserId = getCurrentUserId()
await searchAPI.addComment(id, currentUserId, newComment)
setNewComment('')
- fetchComments() // 刷新评论列表
- } catch (error) {
- console.error('添加评论失败:', error)
+ fetchComments()
+ } catch (err) {
+ console.error('添加评论失败:', err)
alert('评论失败,请重试')
}
}
- // 关注后刷新关注状态
- const handleFollowChange = async (followed) => {
- setIsFollowing(followed)
- // 关注/取关后重新拉取一次关注状态,保证和数据库同步
- if (post && post.user_id) {
- try {
- const userInfo = getUserInfo()
- if (!userInfo?.id) return
- const res = await postsAPI.getUserFollowing(userInfo.id)
- 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 {}
- }
- }
+ const handleFollowChange = (followed) => setIsFollowing(followed)
+ // ──────────────── 渲染逻辑 ────────────────
if (loading) {
return (
<div className="post-detail">
@@ -210,7 +239,6 @@
)
}
- // 优化错误和不存在的判断逻辑
if (error) {
return (
<div className="post-detail">
@@ -226,7 +254,6 @@
)
}
- // 只有明确为 null 或 undefined 时才显示不存在
if (post === null || post === undefined) {
return (
<div className="post-detail">
@@ -254,8 +281,8 @@
<button onClick={handleShare} className="action-btn">
<Share2 size={20} />
</button>
- <button
- onClick={handleBookmark}
+ <button
+ onClick={handleBookmark}
className={`action-btn ${bookmarked ? 'active' : ''}`}
>
<BookmarkPlus size={20} />
@@ -265,31 +292,56 @@
{/* 主要内容区 */}
<main className="post-content">
- {/* 帖子标题 */}
+ {/* 标题 */}
<h1 className="post-title">{post.title}</h1>
- {/* 作者信息和元数据 */}
+ {/* 作者 & 元数据 */}
<div className="post-meta">
<div className="author-info">
<div className="avatar">
- {authorInfo && authorInfo.avatar && authorInfo.avatar.startsWith('http') ? (
- <img className="avatar" src={authorInfo.avatar} alt={authorInfo.username || authorInfo.nickname || post.author || '用户'} />
+ {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 || '用户'} />
+ <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 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}}
+ style={{ marginLeft: 12 }}
/>
)}
</div>
@@ -307,25 +359,40 @@
</div>
{/* 标签 */}
- {post.tags && post.tags.length > 0 && (
+ {post.tags?.length > 0 && (
<div className="post-tags">
- {post.tags.map((tag, index) => (
- <span key={index} className="tag">{tag}</span>
+ {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'}}>
+ <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}`}
+ alt={`媒体${idx + 1}`}
onClick={(mediaUrl) => {
- // 对于图片,显示预览
- if (!mediaUrl.toLowerCase().includes('video') && !mediaUrl.includes('.mp4') && !mediaUrl.includes('.webm')) {
+ // 图片点击预览
+ const lower = mediaUrl.toLowerCase()
+ if (
+ !lower.includes('video') &&
+ !lower.endsWith('.mp4') &&
+ !lower.endsWith('.webm')
+ ) {
setPreviewImg(mediaUrl)
}
}}
@@ -336,19 +403,42 @@
))}
</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
+ 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 && (
@@ -359,7 +449,9 @@
)}
{post.type && (
<>
- <span className="category-label" style={{marginLeft: '1em'}}>类型:</span>
+ <span className="category-label" style={{ marginLeft: '1em' }}>
+ 类型:
+ </span>
<span className="category-name">{post.type}</span>
</>
)}
@@ -369,7 +461,7 @@
{/* 评论区 */}
<div className="comments-section">
<div className="comments-header">
- <button
+ <button
onClick={() => setShowComments(!showComments)}
className="comments-toggle"
>
@@ -380,7 +472,7 @@
{showComments && (
<div className="comments-content">
- {/* 添加评论 */}
+ {/* 新评论 */}
<form onSubmit={handleAddComment} className="comment-form">
<textarea
value={newComment}
@@ -399,20 +491,37 @@
{comments.length === 0 ? (
<p className="no-comments">暂无评论</p>
) : (
- comments.map((comment, index) => (
- <div key={index} className="comment-item">
+ 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 || '用户'} />
+ <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-name">
+ {commentUserMap[comment.user_id] ||
+ comment.user_name ||
+ '匿名用户'}
+ </span>
<span className="comment-time">
- {comment.create_time ? new Date(comment.create_time).toLocaleString('zh-CN') : ''}
+ {comment.create_time
+ ? new Date(comment.create_time).toLocaleString(
+ 'zh-CN'
+ )
+ : ''}
</span>
</div>
- <div className="comment-content">
- {comment.content}
- </div>
+ <div className="comment-content">{comment.content}</div>
</div>
))
)}
@@ -425,29 +534,29 @@
{/* 底部操作栏 */}
<footer className="post-footer">
<div className="action-bar">
- <button
- onClick={handleLike}
+ <button
+ onClick={handleLike}
className={`action-button ${liked ? 'liked' : ''}`}
>
<ThumbsUp size={20} />
<span>{likeCount}</span>
</button>
-
- <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}
+
+ <button
+ onClick={handleBookmark}
className={`action-button ${bookmarked ? 'bookmarked' : ''}`}
>
<BookmarkPlus size={20} />