blob: f998f64f0633b57bea4aa1738fc9459c1a224ff6 [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 {
LaoeGaocif6e5c962025-06-08 23:39:13 +0800104 const { data } = await axios.get(process.env.PUBLIC_URL + `/thread`, {
105 params: {
106 threadId,
107 userId: 22301145
108 }
109 });
LaoeGaociee7c5772025-05-28 12:34:47 +0800110 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
LaoeGaocif6e5c962025-06-08 23:39:13 +0800118
LaoeGaociee7c5772025-05-28 12:34:47 +0800119 // 获取发帖人
120 useEffect(() => {
121 if (!threadInfo) return;
122 // 发帖人
LaoeGaoci388f7762025-05-29 22:24:35 +0800123 axios.get(process.env.PUBLIC_URL + `/user/info?userId=${threadInfo.userId}`)
LaoeGaociee7c5772025-05-28 12:34:47 +0800124 .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(
LaoeGaoci388f7762025-05-29 22:24:35 +0800134 process.env.PUBLIC_URL + `/thread/like`, {
LaoeGaociee7c5772025-05-28 12:34:47 +0800135 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(
LaoeGaoci388f7762025-05-29 22:24:35 +0800150 process.env.PUBLIC_URL + `/thread/like`, {
LaoeGaociee7c5772025-05-28 12:34:47 +0800151 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 {
LaoeGaoci388f7762025-05-29 22:24:35 +0800187 const pageNumber = first / rows + 1;
188 console.log("当前页" + pageNumber + "size" + rows);
LaoeGaociee7c5772025-05-28 12:34:47 +0800189 const response = await axios.get<CommentList>(
LaoeGaoci388f7762025-05-29 22:24:35 +0800190 process.env.PUBLIC_URL + `/comments`, {
191 params: { threadId, pageNumber, rows }
LaoeGaociee7c5772025-05-28 12:34:47 +0800192 }
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>(
LaoeGaoci388f7762025-05-29 22:24:35 +0800200 process.env.PUBLIC_URL + `/user/info`,
LaoeGaociee7c5772025-05-28 12:34:47 +0800201 { 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
LaoeGaoci388f7762025-05-29 22:24:35 +0800227 const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
LaoeGaociee7c5772025-05-28 12:34:47 +0800228
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
LaoeGaoci388f7762025-05-29 22:24:35 +0800257 const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
LaoeGaociee7c5772025-05-28 12:34:47 +0800258
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(
LaoeGaoci388f7762025-05-29 22:24:35 +0800279 process.env.PUBLIC_URL + `/comment?commentId=${commentId}`
LaoeGaociee7c5772025-05-28 12:34:47 +0800280 );
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>
LaoeGaociee7c5772025-05-28 12:34:47 +0800341 </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}