LaoeGaoci | 388f776 | 2025-05-29 22:24:35 +0800 | [diff] [blame] | 1 | 'use client'; |
| 2 | |
| 3 | import React, { useEffect, useState, useRef } from 'react'; |
| 4 | import { Image } from 'primereact/image'; |
| 5 | import { Avatar } from 'primereact/avatar'; |
| 6 | import { Button } from 'primereact/button'; |
| 7 | import { InputText } from "primereact/inputtext"; |
| 8 | // 页面跳转 |
| 9 | import { useParams } from 'next/navigation' |
| 10 | // 接口传输 |
| 11 | import axios from 'axios'; |
| 12 | // 回复评论 |
| 13 | import { OverlayPanel } from 'primereact/overlaypanel'; |
| 14 | import { Sidebar } from 'primereact/sidebar'; |
| 15 | // 分页 |
| 16 | import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator'; |
| 17 | // 消息提醒 |
| 18 | import { Toast } from 'primereact/toast'; |
| 19 | // 样式 |
| 20 | import './reward-detail.scss'; |
| 21 | |
| 22 | |
| 23 | // 评论信息 |
| 24 | interface Comment { |
| 25 | commentId: number; |
| 26 | userId: number | null; |
| 27 | replyId: number; |
| 28 | resourceId: number; |
| 29 | reawardId: number; |
| 30 | content: string; |
| 31 | createAt: string; |
| 32 | } |
| 33 | // 评论列表 |
| 34 | interface CommentList { |
| 35 | total: number; // 评论总数 |
| 36 | records: Comment[]; // 当前页评论数组 |
| 37 | } |
| 38 | // 悬赏信息 |
| 39 | interface RewardInfo { |
| 40 | rewardId: number; |
| 41 | userId: number; |
| 42 | rewardPicture: string; |
| 43 | rewardName: string; |
| 44 | rewardDescription: string; |
| 45 | price: number; |
| 46 | createAt: string; |
| 47 | lastUpdateAt: string; |
| 48 | commentNumber: number; |
| 49 | communityId: number; |
| 50 | } |
| 51 | // 用户信息 |
| 52 | interface UserInfo { |
| 53 | userId: number; |
| 54 | username: string; |
| 55 | avatar: string; |
| 56 | signature: string; |
| 57 | } |
| 58 | // 新评论接口 |
| 59 | interface NewComment { |
| 60 | userId: number; |
| 61 | threadId: number; |
| 62 | resourceId: number; |
| 63 | rewardId: number; |
| 64 | replyId: number; |
| 65 | content: string; |
| 66 | createAt: string; |
| 67 | } |
| 68 | |
| 69 | |
| 70 | //帖子详情界面 |
| 71 | export default function RewardDetailPage() { |
| 72 | // 获取URL参数,页面跳转 |
| 73 | const params = useParams<{ rewardId: string }>() |
| 74 | const rewardId = decodeURIComponent(params.rewardId); // 防止中文路径乱码 |
| 75 | // 消息提醒 |
| 76 | const toast = useRef<Toast>(null); |
| 77 | // 帖子信息 |
| 78 | const [rewardInfo, setRewardInfo] = useState<RewardInfo | null>(null); |
| 79 | // 发帖人信息 |
| 80 | const [userInfo, setUserInfo] = useState<UserInfo | null>(null); |
| 81 | // 评论人信息 |
| 82 | const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map()); |
| 83 | //评论 |
| 84 | const [comments, setComments] = useState<Comment[]>([]); |
| 85 | const [commentValue, setCommentValue] = useState<string>(''); |
| 86 | const [totalComments, setTotalComments] = useState<number>(0); |
| 87 | // 回复 |
| 88 | const [replyValue, setReplyValue] = useState<string>(''); |
| 89 | const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视 |
| 90 | // 评论选择框 |
| 91 | const ops = useRef<OverlayPanel[]>([]); |
| 92 | |
| 93 | // 分页 |
| 94 | const [first, setFirst] = useState<number>(0); |
| 95 | const [rows, setRows] = useState<number>(5); |
| 96 | const onPageChange = (event: PaginatorPageChangeEvent) => { |
| 97 | setFirst(event.first); |
| 98 | setRows(event.rows); |
| 99 | }; |
| 100 | |
| 101 | // 获取帖子信息 |
| 102 | useEffect(() => { |
| 103 | fetchRewardInfo(); |
| 104 | }, [rewardId, rewardInfo]); |
| 105 | |
| 106 | const fetchRewardInfo = async () => { |
| 107 | try { |
| 108 | const { data } = await axios.get(process.env.PUBLIC_URL +`/reward/info?rewardId=${rewardId}`); |
| 109 | setRewardInfo(data); |
| 110 | } catch (err) { |
| 111 | console.error(err); |
| 112 | toast.current?.show({ severity: 'error', summary: 'error', detail: '获取悬赏信息失败' }); |
| 113 | } |
| 114 | }; |
| 115 | |
| 116 | // 获取发帖人 |
| 117 | useEffect(() => { |
| 118 | if (!rewardInfo) return; |
| 119 | // 发帖人 |
| 120 | axios.get(process.env.PUBLIC_URL +`/user/info?userId=${rewardInfo.userId}`) |
| 121 | .then(res => setUserInfo(res.data)) |
| 122 | .catch(console.error); |
| 123 | }, [rewardInfo]); |
| 124 | |
| 125 | |
| 126 | // 当 rewardId 或分页参数变化时重新拉评论 |
| 127 | useEffect(() => { |
| 128 | if (!rewardId) return; |
| 129 | |
| 130 | fetchComments(); |
| 131 | }, [rewardId, first, rows]); |
| 132 | |
| 133 | |
| 134 | //通过评论ID获取评论人信息 |
| 135 | const getReplyUserName = (replyId: number) => { |
| 136 | if (replyId == null || replyId == 0) return ''; |
| 137 | const replyComment = comments.find(comment => comment.commentId === replyId); |
| 138 | if (!replyComment?.userId) return '匿名用户'; |
| 139 | return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户'; |
| 140 | }; |
| 141 | |
| 142 | // 获取评论列表 |
| 143 | const fetchComments = async () => { |
| 144 | try { |
| 145 | const pageNumber = first / rows + 1; |
| 146 | console.log("当前页" + pageNumber + "size" + rows); |
| 147 | const response = await axios.get<CommentList>( |
| 148 | process.env.PUBLIC_URL +`/comments`, { |
| 149 | params: { id: rewardId, pageNumber, rows, type: 'reward' } |
| 150 | } |
| 151 | ); |
| 152 | console.log('获取评论列表:', response.data.records); |
| 153 | setComments(response.data.records); |
| 154 | setTotalComments(response.data.total); |
| 155 | // 拉取评论对应用户信息 |
| 156 | response.data.records.forEach(comment => { |
| 157 | if (comment.userId != null && !commentUserInfos.has(comment.userId)) { |
| 158 | axios.get<UserInfo>( |
| 159 | process.env.PUBLIC_URL +`/user/info`, |
| 160 | { params: { userId: comment.userId } } |
| 161 | ).then(res => { |
| 162 | setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data)); |
| 163 | }); |
| 164 | } |
| 165 | }); |
| 166 | } catch (err) { |
| 167 | console.error('获取评论失败', err); |
| 168 | toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' }); |
| 169 | } |
| 170 | }; |
| 171 | |
| 172 | // 回复评论接口 |
| 173 | const publishReply = async (commentId: number) => { |
| 174 | if (!replyValue.trim() || !rewardInfo) return; |
| 175 | console.log('发布评论:', commentId); |
| 176 | try { |
| 177 | const newComment: NewComment = { |
| 178 | userId: 22301145, |
| 179 | rewardId: rewardInfo.rewardId, |
| 180 | threadId: 0, |
| 181 | resourceId: 0, |
| 182 | replyId: commentId, |
| 183 | content: commentValue, |
| 184 | createAt: new Date().toISOString().slice(0, 19).replace('T', ' ') |
| 185 | }; |
| 186 | |
| 187 | const response = await axios.post(process.env.PUBLIC_URL +'/comment', newComment); |
| 188 | |
| 189 | if (response.status === 200) { |
| 190 | toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' }); |
| 191 | // 更新评论列表 |
| 192 | fetchComments(); |
| 193 | setVisibleReply(false) |
| 194 | // 清空输入框 |
| 195 | setReplyValue(''); |
| 196 | } |
| 197 | } catch (error) { |
| 198 | console.error('发布评论失败:', error); |
| 199 | toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' }); |
| 200 | } |
| 201 | }; |
| 202 | |
| 203 | // 发布评论接口 |
| 204 | const publishComment = async () => { |
| 205 | if (!commentValue.trim() || !rewardInfo) return; |
| 206 | |
| 207 | try { |
| 208 | const newComment: NewComment = { |
| 209 | userId: 22301145, |
| 210 | rewardId: rewardInfo.rewardId, |
| 211 | threadId: 0, |
| 212 | resourceId: 0, |
| 213 | replyId: 0, // 直接评论,不是回复 |
| 214 | content: commentValue, |
| 215 | createAt: new Date().toISOString().slice(0, 19).replace('T', ' ') |
| 216 | }; |
| 217 | |
| 218 | const response = await axios.post(process.env.PUBLIC_URL +'/comment', newComment); |
| 219 | |
| 220 | if (response.status === 200) { |
| 221 | toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' }); |
| 222 | // 更新评论列表 |
| 223 | fetchComments(); |
| 224 | // 清空输入框 |
| 225 | setCommentValue(''); |
| 226 | } |
| 227 | } catch (error) { |
| 228 | console.error('发布评论失败:', error); |
| 229 | toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' }); |
| 230 | } |
| 231 | }; |
| 232 | |
| 233 | // 删除评论接口 |
| 234 | const deleteComment = async (commentId: number) => { |
| 235 | if (!rewardInfo) return; |
| 236 | |
| 237 | try { |
| 238 | // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId |
| 239 | const response = await axios.delete( |
| 240 | process.env.PUBLIC_URL +`/comment?commentId=${commentId}` |
| 241 | ); |
| 242 | |
| 243 | if (response.status === 200) { |
| 244 | fetchComments(); |
| 245 | toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' }); |
| 246 | } else { |
| 247 | toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' }); |
| 248 | console.error('删除评论失败,状态码:', response.status); |
| 249 | } |
| 250 | } catch (error) { |
| 251 | console.error('删除评论接口报错:', error); |
| 252 | } |
| 253 | }; |
| 254 | |
| 255 | const ReplyHeader = ( |
| 256 | <div className="flex align-items-center gap-1"> |
| 257 | <h3>回复评论</h3> |
| 258 | </div> |
| 259 | ); |
| 260 | if (!rewardInfo || !userInfo) return <div>Loading...</div>; |
| 261 | return ( |
| 262 | <div className="reward-detail"> |
| 263 | <Toast ref={toast}></Toast> |
| 264 | {/* 帖子头部 */} |
| 265 | <div className="reward-header"> |
| 266 | <h1>{rewardInfo.rewardName}</h1> |
| 267 | <span className="post-time">{"最新更新时间:" + rewardInfo.lastUpdateAt}</span> |
| 268 | </div> |
| 269 | |
| 270 | {/* 帖子内容 */} |
| 271 | <div className="reward-content"> |
| 272 | <div className="reward-info-container"> |
| 273 | <div className="user-info"> |
| 274 | <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" /> |
| 275 | <div className="user-meta"> |
| 276 | <h3>{userInfo.username}</h3> |
| 277 | <span>{userInfo.signature}</span> |
| 278 | </div> |
| 279 | </div> |
| 280 | |
| 281 | {/* 左侧文字内容 */} |
| 282 | <div className="reward-info"> |
| 283 | <p>{rewardInfo.rewardDescription}</p> |
| 284 | </div> |
| 285 | </div> |
| 286 | |
| 287 | |
| 288 | {/* 右侧图片+价格+按钮 */} |
| 289 | <div className="reward-media"> |
| 290 | <Image |
| 291 | src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + rewardInfo.rewardPicture} |
| 292 | alt={rewardInfo.rewardName} |
| 293 | width="500" |
| 294 | height="400" |
| 295 | /> |
| 296 | <div className="reward-actions"> |
| 297 | <span className="reward-price">¥{rewardInfo.price}</span> |
| 298 | <Button className="submit-bounty">提交悬赏</Button> |
| 299 | </div> |
| 300 | </div> |
| 301 | </div> |
| 302 | {/* 评论列表 */} |
| 303 | <div className="comments-section"> |
| 304 | <div className="comments-header"> |
| 305 | <h2>评论 ({totalComments})</h2> |
| 306 | </div> |
| 307 | <div className="comments-input"> |
| 308 | <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" /> |
| 309 | <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} /> |
| 310 | <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} /> |
| 311 | </div> |
| 312 | <div className="comments-list"> |
| 313 | {comments.map((comment, index) => ( |
| 314 | <div key={comment.commentId} className="comment-item"> |
| 315 | <div className="comment-user"> |
| 316 | <Avatar |
| 317 | image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'} |
| 318 | size="normal" |
| 319 | shape="circle" |
| 320 | /> |
| 321 | <div className="comment-meta"> |
| 322 | <span className="username"> |
| 323 | {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'} |
| 324 | </span> |
| 325 | <div className="comment-time"> |
| 326 | <span className="floor">#{index + 1}楼</span> |
| 327 | <span className="time">{comment.createAt}</span> |
| 328 | </div> |
| 329 | </div> |
| 330 | <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} /> |
| 331 | </div> |
| 332 | <div className="comment-content"> |
| 333 | {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>} |
| 334 | <p>{comment.content}</p> |
| 335 | </div> |
| 336 | <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置 |
| 337 | ref={el => { |
| 338 | if (el) ops.current[index] = el; |
| 339 | }}> |
| 340 | <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} /> |
| 341 | {comment.userId === 22301145 && |
| 342 | <Button |
| 343 | label="删除" |
| 344 | text |
| 345 | size="small" |
| 346 | onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }} |
| 347 | /> |
| 348 | } |
| 349 | </OverlayPanel> |
| 350 | <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}> |
| 351 | <div className="reply-input"> |
| 352 | <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" /> |
| 353 | <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} /> |
| 354 | <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} /> |
| 355 | </div> |
| 356 | </Sidebar> |
| 357 | </div> |
| 358 | ))} |
| 359 | {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)} |
| 360 | </div> |
| 361 | </div> |
| 362 | </div> |
| 363 | ); |
| 364 | } |