| 'use client'; |
| |
| import React, { useEffect, useState, useRef } from 'react'; |
| import { Image } from 'primereact/image'; |
| import { Avatar } from 'primereact/avatar'; |
| import { Button } from 'primereact/button'; |
| import { InputText } from "primereact/inputtext"; |
| // 页面跳转 |
| import { useParams } from 'next/navigation' |
| // 接口传输 |
| import axios from 'axios'; |
| // 回复评论 |
| import { OverlayPanel } from 'primereact/overlaypanel'; |
| import { Sidebar } from 'primereact/sidebar'; |
| // 分页 |
| import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator'; |
| // 消息提醒 |
| import { Toast } from 'primereact/toast'; |
| import { useLocalStorage } from '../../../hook/useLocalStorage'; |
| // 样式 |
| import './reward-detail.scss'; |
| interface User { |
| Id: number; |
| } |
| |
| // 评论信息 |
| interface Comment { |
| commentId: number; |
| userId: number | null; |
| replyId: number; |
| resourceId: number; |
| reawardId: number; |
| content: string; |
| createAt: string; |
| } |
| // 评论列表 |
| interface CommentList { |
| total: number; // 评论总数 |
| records: Comment[]; // 当前页评论数组 |
| } |
| // 悬赏信息 |
| interface RewardInfo { |
| rewardId: number; |
| userId: number; |
| rewardPicture: string; |
| rewardName: string; |
| rewardDescription: string; |
| price: number; |
| createAt: string; |
| lastUpdateAt: string; |
| commentNumber: number; |
| communityId: number; |
| } |
| // 用户信息 |
| interface UserInfo { |
| userId: number; |
| username: string; |
| avatar: string; |
| signature: string; |
| } |
| // 新评论接口 |
| interface NewComment { |
| userId: number; |
| threadId: number; |
| resourceId: number; |
| rewardId: number; |
| replyId: number; |
| content: string; |
| createAt: string; |
| } |
| |
| |
| //帖子详情界面 |
| export default function RewardDetailPage() { |
| const user = useLocalStorage<User>('user'); |
| const userId: number = user?.Id ?? -1; |
| // 获取URL参数,页面跳转 |
| const params = useParams<{ rewardId: string }>() |
| const rewardId = decodeURIComponent(params.rewardId); // 防止中文路径乱码 |
| // 消息提醒 |
| const toast = useRef<Toast>(null); |
| // 帖子信息 |
| const [rewardInfo, setRewardInfo] = useState<RewardInfo | null>(null); |
| // 发帖人信息 |
| const [userInfo, setUserInfo] = useState<UserInfo | null>(null); |
| // 评论人信息 |
| const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map()); |
| //评论 |
| const [comments, setComments] = useState<Comment[]>([]); |
| const [commentValue, setCommentValue] = useState<string>(''); |
| const [totalComments, setTotalComments] = useState<number>(0); |
| // 回复 |
| const [replyValue, setReplyValue] = useState<string>(''); |
| const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视 |
| // 评论选择框 |
| const ops = useRef<OverlayPanel[]>([]); |
| |
| // 分页 |
| const [first, setFirst] = useState<number>(0); |
| const [rows, setRows] = useState<number>(5); |
| const onPageChange = (event: PaginatorPageChangeEvent) => { |
| setFirst(event.first); |
| setRows(event.rows); |
| }; |
| |
| // 获取帖子信息 |
| useEffect(() => { |
| fetchRewardInfo(); |
| }, [rewardId, rewardInfo]); |
| |
| const fetchRewardInfo = async () => { |
| try { |
| const { data } = await axios.get(process.env.PUBLIC_URL + `/reward/info?rewardId=${rewardId}`); |
| setRewardInfo(data); |
| } catch (err) { |
| console.error(err); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '获取悬赏信息失败' }); |
| } |
| }; |
| |
| // 获取发帖人 |
| useEffect(() => { |
| if (!rewardInfo) return; |
| // 发帖人 |
| axios.get(process.env.PUBLIC_URL + `/user/info?userId=${rewardInfo.userId}`) |
| .then(res => setUserInfo(res.data)) |
| .catch(console.error); |
| }, [rewardInfo]); |
| |
| |
| // 当 rewardId 或分页参数变化时重新拉评论 |
| useEffect(() => { |
| if (!rewardId) return; |
| |
| fetchComments(); |
| }, [rewardId, first, rows]); |
| |
| |
| //通过评论ID获取评论人信息 |
| const getReplyUserName = (replyId: number) => { |
| if (replyId == null || replyId == 0) return ''; |
| const replyComment = comments.find(comment => comment.commentId === replyId); |
| if (!replyComment?.userId) return '匿名用户'; |
| return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户'; |
| }; |
| |
| // 获取评论列表 |
| const fetchComments = async () => { |
| try { |
| const pageNumber = first / rows + 1; |
| console.log("当前页" + pageNumber + "size" + rows); |
| const response = await axios.get<CommentList>( |
| process.env.PUBLIC_URL + `/comments`, { |
| params: { id: rewardId, pageNumber, rows, type: 'reward' } |
| } |
| ); |
| console.log('获取评论列表:', response.data.records); |
| setComments(response.data.records); |
| setTotalComments(response.data.total); |
| // 拉取评论对应用户信息 |
| response.data.records.forEach(comment => { |
| if (comment.userId != null && !commentUserInfos.has(comment.userId)) { |
| axios.get<UserInfo>( |
| process.env.PUBLIC_URL + `/user/info`, |
| { params: { userId: comment.userId } } |
| ).then(res => { |
| setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data)); |
| }); |
| } |
| }); |
| } catch (err) { |
| console.error('获取评论失败', err); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' }); |
| } |
| }; |
| |
| // 回复评论接口 |
| const publishReply = async (commentId: number) => { |
| if (!replyValue.trim() || !rewardInfo) return; |
| console.log('发布评论:', commentId); |
| try { |
| const newComment: NewComment = { |
| userId, |
| rewardId: rewardInfo.rewardId, |
| threadId: 0, |
| resourceId: 0, |
| replyId: commentId, |
| content: commentValue, |
| createAt: new Date().toISOString().slice(0, 19).replace('T', ' ') |
| }; |
| |
| const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment); |
| |
| if (response.status === 200) { |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' }); |
| // 更新评论列表 |
| fetchComments(); |
| setVisibleReply(false) |
| // 清空输入框 |
| setReplyValue(''); |
| } |
| } catch (error) { |
| console.error('发布评论失败:', error); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' }); |
| } |
| }; |
| |
| // 发布评论接口 |
| const publishComment = async () => { |
| if (!commentValue.trim() || !rewardInfo) return; |
| |
| try { |
| const newComment: NewComment = { |
| userId, |
| rewardId: rewardInfo.rewardId, |
| threadId: 0, |
| resourceId: 0, |
| replyId: 0, // 直接评论,不是回复 |
| content: commentValue, |
| createAt: new Date().toISOString().slice(0, 19).replace('T', ' ') |
| }; |
| |
| const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment); |
| |
| if (response.status === 200) { |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' }); |
| // 更新评论列表 |
| fetchComments(); |
| // 清空输入框 |
| setCommentValue(''); |
| } |
| } catch (error) { |
| console.error('发布评论失败:', error); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' }); |
| } |
| }; |
| |
| // 删除评论接口 |
| const deleteComment = async (commentId: number) => { |
| if (!rewardInfo) return; |
| |
| try { |
| // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId |
| const response = await axios.delete( |
| process.env.PUBLIC_URL + `/comment?commentId=${commentId}` |
| ); |
| |
| if (response.status === 200) { |
| fetchComments(); |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' }); |
| } else { |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' }); |
| console.error('删除评论失败,状态码:', response.status); |
| } |
| } catch (error) { |
| console.error('删除评论接口报错:', error); |
| } |
| }; |
| |
| const ReplyHeader = ( |
| <div className="flex align-items-center gap-1"> |
| <h3>回复评论</h3> |
| </div> |
| ); |
| if (!rewardInfo || !userInfo) return <div>Loading...</div>; |
| return ( |
| <div className="reward-detail"> |
| <Toast ref={toast}></Toast> |
| {/* 帖子头部 */} |
| <div className="reward-header"> |
| <h1>{rewardInfo.rewardName}</h1> |
| <span className="post-time">{"最新更新时间:" + rewardInfo.lastUpdateAt}</span> |
| </div> |
| |
| {/* 帖子内容 */} |
| <div className="reward-content"> |
| <div className="reward-info-container"> |
| <div className="user-info"> |
| <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" /> |
| <div className="user-meta"> |
| <h3>{userInfo.username}</h3> |
| <span>{userInfo.signature}</span> |
| </div> |
| </div> |
| |
| {/* 左侧文字内容 */} |
| <div className="reward-info"> |
| <p>{rewardInfo.rewardDescription}</p> |
| </div> |
| </div> |
| |
| |
| {/* 右侧图片+价格+按钮 */} |
| <div className="reward-media"> |
| <Image |
| src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + rewardInfo.rewardPicture} |
| alt={rewardInfo.rewardName} |
| width="500" |
| height="400" |
| /> |
| <div className="reward-actions"> |
| <span className="reward-price">¥{rewardInfo.price}</span> |
| <Button className="submit-bounty">提交悬赏</Button> |
| </div> |
| </div> |
| </div> |
| {/* 评论列表 */} |
| <div className="comments-section"> |
| <div className="comments-header"> |
| <h2>评论 ({totalComments})</h2> |
| </div> |
| <div className="comments-input"> |
| <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" /> |
| <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} /> |
| <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} /> |
| </div> |
| <div className="comments-list"> |
| {comments.map((comment, index) => ( |
| <div key={comment.commentId} className="comment-item"> |
| <div className="comment-user"> |
| <Avatar |
| image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'} |
| size="normal" |
| shape="circle" |
| /> |
| <div className="comment-meta"> |
| <span className="username"> |
| {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'} |
| </span> |
| <div className="comment-time"> |
| <span className="floor">#{index + 1}楼</span> |
| <span className="time">{comment.createAt}</span> |
| </div> |
| </div> |
| <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} /> |
| </div> |
| <div className="comment-content"> |
| {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>} |
| <p>{comment.content}</p> |
| </div> |
| <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置 |
| ref={el => { |
| if (el) ops.current[index] = el; |
| }}> |
| <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} /> |
| {comment.userId === userId && |
| <Button |
| label="删除" |
| text |
| size="small" |
| onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }} |
| /> |
| } |
| </OverlayPanel> |
| <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}> |
| <div className="reply-input"> |
| <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" /> |
| <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} /> |
| <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} /> |
| </div> |
| </Sidebar> |
| </div> |
| ))} |
| {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)} |
| </div> |
| </div> |
| </div> |
| ); |
| } |