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