blob: baec9a635f597a08a658226bf0885ff8244cceea [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
Seamherd6c950f2025-06-09 23:48:28 +0800573 const buyResource = async () => {
574 console.log("Buy Resource")
575 try {
576 const response = await axios.post(process.env.PUBLIC_URL + '/resource/purchase', {
577 userId,
578 resourceId
579 });
580
581 if (response.status === 200) {
582 toast.current?.show({ severity: 'success', summary: 'Success', detail: '购买成功' });
583 } else {
584 toast.current?.show({ severity: 'error', summary: 'error', detail: '购买失败' });
585
586 }
587 } catch (error) {
588 console.error('购买失败:', error);
589 toast.current?.show({ severity: 'error', summary: 'error', detail: '购买失败' });
590 }
591 }
592
lfmylbhf789e7172025-06-06 18:37:21 +0800593 // 发布评论接口
594 const publishComment = async () => {
595 if (!commentValue.trim() || !resource) return;
596
597 try {
598 const newComment: NewComment = {
LaoeGaocid0773912025-06-09 00:38:40 +0800599 userId,
Seamherbb14ecb2025-06-09 22:37:20 +0800600 rewardId: null,
601 threadId: null,
lfmylbhf789e7172025-06-06 18:37:21 +0800602 resourceId: resource.resourceId,
Seamherbb14ecb2025-06-09 22:37:20 +0800603 replyId: null, // 直接评论,不是回复
lfmylbhf789e7172025-06-06 18:37:21 +0800604 content: commentValue,
Seamherbb14ecb2025-06-09 22:37:20 +0800605 createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
lfmylbhf789e7172025-06-06 18:37:21 +0800606 };
607
LaoeGaocid0773912025-06-09 00:38:40 +0800608 const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
lfmylbhf789e7172025-06-06 18:37:21 +0800609
610 if (response.status === 200) {
611 toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
612 // 更新评论列表
613 fetchComments();
614 // 清空输入框
615 setCommentValue('');
616 }
617 } catch (error) {
618 console.error('发布评论失败:', error);
619 toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' });
620 }
621 };
622
623 // 删除评论接口
624 const deleteComment = async (commentId: number) => {
625 if (!resourceId) return;
626
627 try {
628 // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
629 const response = await axios.delete(
LaoeGaocid0773912025-06-09 00:38:40 +0800630 process.env.PUBLIC_URL + `/comment?commentId=${commentId}`
lfmylbhf789e7172025-06-06 18:37:21 +0800631 );
632
633 if (response.status === 200) {
634 fetchComments();
635 toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' });
636 } else {
637 toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' });
638 console.error('删除评论失败,状态码:', response.status);
639 }
640 } catch (error) {
641 console.error('删除评论接口报错:', error);
642 }
643 };
644
645 const ReplyHeader = (
LaoeGaocid0773912025-06-09 00:38:40 +0800646 <div className="flex align-items-center gap-1">
647 <h3>回复评论</h3>
648 </div>
LaoeGaoci85307e62025-05-30 23:28:42 +0800649 );
lfmylbhf789e7172025-06-06 18:37:21 +0800650 if (!resourceId || !userInfo) return <div>Loading...</div>;
LaoeGaoci85307e62025-05-30 23:28:42 +0800651
LaoeGaocid0773912025-06-09 00:38:40 +0800652 return (
653 <div className="resource-detail-container">
654 <Toast ref={toast}></Toast>
655 {/*资源标题*/}
656 <div className="resource-header">
657 {resource?.resourceName}
lfmylbhf789e7172025-06-06 18:37:21 +0800658 </div>
LaoeGaocid0773912025-06-09 00:38:40 +0800659 {/*资源详细信息*/}
660 <div className="resource-info">
661 <Image
662 src={(process.env.NEXT_PUBLIC_NGINX_URL! + resource?.resourcePicture)}
663 alt={resource?.resourceName}
664 width="540px"
665 height="300px"
666 />
667 <div className="resource-info-detail">
668 {/*资源热度*/}
669 <div className="resource-hot">
670 <Fire theme="outline" size="50" fill="#f5a623" strokeWidth={3} />
671 <span className="resource-hot-data">{resource?.hot}</span>
672 </div>
673
674 {/*资源标签*/}
675 <div className="resource-label">
676 {resource?.gameplayList.map((tag, index) => (
677 <Button
678 key={index} label={tag}
679 className="resource-label-button"
680 onClick={() => {
681 router.push(`/resource/classification/`)
682 }}
683 />
684 ))}
685 </div>
686
687 {/*资源浏览量和下载量*/}
688 <div className="resource-data">
689 <div className="resource-data-container">
690 <i className="pi pi-download" />
691 <span className="resource-data-container-number">下载量:{formatCount(resource?.downloads)}</span>
692 </div>
693 </div>
694
695 {/*资源发布时间和更新时间*/}
696 <div className="resource-time">
697 <div className="resource-time-data">
698 发布时间:{resource?.uploadTime}
699 </div>
700 <div className="resource-time-data">
701 更新时间:{resource?.lastUpdateTime}
702 </div>
703 </div>
704 </div>
705 </div>
706 {/*资源总结*/}
707 <div className="resource-summary">
708 {resource?.resourceSummary}
709 </div>
710 {/*关注作者、点赞、抽藏、购买资源*/}
711 <div className="resource-operation">
712 <div className="resource-author">
713 <Avatar
714 image={`${process.env.NEXT_PUBLIC_NGINX_URL}/users/${resourceAuthor?.avatar}`}
715 shape="circle"
716 style={{ width: "60px", height: "60px" }}
717 />
718 <span className="resource-author-name">{resourceAuthor?.username}</span>
719
720 {handleSubscribe()}
721 </div>
722
723 <div className="resource-operation-detail">
724 <div className="resource-operation-detail-data">
725 <i
726 className={isCollected ? "pi pi-star-fill" : "pi pi-star"}
727 onClick={handleCollection}
728 style={{
729 cursor: 'pointer',
730 fontSize: '30px',
731 color: isCollected ? 'rgba(82, 102, 101, 1)' : 'inherit',
732 transition: 'color 0.3s ease'
733 }}
734 />
735 <span>{formatCount(collectionCount)}</span>
736 </div>
737
738
739
740 <div className="resource-operation-detail-data">
741 {isLiked ? <SmilingFace
742 theme="filled"
743 size="30"
744 fill="#526665"
745 strokeWidth={5}
746 onClick={handleLike}
747 style={{ cursor: 'pointer' }}
748 />
749 : <SmilingFace
750 theme="outline"
751 size="30"
752 fill="#526665"
753 strokeWidth={5}
754 onClick={handleLike}
755 style={{ cursor: 'pointer' }}
756 />
757 }
758 <span>{formatCount(likeCount)}</span>
759 </div>
760
761 <ButtonGroup >
LaoeGaocid0773912025-06-09 00:38:40 +0800762 <Button label={"$" + resource?.price} style={{
763 height: "44px", background: "rgba(82, 102, 101, 1)",
764 borderStyle: "solid", borderWidth: "1px", borderColor: "rgba(82, 102, 101, 1)",
765 borderRadius: "0 20px 20px 0", fontSize: "26px",
Seamherd6c950f2025-06-09 23:48:28 +0800766 }} onClick={buyResource} />
LaoeGaocid0773912025-06-09 00:38:40 +0800767 </ButtonGroup>
768 </div>
769 </div>
770 {/*资源详情*/}
771 <div className="resource-detail">
772 <h1 className="resource-detail-title">资源详情</h1>
773 <div className="resource-detail-text">
774 {resource?.resourceDetail}
775 </div>
776 </div>
777 {/* 评论列表 */}
778 <div className="comments-section">
779 <div className="comments-header">
780 <h2>评论 ({totalComments})</h2>
781 <Link href="/community" className="no-underline">进入社区</Link>
782 </div>
783 <div className="comments-input">
Seamherbb14ecb2025-06-09 22:37:20 +0800784 <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" />
LaoeGaocid0773912025-06-09 00:38:40 +0800785 <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} />
786 <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} />
787 </div>
788 <div className="comments-list">
789 {comments.map((comment, index) => (
790 <div key={comment.commentId} className="comment-item">
791 <div className="comment-user">
792 <Avatar
Seamherbb14ecb2025-06-09 22:37:20 +0800793 image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
LaoeGaocid0773912025-06-09 00:38:40 +0800794 size="normal"
795 shape="circle"
796 />
797 <div className="comment-meta">
798 <span className="username">
799 {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'}
800 </span>
801 <div className="comment-time">
802 <span className="floor">#{first + index + 1}楼</span>
803 <span className="time">{comment.createAt}</span>
804 </div>
805 </div>
806 <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} />
807 </div>
808 <div className="comment-content">
809 {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>}
810 <p>{comment.content}</p>
811 </div>
812 <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置
813 ref={el => {
814 if (el) ops.current[index] = el;
815 }}>
816 <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} />
817 {comment.userId === userId &&
818 <Button
819 label="删除"
820 text
821 size="small"
822 onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }}
823 />
824 }
825 </OverlayPanel>
826 <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
827 <div className="reply-input">
Seamherbb14ecb2025-06-09 22:37:20 +0800828 <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" />
LaoeGaocid0773912025-06-09 00:38:40 +0800829 <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} />
830 <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} />
831 </div>
832 </Sidebar>
833 </div>
834 ))}
835 {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
836 </div>
837 </div>
838
839 {/*用户购买资源弹窗*/}
840 <Dialog
841 header="购买资源"
842 visible={visible}
843 onHide={() => setVisible(false)}
844 className="purchase-dialog"
845 modal
846 footer={
847 <div className="dialog-footer">
848 <Button label="购买" icon="pi pi-check" onClick={handlePurchase} autoFocus />
849 <Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" />
850 </div>
851 }
852 >
853 <div className="form-text">
854 购买该资源需要{resource?.price}积分,是否购买?
855 </div>
856 </Dialog>
857 </div>
lfmylbhf789e7172025-06-06 18:37:21 +0800858 )
859}
860