blob: 22abfdb1d09cfaf4fa217b2fb772c10aeb39ed93 [file] [log] [blame]
LaoeGaoci85307e62025-05-30 23:28:42 +08001'use client';
LaoeGaocid0773912025-06-09 00:38:40 +08002import React, { useEffect, useRef, useState } from 'react';
3import { Image } from "primereact/image";
4import { Button } from "primereact/button";
5import { Avatar } from "primereact/avatar";
6import { ButtonGroup } from "primereact/buttongroup";
7import { InputText } from "primereact/inputtext";
lfmylbhf789e7172025-06-06 18:37:21 +08008import { Dialog } from 'primereact/dialog';
9// 引入图标
10import 'primeicons/primeicons.css';
11// 引入火苗图标
12import Fire from '@icon-park/react/lib/icons/Fire';
13import SmilingFace from '@icon-park/react/lib/icons/SmilingFace';
14// 消息提醒
15import { Toast } from 'primereact/toast';
16// 接口传输
17import axios from "axios";
18// 页面跳转
19import { useParams } from 'next/navigation'
20import { useRouter } from 'next/navigation';
21import Link from 'next/link';
22// 回复评论
23import { OverlayPanel } from 'primereact/overlaypanel';
24import { Sidebar } from 'primereact/sidebar';
25// 分页
26import { Paginator, PaginatorPageChangeEvent } from 'primereact/paginator';
LaoeGaocid0773912025-06-09 00:38:40 +080027import { useLocalStorage } from '../../../hook/useLocalStorage';
lfmylbhf789e7172025-06-06 18:37:21 +080028// 样式
29import './resource-detail.scss';
LaoeGaocid0773912025-06-09 00:38:40 +080030interface User {
31 Id: number;
32}
lfmylbhf789e7172025-06-06 18:37:21 +080033// 种子
34interface Torrent {
LaoeGaocid0773912025-06-09 00:38:40 +080035 torrentRecordId: number;
lfmylbhf789e7172025-06-06 18:37:21 +080036 torrentUrl: string;
LaoeGaocid0773912025-06-09 00:38:40 +080037 infoHash: string;
38 uploadTime: string;
39 uploaderUserId: number;
lfmylbhf789e7172025-06-06 18:37:21 +080040}
41
42// 资源版本
43interface ResourceVersion {
LaoeGaocid0773912025-06-09 00:38:40 +080044 resourceVersionId: string; // 资源版本id
lfmylbhf789e7172025-06-06 18:37:21 +080045 resourceVersionName: string; // 资源版本名称
46 compatibleVersions: string[]; // 兼容的游戏版本列表
47 torrentList: Torrent[]; // 种子列表
48}
49
50// 资源信息
51interface Resource {
LaoeGaocid0773912025-06-09 00:38:40 +080052 resourceId: number;
lfmylbhf789e7172025-06-06 18:37:21 +080053 resourceName: string; // 资源标题
LaoeGaocid0773912025-06-09 00:38:40 +080054 resourcePicture: string; // 资源照片网址
lfmylbhf789e7172025-06-06 18:37:21 +080055 resourceSummary: string; // 资源简介(一句话)
LaoeGaocid0773912025-06-09 00:38:40 +080056 resourceDetail: string; // 资源介绍
57 uploadTime: string; // 上传时间
lfmylbhf789e7172025-06-06 18:37:21 +080058 lastUpdateTime: string; // 最近更新时间
LaoeGaocid0773912025-06-09 00:38:40 +080059 price: number;
lfmylbhf789e7172025-06-06 18:37:21 +080060 downloads: number; // 下载数
LaoeGaocid0773912025-06-09 00:38:40 +080061 likes: number; // 点赞数
62 collections: number; // 收藏数
63 comments: number; // 评论数
LaoeGaocid0773912025-06-09 00:38:40 +080064 classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map
lfmylbhf789e7172025-06-06 18:37:21 +080065 hot: number; // 资源热度
66 gameplayList: string[]; // 资源标签
LaoeGaocid0773912025-06-09 00:38:40 +080067 resourceVersionList: ResourceVersion[]; // 资源版本列表
68 isCollect: boolean; // 是否被收藏
69 isLike: boolean; // 是否被点赞
70 isPurchase: boolean; // 是否被购买
71 isUpload: boolean; // 是否是该用户上传的
Seamherbb14ecb2025-06-09 22:37:20 +080072 uploaderId: number; // 资源上传者的id
lfmylbhf789e7172025-06-06 18:37:21 +080073}
74
75// 评论信息
76interface Comment {
77 commentId: number;
78 userId: number | null;
79 replyId: number;
80 resourceId: number;
81 reawardId: number;
82 content: string;
83 createAt: string;
84}
85// 评论列表
86interface CommentList {
87 total: number; // 评论总数
88 records: Comment[]; // 当前页评论数组
89}
90
91// 用户信息
92interface UserInfo {
93 userId: number;
94 username: string;
95 avatar: string;
96 signature: string;
97}
98// 新评论接口
99interface NewComment {
100 userId: number;
Seamherbb14ecb2025-06-09 22:37:20 +0800101 threadId: number | null;
102 resourceId: number | null;
103 rewardId: number | null;
104 replyId: number | null;
lfmylbhf789e7172025-06-06 18:37:21 +0800105 content: string;
106 createAt: string;
107}
108
109// 资源作者
110interface ResourceAuthor {
111 userId: number;
112 username: string;
113 avatar: string;
114 signature: string;
115}
116
117// 关注
118interface Subscriber {
119 userId: number;
LaoeGaocid0773912025-06-09 00:38:40 +0800120 username: string;
lfmylbhf789e7172025-06-06 18:37:21 +0800121}
122
123// 关注列表
124interface SubscriberList {
125 userList: Subscriber[];
126}
127
128export default function ResourceDetail() {
LaoeGaocid0773912025-06-09 00:38:40 +0800129 const user = useLocalStorage<User>('user');
130 const userId: number = user?.Id ?? -1;
lfmylbhf789e7172025-06-06 18:37:21 +0800131 // 获取URL参数
132 const params = useParams<{ resourceId: string }>();
Seamherbb14ecb2025-06-09 22:37:20 +0800133 const resourceId = Number(decodeURIComponent(params.resourceId)); // 防止中文路径乱码
lfmylbhf789e7172025-06-06 18:37:21 +0800134 // 页面跳转
135 const router = useRouter();
136
137 // 资源信息
138 const [resource, setResource] = useState<Resource | null>(null);
139 // 资源作者信息
140 const [resourceAuthor, setResourceAuthor] = useState<ResourceAuthor>();
141 // 资源作者id
142 const [resourceAuthorId, setResourceAuthorId] = useState<number>(0);
143 // 关注列表
144 const [subscriberList, setSubscriberList] = useState<SubscriberList>();
145 // 添加本地关注状态
146 const [isSubscribed, setIsSubscribed] = useState(false);
147 // 添加本地收藏状态
148 const [isCollected, setIsCollected] = useState(false);
149 const [collectionCount, setCollectionCount] = useState(0);
150 // 添加本地点赞状态
151 const [isLiked, setIsLiked] = useState(false);
152 const [likeCount, setLikeCount] = useState(0);
153 // 发帖人信息
154 const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
155 // 评论人信息
156 const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map());
157 //评论
158 const [comments, setComments] = useState<Comment[]>([]);
159 const [commentValue, setCommentValue] = useState<string>('');
160 const [totalComments, setTotalComments] = useState<number>(0);
161 // 回复
162 const [replyValue, setReplyValue] = useState<string>('');
163 const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视
164 // 评论选择框
165 const ops = useRef<OverlayPanel[]>([]);
166 // 购买弹窗状态
167 const [visible, setVisible] = useState<boolean>(false);
168 // 消息提醒
169 const toast = useRef<Toast>(null);
170
171 // 分页
172 const [first, setFirst] = useState<number>(0);
173 const [rows, setRows] = useState<number>(5);
174 const onPageChange = (event: PaginatorPageChangeEvent) => {
175 setFirst(event.first);
176 setRows(event.rows);
177 };
178
179 useEffect(() => {
180 fetchResourceInfo();
181 }, []);
182
183
184 // 获取资源信息
185 const fetchResourceInfo = async () => {
186 try {
187 // console.log(resourceId);
LaoeGaocid0773912025-06-09 00:38:40 +0800188 const response = await axios.get<Resource>(process.env.PUBLIC_URL + `/resource/info`, {
189 params: { resourceId: resourceId, userId }
lfmylbhf789e7172025-06-06 18:37:21 +0800190 });
191 console.log('获取资源信息:', response.data);
192 setResource(response.data);
193
194 // 初始化本地收藏状态
195 if (response.data) {
196 setIsCollected(response.data.isCollect);
197 setCollectionCount(response.data.collections);
198 }
199
Seamherbb14ecb2025-06-09 22:37:20 +0800200 setResourceAuthorId(response.data.uploaderId);
lfmylbhf789e7172025-06-06 18:37:21 +0800201 } catch (err) {
202 console.error('获取资源信息失败', err);
203 toast.current?.show({ severity: 'error', summary: 'error', detail: "获取资源信息失败" });
204 }
205 };
206
207 // 获取到资源作者id时,获取资源作者信息
208 useEffect(() => {
209 fetchResourceAuthor();
210 }, [resourceAuthorId]);
211
212 // 获取资源作者信息
213 const fetchResourceAuthor = async () => {
214 try {
215 // console.log(resourceId);
216 // console.log(resourceAuthorId);
LaoeGaocid0773912025-06-09 00:38:40 +0800217 const response = await axios.get<ResourceAuthor>(process.env.PUBLIC_URL + `/user/info`, {
lfmylbhf789e7172025-06-06 18:37:21 +0800218 params: { userId: resourceAuthorId }
219 });
220 console.log('获取资源作者信息:', response.data);
221 setResourceAuthor(response.data);
222 // setResourceAuthorId(response.data.userId);
223 } catch (err) {
224 console.error('获取资源作者信息失败', err);
225 toast.current?.show({ severity: 'error', summary: 'error', detail: "获取资源作者信息失败" });
226 }
227 };
228
229 useEffect(() => {
230 fetchSubscriber();
231 }, []);
232
233 // 获取正在浏览资源的用户的关注列表
234 const fetchSubscriber = async () => {
235 try {
236 const response = await axios.get<SubscriberList>(process.env.PUBLIC_URL + `/user/subscriber`, {
LaoeGaocid0773912025-06-09 00:38:40 +0800237 params: { userId }
lfmylbhf789e7172025-06-06 18:37:21 +0800238 });
239 console.log("关注列表:", response.data);
240 setSubscriberList(response.data);
241 } catch (err) {
242 console.error('获取浏览用户关注列表失败', err);
243 toast.current?.show({ severity: 'error', summary: 'error', detail: "获取浏览用户关注列表失败" });
244 }
245 }
246
247 useEffect(() => {
Seamherbb14ecb2025-06-09 22:37:20 +0800248 if (!resource?.uploaderId || !subscriberList?.userList) return;
lfmylbhf789e7172025-06-06 18:37:21 +0800249
Seamherbb14ecb2025-06-09 22:37:20 +0800250 const authorId = resource.uploaderId;
lfmylbhf789e7172025-06-06 18:37:21 +0800251 // 设置 isSubscribed 状态
252 const subscribed = subscriberList.userList.some(user => user.userId === authorId);
253 setIsSubscribed(subscribed);
254 }, [subscriberList, resource]);
255
256 // 若浏览用户与资源作者是同一人,则不显示关注按钮。若不是同一人,则显示按钮
257 const handleSubscribe = () => {
258 // 资源作者 ID
Seamherbb14ecb2025-06-09 22:37:20 +0800259 const authorId = resource?.uploaderId;
lfmylbhf789e7172025-06-06 18:37:21 +0800260 // 当前登录用户 ID
LaoeGaocid0773912025-06-09 00:38:40 +0800261 const currentUserId = userId;
lfmylbhf789e7172025-06-06 18:37:21 +0800262
263 // 资源作者与浏览用户是同一人,不显示按钮
LaoeGaocid0773912025-06-09 00:38:40 +0800264 if (!authorId || authorId == currentUserId) {
lfmylbhf789e7172025-06-06 18:37:21 +0800265 return null;
266 }
267
268 return isSubscribed ? (
LaoeGaocid0773912025-06-09 00:38:40 +0800269 // 如果已关注,显示“取消关注”按钮
270 <Button
271 label="取消关注"
272 onClick={async () => {
273 try {
274 const response = await axios.delete(
275 process.env.PUBLIC_URL + '/user/subscription',
276 {
277 params: { userId: currentUserId, followerId: authorId }
lfmylbhf789e7172025-06-06 18:37:21 +0800278 }
LaoeGaocid0773912025-06-09 00:38:40 +0800279 );
280
281 if (response.status === 200) {
282 setIsSubscribed(false); // 🔥立刻更新按钮状态
283 toast.current?.show({
284 severity: 'success',
285 summary: '取消成功',
286 detail: '已取消关注该用户',
287 });
288
289 fetchSubscriber(); // 重新拉取完整关注列表
290 }
291 } catch (error) {
292 console.error('取消关注失败:', error);
293 toast.current?.show({
294 severity: 'error',
295 summary: '错误',
296 detail: '取消关注失败',
297 });
298 }
299 }}
300 />
lfmylbhf789e7172025-06-06 18:37:21 +0800301 ) : (
LaoeGaocid0773912025-06-09 00:38:40 +0800302 // 若未关注,则显示关注按钮
303 <Button
304 label="关注"
305 onClick={async () => {
306 try {
307 const postData = {
308 userId: currentUserId,
309 followerId: authorId,
310 };
311 const response = await axios.post(
312 process.env.PUBLIC_URL + '/user/subscription',
313 postData
314 );
lfmylbhf789e7172025-06-06 18:37:21 +0800315
LaoeGaocid0773912025-06-09 00:38:40 +0800316 if (response.status === 200) {
317 setIsSubscribed(true); // 🔥立刻更新按钮状态
318 toast.current?.show({
319 severity: 'success',
320 summary: '关注成功',
321 detail: '已成功关注该用户',
322 });
lfmylbhf789e7172025-06-06 18:37:21 +0800323
LaoeGaocid0773912025-06-09 00:38:40 +0800324 fetchSubscriber(); // 刷新列表
325 }
326 } catch (error) {
327 console.error('关注失败:', error);
328 toast.current?.show({
329 severity: 'error',
330 summary: '错误',
331 detail: '关注失败',
332 });
333 }
334 }}
335 />
lfmylbhf789e7172025-06-06 18:37:21 +0800336 );
lfmylbhf789e7172025-06-06 18:37:21 +0800337 }
338
339 // 判断该资源是否已被购买, 返回不同的购买按钮
340 const isPurchase = () => {
341 // 作者本人查看资源,不显示购买按钮
Seamherbb14ecb2025-06-09 22:37:20 +0800342 if (resource?.uploaderId == userId) {
LaoeGaocid0773912025-06-09 00:38:40 +0800343 return;
lfmylbhf789e7172025-06-06 18:37:21 +0800344 }
345
346 // 该资源已被购买
347 if (resource?.isPurchase) {
348 return (
LaoeGaocid0773912025-06-09 00:38:40 +0800349 <Button label="已购买" style={{
350 width: "120px", height: "44px",
351 borderRadius: "20px 0 0 20px",
352 }} disabled={true} />
lfmylbhf789e7172025-06-06 18:37:21 +0800353 )
354 } else {
355 // 该资源未被购买
356 return (
LaoeGaocid0773912025-06-09 00:38:40 +0800357 <Button
358 label="立即购买"
359 style={{
360 width: "120px", height: "44px",
361 borderRadius: "20px 0 0 20px",
362 }}
363 onClick={() => setVisible(true)}
364 />
lfmylbhf789e7172025-06-06 18:37:21 +0800365 )
366 }
367 }
368
369 // 购买按钮接口
370 const handlePurchase = async () => {
371 try {
372 const postData = {
LaoeGaocid0773912025-06-09 00:38:40 +0800373 userId, // 记得用户登录状态获取
lfmylbhf789e7172025-06-06 18:37:21 +0800374 resourceId: resource?.resourceId
375 };
376 // 发送POST请求
377 const response = await axios.post(process.env.PUBLIC_URL + '/resource/purchase', postData);
378
379 if (response.status === 200) {
380 toast.current?.show({ severity: 'success', summary: 'Success', detail: '成功购买资源' });
381 // 购买成功
382 setVisible(false);
383 // 刷新购买按钮
384 isPurchase();
385 } else if (response.status === 412) {
386 toast.current?.show({ severity: 'error', summary: 'error', detail: '积分不足,购买失败' });
387 }
388 } catch (error) {
389 console.error('购买资源失败:', error);
390 toast.current?.show({ severity: 'error', summary: 'error', detail: '购买资源失败' });
391 }
392 };
393
394 // 处理收藏操作
395 const handleCollection = async () => {
396 const newCollectionState = !isCollected;
397 const newCollectionCount = newCollectionState ? collectionCount + 1 : collectionCount - 1;
398
399 // 立即更新本地状态
400 setIsCollected(newCollectionState);
401 setCollectionCount(newCollectionCount);
402
403 try {
404 if (newCollectionState) {
405 // 收藏操作
LaoeGaocid0773912025-06-09 00:38:40 +0800406 await axios.post(process.env.PUBLIC_URL + `/resource/collection`, {
Seamherbb14ecb2025-06-09 22:37:20 +0800407 resourceId: resourceId, userId
lfmylbhf789e7172025-06-06 18:37:21 +0800408 });
409 console.log('收藏资源');
410 } else {
411 // 取消收藏操作
LaoeGaocid0773912025-06-09 00:38:40 +0800412 await axios.delete(process.env.PUBLIC_URL + `/resource/collection`, {
413 params: { resourceId: resourceId, userId }
lfmylbhf789e7172025-06-06 18:37:21 +0800414 });
415 console.log('取消收藏资源');
416 }
417 } catch (err) {
418 console.error(newCollectionState ? '收藏资源失败' : '取消收藏失败', err);
419 toast.current?.show({
420 severity: 'error',
421 summary: 'error',
422 detail: newCollectionState ? "收藏资源失败" : "取消收藏失败"
423 });
424
425 // 如果请求失败,回滚状态
426 setIsCollected(!newCollectionState);
427 setCollectionCount(newCollectionState ? collectionCount - 1 : collectionCount + 1);
428 }
429 };
430
431 // 处理点赞行为
432 const handleLike = async () => {
433 const newLikeState = !isLiked;
434 const newLikeCount = newLikeState ? likeCount + 1 : likeCount - 1;
435
436 // 立即更新本地状态
437 setIsLiked(newLikeState);
438 setLikeCount(newLikeCount);
439
440 try {
441 if (newLikeState) {
442 // 点赞操作
LaoeGaocid0773912025-06-09 00:38:40 +0800443 await axios.post(process.env.PUBLIC_URL + `/resource/like`, {
Seamherbb14ecb2025-06-09 22:37:20 +0800444 resourceId: resourceId, userId
lfmylbhf789e7172025-06-06 18:37:21 +0800445 });
446 console.log('点赞资源');
447 } else {
448 // 取消点赞操作
LaoeGaocid0773912025-06-09 00:38:40 +0800449 await axios.delete(process.env.PUBLIC_URL + `/resource/like`, {
450 params: { resourceId: resourceId, userId }
lfmylbhf789e7172025-06-06 18:37:21 +0800451 });
452 console.log('取消点赞资源');
453 }
454 } catch (err) {
455 console.error(newLikeState ? '点赞资源失败' : '取消点赞失败', err);
456 toast.current?.show({
457 severity: 'error',
458 summary: 'error',
459 detail: newLikeState ? "点赞资源失败" : "取消点赞失败"
460 });
461
462 // 如果请求失败,回滚状态
463 setIsLiked(!newLikeState);
464 setLikeCount(newLikeState ? newLikeCount - 1 : newLikeCount + 1);
465 }
466 };
467
468 // 格式化数字显示 (3000 -> 3k)
469 const formatCount = (count?: number): string => {
470 if (count == null) return "0"; // 同时处理 undefined/null
471
472 const absCount = Math.abs(count); // 处理负数
473
474 const format = (num: number, suffix: string) => {
475 const fixed = num.toFixed(1);
476 return fixed.endsWith('.0')
LaoeGaocid0773912025-06-09 00:38:40 +0800477 ? `${Math.floor(num)}${suffix}`
478 : `${fixed}${suffix}`;
lfmylbhf789e7172025-06-06 18:37:21 +0800479 };
480
481 if (absCount >= 1e6) return format(count / 1e6, "m");
482 if (absCount >= 1e3) return format(count / 1e3, "k");
483 return count.toString();
484 };
485
486 // 获取发帖人
487 useEffect(() => {
488 if (!resource) return;
489 // 发帖人
Seamherbb14ecb2025-06-09 22:37:20 +0800490 axios.get(process.env.PUBLIC_URL + `/user/info?userId=${resource?.uploaderId}`)
LaoeGaocid0773912025-06-09 00:38:40 +0800491 .then(res => setUserInfo(res.data))
492 .catch(console.error);
lfmylbhf789e7172025-06-06 18:37:21 +0800493 }, [resource]);
494
495 // 当 resourceId 或分页参数变化时重新拉评论
496 useEffect(() => {
497 if (!resourceId) return;
498
499 fetchComments();
500 }, [resourceId, first, rows]);
501
502
503 //通过评论ID获取评论人信息
504 const getReplyUserName = (replyId: number) => {
505 if (replyId == null || replyId == 0) return '';
506 const replyComment = comments.find(comment => comment.commentId === replyId);
507 if (!replyComment?.userId) return '匿名用户';
508 return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户';
509 };
510
511 // 获取该资源的评论
512 const fetchComments = async () => {
513 try {
514 const pageNumber = first / rows + 1;
515 console.log("当前页" + pageNumber + "size" + rows);
516 const response = await axios.get<CommentList>(
Seamherbb14ecb2025-06-09 22:37:20 +0800517 process.env.PUBLIC_URL + `/comment`, {
LaoeGaocid0773912025-06-09 00:38:40 +0800518 params: { id: resourceId, pageNumber, rows, type: 'resource' }
519 }
lfmylbhf789e7172025-06-06 18:37:21 +0800520 );
521 console.log('获取评论列表:', response.data.records);
522 setComments(response.data.records);
523 setTotalComments(response.data.total);
524 // 拉取评论对应用户信息
525 response.data.records.forEach(comment => {
526 if (comment.userId != null && !commentUserInfos.has(comment.userId)) {
527 axios.get<UserInfo>(
LaoeGaocid0773912025-06-09 00:38:40 +0800528 process.env.PUBLIC_URL + `/user/info`,
529 { params: { userId: comment.userId } }
lfmylbhf789e7172025-06-06 18:37:21 +0800530 ).then(res => {
531 setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data));
532 });
533 }
534 });
535 } catch (err) {
536 console.error('获取评论失败', err);
537 toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' });
538 }
539 };
540
541 // 回复评论接口
542 const publishReply = async (commentId: number) => {
543 if (!replyValue.trim() || !resource) return;
544 console.log('发布评论:', commentId);
545 // console.log(typeof resourceId);
546 try {
547 const newComment: NewComment = {
LaoeGaocid0773912025-06-09 00:38:40 +0800548 userId,
Seamherbb14ecb2025-06-09 22:37:20 +0800549 rewardId: null,
550 threadId: null,
lfmylbhf789e7172025-06-06 18:37:21 +0800551 resourceId: resource.resourceId,
552 replyId: commentId,
553 content: commentValue,
Seamherbb14ecb2025-06-09 22:37:20 +0800554 createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
lfmylbhf789e7172025-06-06 18:37:21 +0800555 };
556
LaoeGaocid0773912025-06-09 00:38:40 +0800557 const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
lfmylbhf789e7172025-06-06 18:37:21 +0800558
559 if (response.status === 200) {
560 toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' });
561 // 更新评论列表
562 fetchComments();
563 setVisibleReply(false)
564 // 清空输入框
565 setReplyValue('');
566 }
567 } catch (error) {
568 console.error('发布评论失败:', error);
569 toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' });
570 }
571 };
572
573 // 发布评论接口
574 const publishComment = async () => {
575 if (!commentValue.trim() || !resource) return;
576
577 try {
578 const newComment: NewComment = {
LaoeGaocid0773912025-06-09 00:38:40 +0800579 userId,
Seamherbb14ecb2025-06-09 22:37:20 +0800580 rewardId: null,
581 threadId: null,
lfmylbhf789e7172025-06-06 18:37:21 +0800582 resourceId: resource.resourceId,
Seamherbb14ecb2025-06-09 22:37:20 +0800583 replyId: null, // 直接评论,不是回复
lfmylbhf789e7172025-06-06 18:37:21 +0800584 content: commentValue,
Seamherbb14ecb2025-06-09 22:37:20 +0800585 createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
lfmylbhf789e7172025-06-06 18:37:21 +0800586 };
587
LaoeGaocid0773912025-06-09 00:38:40 +0800588 const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
lfmylbhf789e7172025-06-06 18:37:21 +0800589
590 if (response.status === 200) {
591 toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
592 // 更新评论列表
593 fetchComments();
594 // 清空输入框
595 setCommentValue('');
596 }
597 } catch (error) {
598 console.error('发布评论失败:', error);
599 toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' });
600 }
601 };
602
603 // 删除评论接口
604 const deleteComment = async (commentId: number) => {
605 if (!resourceId) return;
606
607 try {
608 // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
609 const response = await axios.delete(
LaoeGaocid0773912025-06-09 00:38:40 +0800610 process.env.PUBLIC_URL + `/comment?commentId=${commentId}`
lfmylbhf789e7172025-06-06 18:37:21 +0800611 );
612
613 if (response.status === 200) {
614 fetchComments();
615 toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' });
616 } else {
617 toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' });
618 console.error('删除评论失败,状态码:', response.status);
619 }
620 } catch (error) {
621 console.error('删除评论接口报错:', error);
622 }
623 };
624
625 const ReplyHeader = (
LaoeGaocid0773912025-06-09 00:38:40 +0800626 <div className="flex align-items-center gap-1">
627 <h3>回复评论</h3>
628 </div>
LaoeGaoci85307e62025-05-30 23:28:42 +0800629 );
lfmylbhf789e7172025-06-06 18:37:21 +0800630 if (!resourceId || !userInfo) return <div>Loading...</div>;
LaoeGaoci85307e62025-05-30 23:28:42 +0800631
LaoeGaocid0773912025-06-09 00:38:40 +0800632 return (
633 <div className="resource-detail-container">
634 <Toast ref={toast}></Toast>
635 {/*资源标题*/}
636 <div className="resource-header">
637 {resource?.resourceName}
lfmylbhf789e7172025-06-06 18:37:21 +0800638 </div>
LaoeGaocid0773912025-06-09 00:38:40 +0800639 {/*资源详细信息*/}
640 <div className="resource-info">
641 <Image
642 src={(process.env.NEXT_PUBLIC_NGINX_URL! + resource?.resourcePicture)}
643 alt={resource?.resourceName}
644 width="540px"
645 height="300px"
646 />
647 <div className="resource-info-detail">
648 {/*资源热度*/}
649 <div className="resource-hot">
650 <Fire theme="outline" size="50" fill="#f5a623" strokeWidth={3} />
651 <span className="resource-hot-data">{resource?.hot}</span>
652 </div>
653
654 {/*资源标签*/}
655 <div className="resource-label">
656 {resource?.gameplayList.map((tag, index) => (
657 <Button
658 key={index} label={tag}
659 className="resource-label-button"
660 onClick={() => {
661 router.push(`/resource/classification/`)
662 }}
663 />
664 ))}
665 </div>
666
667 {/*资源浏览量和下载量*/}
668 <div className="resource-data">
669 <div className="resource-data-container">
670 <i className="pi pi-download" />
671 <span className="resource-data-container-number">下载量:{formatCount(resource?.downloads)}</span>
672 </div>
673 </div>
674
675 {/*资源发布时间和更新时间*/}
676 <div className="resource-time">
677 <div className="resource-time-data">
678 发布时间:{resource?.uploadTime}
679 </div>
680 <div className="resource-time-data">
681 更新时间:{resource?.lastUpdateTime}
682 </div>
683 </div>
684 </div>
685 </div>
686 {/*资源总结*/}
687 <div className="resource-summary">
688 {resource?.resourceSummary}
689 </div>
690 {/*关注作者、点赞、抽藏、购买资源*/}
691 <div className="resource-operation">
692 <div className="resource-author">
693 <Avatar
694 image={`${process.env.NEXT_PUBLIC_NGINX_URL}/users/${resourceAuthor?.avatar}`}
695 shape="circle"
696 style={{ width: "60px", height: "60px" }}
697 />
698 <span className="resource-author-name">{resourceAuthor?.username}</span>
699
700 {handleSubscribe()}
701 </div>
702
703 <div className="resource-operation-detail">
704 <div className="resource-operation-detail-data">
705 <i
706 className={isCollected ? "pi pi-star-fill" : "pi pi-star"}
707 onClick={handleCollection}
708 style={{
709 cursor: 'pointer',
710 fontSize: '30px',
711 color: isCollected ? 'rgba(82, 102, 101, 1)' : 'inherit',
712 transition: 'color 0.3s ease'
713 }}
714 />
715 <span>{formatCount(collectionCount)}</span>
716 </div>
717
718
719
720 <div className="resource-operation-detail-data">
721 {isLiked ? <SmilingFace
722 theme="filled"
723 size="30"
724 fill="#526665"
725 strokeWidth={5}
726 onClick={handleLike}
727 style={{ cursor: 'pointer' }}
728 />
729 : <SmilingFace
730 theme="outline"
731 size="30"
732 fill="#526665"
733 strokeWidth={5}
734 onClick={handleLike}
735 style={{ cursor: 'pointer' }}
736 />
737 }
738 <span>{formatCount(likeCount)}</span>
739 </div>
740
741 <ButtonGroup >
742 {isPurchase()}
743 <Button label={"$" + resource?.price} style={{
744 height: "44px", background: "rgba(82, 102, 101, 1)",
745 borderStyle: "solid", borderWidth: "1px", borderColor: "rgba(82, 102, 101, 1)",
746 borderRadius: "0 20px 20px 0", fontSize: "26px",
747 }} disabled={true} />
748 </ButtonGroup>
749 </div>
750 </div>
751 {/*资源详情*/}
752 <div className="resource-detail">
753 <h1 className="resource-detail-title">资源详情</h1>
754 <div className="resource-detail-text">
755 {resource?.resourceDetail}
756 </div>
757 </div>
758 {/* 评论列表 */}
759 <div className="comments-section">
760 <div className="comments-header">
761 <h2>评论 ({totalComments})</h2>
762 <Link href="/community" className="no-underline">进入社区</Link>
763 </div>
764 <div className="comments-input">
Seamherbb14ecb2025-06-09 22:37:20 +0800765 <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" />
LaoeGaocid0773912025-06-09 00:38:40 +0800766 <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} />
767 <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} />
768 </div>
769 <div className="comments-list">
770 {comments.map((comment, index) => (
771 <div key={comment.commentId} className="comment-item">
772 <div className="comment-user">
773 <Avatar
Seamherbb14ecb2025-06-09 22:37:20 +0800774 image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
LaoeGaocid0773912025-06-09 00:38:40 +0800775 size="normal"
776 shape="circle"
777 />
778 <div className="comment-meta">
779 <span className="username">
780 {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'}
781 </span>
782 <div className="comment-time">
783 <span className="floor">#{first + index + 1}楼</span>
784 <span className="time">{comment.createAt}</span>
785 </div>
786 </div>
787 <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} />
788 </div>
789 <div className="comment-content">
790 {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>}
791 <p>{comment.content}</p>
792 </div>
793 <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置
794 ref={el => {
795 if (el) ops.current[index] = el;
796 }}>
797 <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} />
798 {comment.userId === userId &&
799 <Button
800 label="删除"
801 text
802 size="small"
803 onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }}
804 />
805 }
806 </OverlayPanel>
807 <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
808 <div className="reply-input">
Seamherbb14ecb2025-06-09 22:37:20 +0800809 <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" />
LaoeGaocid0773912025-06-09 00:38:40 +0800810 <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} />
811 <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} />
812 </div>
813 </Sidebar>
814 </div>
815 ))}
816 {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
817 </div>
818 </div>
819
820 {/*用户购买资源弹窗*/}
821 <Dialog
822 header="购买资源"
823 visible={visible}
824 onHide={() => setVisible(false)}
825 className="purchase-dialog"
826 modal
827 footer={
828 <div className="dialog-footer">
829 <Button label="购买" icon="pi pi-check" onClick={handlePurchase} autoFocus />
830 <Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" />
831 </div>
832 }
833 >
834 <div className="form-text">
835 购买该资源需要{resource?.price}积分,是否购买?
836 </div>
837 </Dialog>
838 </div>
lfmylbhf789e7172025-06-06 18:37:21 +0800839 )
840}
841