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