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