LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 1 | 'use client'; |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +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"; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 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 | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 27 | import { useLocalStorage } from '../../../hook/useLocalStorage'; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 28 | // 样式 |
| 29 | import './resource-detail.scss'; |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 30 | interface User { |
| 31 | Id: number; |
| 32 | } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 33 | // 种子 |
| 34 | interface Torrent { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 35 | torrentRecordId: number; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 36 | torrentUrl: string; |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 37 | infoHash: string; |
| 38 | uploadTime: string; |
| 39 | uploaderUserId: number; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 40 | } |
| 41 | |
| 42 | // 资源版本 |
| 43 | interface ResourceVersion { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 44 | resourceVersionId: string; // 资源版本id |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 45 | resourceVersionName: string; // 资源版本名称 |
| 46 | compatibleVersions: string[]; // 兼容的游戏版本列表 |
| 47 | torrentList: Torrent[]; // 种子列表 |
| 48 | } |
| 49 | |
| 50 | // 资源信息 |
| 51 | interface Resource { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 52 | resourceId: number; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 53 | resourceName: string; // 资源标题 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 54 | resourcePicture: string; // 资源照片网址 |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 55 | resourceSummary: string; // 资源简介(一句话) |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 56 | resourceDetail: string; // 资源介绍 |
| 57 | uploadTime: string; // 上传时间 |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 58 | lastUpdateTime: string; // 最近更新时间 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 59 | price: number; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 60 | downloads: number; // 下载数 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 61 | likes: number; // 点赞数 |
| 62 | collections: number; // 收藏数 |
| 63 | comments: number; // 评论数 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 64 | classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 65 | hot: number; // 资源热度 |
| 66 | gameplayList: string[]; // 资源标签 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 67 | resourceVersionList: ResourceVersion[]; // 资源版本列表 |
| 68 | isCollect: boolean; // 是否被收藏 |
| 69 | isLike: boolean; // 是否被点赞 |
| 70 | isPurchase: boolean; // 是否被购买 |
| 71 | isUpload: boolean; // 是否是该用户上传的 |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 72 | uploaderId: number; // 资源上传者的id |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 73 | } |
| 74 | |
| 75 | // 评论信息 |
| 76 | interface 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 | // 评论列表 |
| 86 | interface CommentList { |
| 87 | total: number; // 评论总数 |
| 88 | records: Comment[]; // 当前页评论数组 |
| 89 | } |
| 90 | |
| 91 | // 用户信息 |
| 92 | interface UserInfo { |
| 93 | userId: number; |
| 94 | username: string; |
| 95 | avatar: string; |
| 96 | signature: string; |
| 97 | } |
| 98 | // 新评论接口 |
| 99 | interface NewComment { |
| 100 | userId: number; |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 101 | threadId: number | null; |
| 102 | resourceId: number | null; |
| 103 | rewardId: number | null; |
| 104 | replyId: number | null; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 105 | content: string; |
| 106 | createAt: string; |
| 107 | } |
| 108 | |
| 109 | // 资源作者 |
| 110 | interface ResourceAuthor { |
| 111 | userId: number; |
| 112 | username: string; |
| 113 | avatar: string; |
| 114 | signature: string; |
| 115 | } |
| 116 | |
| 117 | // 关注 |
| 118 | interface Subscriber { |
| 119 | userId: number; |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 120 | username: string; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 121 | } |
| 122 | |
| 123 | // 关注列表 |
| 124 | interface SubscriberList { |
| 125 | userList: Subscriber[]; |
| 126 | } |
| 127 | |
| 128 | export default function ResourceDetail() { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 129 | const user = useLocalStorage<User>('user'); |
| 130 | const userId: number = user?.Id ?? -1; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 131 | // 获取URL参数 |
| 132 | const params = useParams<{ resourceId: string }>(); |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 133 | const resourceId = Number(decodeURIComponent(params.resourceId)); // 防止中文路径乱码 |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 134 | // 页面跳转 |
| 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); |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 188 | const response = await axios.get<Resource>(process.env.PUBLIC_URL + `/resource/info`, { |
| 189 | params: { resourceId: resourceId, userId } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 190 | }); |
| 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 | |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 200 | setResourceAuthorId(response.data.uploaderId); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 201 | } 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); |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 217 | const response = await axios.get<ResourceAuthor>(process.env.PUBLIC_URL + `/user/info`, { |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 218 | 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`, { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 237 | params: { userId } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 238 | }); |
| 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(() => { |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 248 | if (!resource?.uploaderId || !subscriberList?.userList) return; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 249 | |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 250 | const authorId = resource.uploaderId; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 251 | // 设置 isSubscribed 状态 |
| 252 | const subscribed = subscriberList.userList.some(user => user.userId === authorId); |
| 253 | setIsSubscribed(subscribed); |
| 254 | }, [subscriberList, resource]); |
| 255 | |
| 256 | // 若浏览用户与资源作者是同一人,则不显示关注按钮。若不是同一人,则显示按钮 |
| 257 | const handleSubscribe = () => { |
| 258 | // 资源作者 ID |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 259 | const authorId = resource?.uploaderId; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 260 | // 当前登录用户 ID |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 261 | const currentUserId = userId; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 262 | |
| 263 | // 资源作者与浏览用户是同一人,不显示按钮 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 264 | if (!authorId || authorId == currentUserId) { |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 265 | return null; |
| 266 | } |
| 267 | |
| 268 | return isSubscribed ? ( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 269 | // 如果已关注,显示“取消关注”按钮 |
| 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 } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 278 | } |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 279 | ); |
| 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 | /> |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 301 | ) : ( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 302 | // 若未关注,则显示关注按钮 |
| 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 | ); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 315 | |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 316 | if (response.status === 200) { |
| 317 | setIsSubscribed(true); // 🔥立刻更新按钮状态 |
| 318 | toast.current?.show({ |
| 319 | severity: 'success', |
| 320 | summary: '关注成功', |
| 321 | detail: '已成功关注该用户', |
| 322 | }); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 323 | |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 324 | fetchSubscriber(); // 刷新列表 |
| 325 | } |
| 326 | } catch (error) { |
| 327 | console.error('关注失败:', error); |
| 328 | toast.current?.show({ |
| 329 | severity: 'error', |
| 330 | summary: '错误', |
| 331 | detail: '关注失败', |
| 332 | }); |
| 333 | } |
| 334 | }} |
| 335 | /> |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 336 | ); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 337 | } |
| 338 | |
| 339 | // 判断该资源是否已被购买, 返回不同的购买按钮 |
| 340 | const isPurchase = () => { |
| 341 | // 作者本人查看资源,不显示购买按钮 |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 342 | if (resource?.uploaderId == userId) { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 343 | return; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 344 | } |
| 345 | |
| 346 | // 该资源已被购买 |
| 347 | if (resource?.isPurchase) { |
| 348 | return ( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 349 | <Button label="已购买" style={{ |
| 350 | width: "120px", height: "44px", |
| 351 | borderRadius: "20px 0 0 20px", |
| 352 | }} disabled={true} /> |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 353 | ) |
| 354 | } else { |
| 355 | // 该资源未被购买 |
| 356 | return ( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 357 | <Button |
| 358 | label="立即购买" |
| 359 | style={{ |
| 360 | width: "120px", height: "44px", |
| 361 | borderRadius: "20px 0 0 20px", |
| 362 | }} |
| 363 | onClick={() => setVisible(true)} |
| 364 | /> |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 365 | ) |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | // 购买按钮接口 |
| 370 | const handlePurchase = async () => { |
| 371 | try { |
| 372 | const postData = { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 373 | userId, // 记得用户登录状态获取 |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 374 | 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 | // 收藏操作 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 406 | await axios.post(process.env.PUBLIC_URL + `/resource/collection`, { |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 407 | resourceId: resourceId, userId |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 408 | }); |
| 409 | console.log('收藏资源'); |
| 410 | } else { |
| 411 | // 取消收藏操作 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 412 | await axios.delete(process.env.PUBLIC_URL + `/resource/collection`, { |
| 413 | params: { resourceId: resourceId, userId } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 414 | }); |
| 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 | // 点赞操作 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 443 | await axios.post(process.env.PUBLIC_URL + `/resource/like`, { |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 444 | resourceId: resourceId, userId |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 445 | }); |
| 446 | console.log('点赞资源'); |
| 447 | } else { |
| 448 | // 取消点赞操作 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 449 | await axios.delete(process.env.PUBLIC_URL + `/resource/like`, { |
| 450 | params: { resourceId: resourceId, userId } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 451 | }); |
| 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') |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 477 | ? `${Math.floor(num)}${suffix}` |
| 478 | : `${fixed}${suffix}`; |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 479 | }; |
| 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 | // 发帖人 |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 490 | axios.get(process.env.PUBLIC_URL + `/user/info?userId=${resource?.uploaderId}`) |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 491 | .then(res => setUserInfo(res.data)) |
| 492 | .catch(console.error); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 493 | }, [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>( |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 517 | process.env.PUBLIC_URL + `/comment`, { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 518 | params: { id: resourceId, pageNumber, rows, type: 'resource' } |
| 519 | } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 520 | ); |
| 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>( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 528 | process.env.PUBLIC_URL + `/user/info`, |
| 529 | { params: { userId: comment.userId } } |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 530 | ).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 = { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 548 | userId, |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 549 | rewardId: null, |
| 550 | threadId: null, |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 551 | resourceId: resource.resourceId, |
| 552 | replyId: commentId, |
| 553 | content: commentValue, |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 554 | createAt: new Date().toISOString().slice(0, 10).replace('T', ' ') |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 555 | }; |
| 556 | |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 557 | const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 558 | |
| 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 = { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 579 | userId, |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 580 | rewardId: null, |
| 581 | threadId: null, |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 582 | resourceId: resource.resourceId, |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 583 | replyId: null, // 直接评论,不是回复 |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 584 | content: commentValue, |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 585 | createAt: new Date().toISOString().slice(0, 10).replace('T', ' ') |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 586 | }; |
| 587 | |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 588 | const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 589 | |
| 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( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 610 | process.env.PUBLIC_URL + `/comment?commentId=${commentId}` |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 611 | ); |
| 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 = ( |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 626 | <div className="flex align-items-center gap-1"> |
| 627 | <h3>回复评论</h3> |
| 628 | </div> |
LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 629 | ); |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 630 | if (!resourceId || !userInfo) return <div>Loading...</div>; |
LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 631 | |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 632 | return ( |
| 633 | <div className="resource-detail-container"> |
| 634 | <Toast ref={toast}></Toast> |
| 635 | {/*资源标题*/} |
| 636 | <div className="resource-header"> |
| 637 | {resource?.resourceName} |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 638 | </div> |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 639 | {/*资源详细信息*/} |
| 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"> |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 765 | <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" /> |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 766 | <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 |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 774 | image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'} |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 775 | 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"> |
Seamher | bb14ecb | 2025-06-09 22:37:20 +0800 | [diff] [blame] | 809 | <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" /> |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame] | 810 | <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> |
lfmylbhf | 789e717 | 2025-06-06 18:37:21 +0800 | [diff] [blame] | 839 | ) |
| 840 | } |
| 841 | |