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