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