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