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