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