| 'use client'; |
| import React, { useEffect, useRef, useState } from 'react'; |
| import { Image } from "primereact/image"; |
| import { Button } from "primereact/button"; |
| import { Avatar } from "primereact/avatar"; |
| import { ButtonGroup } from "primereact/buttongroup"; |
| import { InputText } from "primereact/inputtext"; |
| import { Dialog } from 'primereact/dialog'; |
| // 引入图标 |
| import 'primeicons/primeicons.css'; |
| // 引入火苗图标 |
| import Fire from '@icon-park/react/lib/icons/Fire'; |
| import SmilingFace from '@icon-park/react/lib/icons/SmilingFace'; |
| // 消息提醒 |
| import { Toast } from 'primereact/toast'; |
| // 接口传输 |
| import axios from "axios"; |
| // 页面跳转 |
| import { useParams } from 'next/navigation' |
| import { useRouter } from 'next/navigation'; |
| import Link from 'next/link'; |
| // 回复评论 |
| import { OverlayPanel } from 'primereact/overlaypanel'; |
| import { Sidebar } from 'primereact/sidebar'; |
| // 分页 |
| import { Paginator, PaginatorPageChangeEvent } from 'primereact/paginator'; |
| import { useLocalStorage } from '../../../hook/useLocalStorage'; |
| // 样式 |
| import './resource-detail.scss'; |
| interface User { |
| Id: number; |
| } |
| // 种子 |
| interface Torrent { |
| torrentRecordId: number; |
| torrentUrl: string; |
| infoHash: string; |
| uploadTime: string; |
| uploaderUserId: number; |
| } |
| |
| // 资源版本 |
| interface ResourceVersion { |
| resourceVersionId: string; // 资源版本id |
| resourceVersionName: string; // 资源版本名称 |
| compatibleVersions: string[]; // 兼容的游戏版本列表 |
| torrentList: Torrent[]; // 种子列表 |
| } |
| |
| // 资源信息 |
| interface Resource { |
| resourceId: number; |
| resourceName: string; // 资源标题 |
| resourcePicture: string; // 资源照片网址 |
| resourceSummary: string; // 资源简介(一句话) |
| resourceDetail: string; // 资源介绍 |
| uploadTime: string; // 上传时间 |
| lastUpdateTime: string; // 最近更新时间 |
| price: number; |
| downloads: number; // 下载数 |
| likes: number; // 点赞数 |
| collections: number; // 收藏数 |
| comments: number; // 评论数 |
| classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map |
| hot: number; // 资源热度 |
| gameplayList: string[]; // 资源标签 |
| resourceVersionList: ResourceVersion[]; // 资源版本列表 |
| isCollect: boolean; // 是否被收藏 |
| isLike: boolean; // 是否被点赞 |
| isPurchase: boolean; // 是否被购买 |
| isUpload: boolean; // 是否是该用户上传的 |
| uploaderId: number; // 资源上传者的id |
| } |
| |
| // 评论信息 |
| interface Comment { |
| commentId: number; |
| userId: number | null; |
| replyId: number; |
| resourceId: number; |
| reawardId: number; |
| content: string; |
| createAt: string; |
| } |
| // 评论列表 |
| interface CommentList { |
| total: number; // 评论总数 |
| records: Comment[]; // 当前页评论数组 |
| } |
| |
| // 用户信息 |
| interface UserInfo { |
| userId: number; |
| username: string; |
| avatar: string; |
| signature: string; |
| } |
| // 新评论接口 |
| interface NewComment { |
| userId: number; |
| threadId: number | null; |
| resourceId: number | null; |
| rewardId: number | null; |
| replyId: number | null; |
| content: string; |
| createAt: string; |
| } |
| |
| // 资源作者 |
| interface ResourceAuthor { |
| userId: number; |
| username: string; |
| avatar: string; |
| signature: string; |
| } |
| |
| // 关注 |
| interface Subscriber { |
| userId: number; |
| username: string; |
| } |
| |
| // 关注列表 |
| interface SubscriberList { |
| userList: Subscriber[]; |
| } |
| |
| export default function ResourceDetail() { |
| const user = useLocalStorage<User>('user'); |
| const userId: number = user?.Id ?? -1; |
| // 获取URL参数 |
| const params = useParams<{ resourceId: string }>(); |
| const resourceId = Number(decodeURIComponent(params.resourceId)); // 防止中文路径乱码 |
| // 页面跳转 |
| const router = useRouter(); |
| |
| // 资源信息 |
| const [resource, setResource] = useState<Resource | null>(null); |
| // 资源作者信息 |
| const [resourceAuthor, setResourceAuthor] = useState<ResourceAuthor>(); |
| // 资源作者id |
| const [resourceAuthorId, setResourceAuthorId] = useState<number>(0); |
| // 关注列表 |
| const [subscriberList, setSubscriberList] = useState<SubscriberList>(); |
| // 添加本地关注状态 |
| const [isSubscribed, setIsSubscribed] = useState(false); |
| // 添加本地收藏状态 |
| const [isCollected, setIsCollected] = useState(false); |
| const [collectionCount, setCollectionCount] = useState(0); |
| // 添加本地点赞状态 |
| const [isLiked, setIsLiked] = useState(false); |
| const [likeCount, setLikeCount] = useState(0); |
| // 发帖人信息 |
| const [userInfo, setUserInfo] = useState<UserInfo | null>(null); |
| // 评论人信息 |
| const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map()); |
| //评论 |
| const [comments, setComments] = useState<Comment[]>([]); |
| const [commentValue, setCommentValue] = useState<string>(''); |
| const [totalComments, setTotalComments] = useState<number>(0); |
| // 回复 |
| const [replyValue, setReplyValue] = useState<string>(''); |
| const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视 |
| // 评论选择框 |
| const ops = useRef<OverlayPanel[]>([]); |
| // 购买弹窗状态 |
| const [visible, setVisible] = useState<boolean>(false); |
| // 消息提醒 |
| const toast = useRef<Toast>(null); |
| |
| // 分页 |
| const [first, setFirst] = useState<number>(0); |
| const [rows, setRows] = useState<number>(5); |
| const onPageChange = (event: PaginatorPageChangeEvent) => { |
| setFirst(event.first); |
| setRows(event.rows); |
| }; |
| |
| useEffect(() => { |
| fetchResourceInfo(); |
| }, []); |
| |
| |
| // 获取资源信息 |
| const fetchResourceInfo = async () => { |
| try { |
| // console.log(resourceId); |
| const response = await axios.get<Resource>(process.env.PUBLIC_URL + `/resource/info`, { |
| params: { resourceId: resourceId, userId } |
| }); |
| console.log('获取资源信息:', response.data); |
| setResource(response.data); |
| |
| // 初始化本地收藏状态 |
| if (response.data) { |
| setIsCollected(response.data.isCollect); |
| setCollectionCount(response.data.collections); |
| } |
| |
| setResourceAuthorId(response.data.uploaderId); |
| } catch (err) { |
| console.error('获取资源信息失败', err); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: "获取资源信息失败" }); |
| } |
| }; |
| |
| // 获取到资源作者id时,获取资源作者信息 |
| useEffect(() => { |
| fetchResourceAuthor(); |
| }, [resourceAuthorId]); |
| |
| // 获取资源作者信息 |
| const fetchResourceAuthor = async () => { |
| try { |
| // console.log(resourceId); |
| // console.log(resourceAuthorId); |
| const response = await axios.get<ResourceAuthor>(process.env.PUBLIC_URL + `/user/info`, { |
| params: { userId: resourceAuthorId } |
| }); |
| console.log('获取资源作者信息:', response.data); |
| setResourceAuthor(response.data); |
| // setResourceAuthorId(response.data.userId); |
| } catch (err) { |
| console.error('获取资源作者信息失败', err); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: "获取资源作者信息失败" }); |
| } |
| }; |
| |
| useEffect(() => { |
| fetchSubscriber(); |
| }, []); |
| |
| // 获取正在浏览资源的用户的关注列表 |
| const fetchSubscriber = async () => { |
| try { |
| const response = await axios.get<SubscriberList>(process.env.PUBLIC_URL + `/user/subscriber`, { |
| params: { userId } |
| }); |
| console.log("关注列表:", response.data); |
| setSubscriberList(response.data); |
| } catch (err) { |
| console.error('获取浏览用户关注列表失败', err); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: "获取浏览用户关注列表失败" }); |
| } |
| } |
| |
| useEffect(() => { |
| if (!resource?.uploaderId || !subscriberList?.userList) return; |
| |
| const authorId = resource.uploaderId; |
| // 设置 isSubscribed 状态 |
| const subscribed = subscriberList.userList.some(user => user.userId === authorId); |
| setIsSubscribed(subscribed); |
| }, [subscriberList, resource]); |
| |
| // 若浏览用户与资源作者是同一人,则不显示关注按钮。若不是同一人,则显示按钮 |
| const handleSubscribe = () => { |
| // 资源作者 ID |
| const authorId = resource?.uploaderId; |
| // 当前登录用户 ID |
| const currentUserId = userId; |
| |
| // 资源作者与浏览用户是同一人,不显示按钮 |
| if (!authorId || authorId == currentUserId) { |
| return null; |
| } |
| |
| return isSubscribed ? ( |
| // 如果已关注,显示“取消关注”按钮 |
| <Button |
| label="取消关注" |
| onClick={async () => { |
| try { |
| const response = await axios.delete( |
| process.env.PUBLIC_URL + '/user/subscription', |
| { |
| params: { userId: currentUserId, followerId: authorId } |
| } |
| ); |
| |
| if (response.status === 200) { |
| setIsSubscribed(false); // 🔥立刻更新按钮状态 |
| toast.current?.show({ |
| severity: 'success', |
| summary: '取消成功', |
| detail: '已取消关注该用户', |
| }); |
| |
| fetchSubscriber(); // 重新拉取完整关注列表 |
| } |
| } catch (error) { |
| console.error('取消关注失败:', error); |
| toast.current?.show({ |
| severity: 'error', |
| summary: '错误', |
| detail: '取消关注失败', |
| }); |
| } |
| }} |
| /> |
| ) : ( |
| // 若未关注,则显示关注按钮 |
| <Button |
| label="关注" |
| onClick={async () => { |
| try { |
| const postData = { |
| userId: currentUserId, |
| followerId: authorId, |
| }; |
| const response = await axios.post( |
| process.env.PUBLIC_URL + '/user/subscription', |
| postData |
| ); |
| |
| if (response.status === 200) { |
| setIsSubscribed(true); // 🔥立刻更新按钮状态 |
| toast.current?.show({ |
| severity: 'success', |
| summary: '关注成功', |
| detail: '已成功关注该用户', |
| }); |
| |
| fetchSubscriber(); // 刷新列表 |
| } |
| } catch (error) { |
| console.error('关注失败:', error); |
| toast.current?.show({ |
| severity: 'error', |
| summary: '错误', |
| detail: '关注失败', |
| }); |
| } |
| }} |
| /> |
| ); |
| } |
| |
| // 判断该资源是否已被购买, 返回不同的购买按钮 |
| const isPurchase = () => { |
| // 作者本人查看资源,不显示购买按钮 |
| if (resource?.uploaderId == userId) { |
| return; |
| } |
| |
| // 该资源已被购买 |
| if (resource?.isPurchase) { |
| return ( |
| <Button label="已购买" style={{ |
| width: "120px", height: "44px", |
| borderRadius: "20px 0 0 20px", |
| }} disabled={true} /> |
| ) |
| } else { |
| // 该资源未被购买 |
| return ( |
| <Button |
| label="立即购买" |
| style={{ |
| width: "120px", height: "44px", |
| borderRadius: "20px 0 0 20px", |
| }} |
| onClick={() => setVisible(true)} |
| /> |
| ) |
| } |
| } |
| |
| // 购买按钮接口 |
| const handlePurchase = async () => { |
| try { |
| const postData = { |
| userId, // 记得用户登录状态获取 |
| resourceId: resource?.resourceId |
| }; |
| // 发送POST请求 |
| const response = await axios.post(process.env.PUBLIC_URL + '/resource/purchase', postData); |
| |
| if (response.status === 200) { |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '成功购买资源' }); |
| // 购买成功 |
| setVisible(false); |
| // 刷新购买按钮 |
| isPurchase(); |
| } else if (response.status === 412) { |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '积分不足,购买失败' }); |
| } |
| } catch (error) { |
| console.error('购买资源失败:', error); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '购买资源失败' }); |
| } |
| }; |
| |
| // 处理收藏操作 |
| const handleCollection = async () => { |
| const newCollectionState = !isCollected; |
| const newCollectionCount = newCollectionState ? collectionCount + 1 : collectionCount - 1; |
| |
| // 立即更新本地状态 |
| setIsCollected(newCollectionState); |
| setCollectionCount(newCollectionCount); |
| |
| try { |
| if (newCollectionState) { |
| // 收藏操作 |
| await axios.post(process.env.PUBLIC_URL + `/resource/collection`, { |
| resourceId: resourceId, userId |
| }); |
| console.log('收藏资源'); |
| } else { |
| // 取消收藏操作 |
| await axios.delete(process.env.PUBLIC_URL + `/resource/collection`, { |
| params: { resourceId: resourceId, userId } |
| }); |
| console.log('取消收藏资源'); |
| } |
| } catch (err) { |
| console.error(newCollectionState ? '收藏资源失败' : '取消收藏失败', err); |
| toast.current?.show({ |
| severity: 'error', |
| summary: 'error', |
| detail: newCollectionState ? "收藏资源失败" : "取消收藏失败" |
| }); |
| |
| // 如果请求失败,回滚状态 |
| setIsCollected(!newCollectionState); |
| setCollectionCount(newCollectionState ? collectionCount - 1 : collectionCount + 1); |
| } |
| }; |
| |
| // 处理点赞行为 |
| const handleLike = async () => { |
| const newLikeState = !isLiked; |
| const newLikeCount = newLikeState ? likeCount + 1 : likeCount - 1; |
| |
| // 立即更新本地状态 |
| setIsLiked(newLikeState); |
| setLikeCount(newLikeCount); |
| |
| try { |
| if (newLikeState) { |
| // 点赞操作 |
| await axios.post(process.env.PUBLIC_URL + `/resource/like`, { |
| resourceId: resourceId, userId |
| }); |
| console.log('点赞资源'); |
| } else { |
| // 取消点赞操作 |
| await axios.delete(process.env.PUBLIC_URL + `/resource/like`, { |
| params: { resourceId: resourceId, userId } |
| }); |
| console.log('取消点赞资源'); |
| } |
| } catch (err) { |
| console.error(newLikeState ? '点赞资源失败' : '取消点赞失败', err); |
| toast.current?.show({ |
| severity: 'error', |
| summary: 'error', |
| detail: newLikeState ? "点赞资源失败" : "取消点赞失败" |
| }); |
| |
| // 如果请求失败,回滚状态 |
| setIsLiked(!newLikeState); |
| setLikeCount(newLikeState ? newLikeCount - 1 : newLikeCount + 1); |
| } |
| }; |
| |
| // 格式化数字显示 (3000 -> 3k) |
| const formatCount = (count?: number): string => { |
| if (count == null) return "0"; // 同时处理 undefined/null |
| |
| const absCount = Math.abs(count); // 处理负数 |
| |
| const format = (num: number, suffix: string) => { |
| const fixed = num.toFixed(1); |
| return fixed.endsWith('.0') |
| ? `${Math.floor(num)}${suffix}` |
| : `${fixed}${suffix}`; |
| }; |
| |
| if (absCount >= 1e6) return format(count / 1e6, "m"); |
| if (absCount >= 1e3) return format(count / 1e3, "k"); |
| return count.toString(); |
| }; |
| |
| // 获取发帖人 |
| useEffect(() => { |
| if (!resource) return; |
| // 发帖人 |
| axios.get(process.env.PUBLIC_URL + `/user/info?userId=${resource?.uploaderId}`) |
| .then(res => setUserInfo(res.data)) |
| .catch(console.error); |
| }, [resource]); |
| |
| // 当 resourceId 或分页参数变化时重新拉评论 |
| useEffect(() => { |
| if (!resourceId) return; |
| |
| fetchComments(); |
| }, [resourceId, first, rows]); |
| |
| |
| //通过评论ID获取评论人信息 |
| const getReplyUserName = (replyId: number) => { |
| if (replyId == null || replyId == 0) return ''; |
| const replyComment = comments.find(comment => comment.commentId === replyId); |
| if (!replyComment?.userId) return '匿名用户'; |
| return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户'; |
| }; |
| |
| // 获取该资源的评论 |
| const fetchComments = async () => { |
| try { |
| const pageNumber = first / rows + 1; |
| console.log("当前页" + pageNumber + "size" + rows); |
| const response = await axios.get<CommentList>( |
| process.env.PUBLIC_URL + `/comment`, { |
| params: { id: resourceId, pageNumber, rows, type: 'resource' } |
| } |
| ); |
| console.log('获取评论列表:', response.data.records); |
| setComments(response.data.records); |
| setTotalComments(response.data.total); |
| // 拉取评论对应用户信息 |
| response.data.records.forEach(comment => { |
| if (comment.userId != null && !commentUserInfos.has(comment.userId)) { |
| axios.get<UserInfo>( |
| process.env.PUBLIC_URL + `/user/info`, |
| { params: { userId: comment.userId } } |
| ).then(res => { |
| setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data)); |
| }); |
| } |
| }); |
| } catch (err) { |
| console.error('获取评论失败', err); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' }); |
| } |
| }; |
| |
| // 回复评论接口 |
| const publishReply = async (commentId: number) => { |
| if (!replyValue.trim() || !resource) return; |
| console.log('发布评论:', commentId); |
| // console.log(typeof resourceId); |
| try { |
| const newComment: NewComment = { |
| userId, |
| rewardId: null, |
| threadId: null, |
| resourceId: resource.resourceId, |
| replyId: commentId, |
| content: commentValue, |
| createAt: new Date().toISOString().slice(0, 10).replace('T', ' ') |
| }; |
| |
| const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment); |
| |
| if (response.status === 200) { |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' }); |
| // 更新评论列表 |
| fetchComments(); |
| setVisibleReply(false) |
| // 清空输入框 |
| setReplyValue(''); |
| } |
| } catch (error) { |
| console.error('发布评论失败:', error); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' }); |
| } |
| }; |
| |
| const buyResource = async () => { |
| console.log("Buy Resource") |
| try { |
| const response = await axios.post(process.env.PUBLIC_URL + '/resource/purchase', { |
| userId, |
| resourceId |
| }); |
| |
| if (response.status === 200) { |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '购买成功' }); |
| } else { |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '购买失败' }); |
| |
| } |
| } catch (error) { |
| console.error('购买失败:', error); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '购买失败' }); |
| } |
| } |
| |
| // 发布评论接口 |
| const publishComment = async () => { |
| if (!commentValue.trim() || !resource) return; |
| |
| try { |
| const newComment: NewComment = { |
| userId, |
| rewardId: null, |
| threadId: null, |
| resourceId: resource.resourceId, |
| replyId: null, // 直接评论,不是回复 |
| content: commentValue, |
| createAt: new Date().toISOString().slice(0, 10).replace('T', ' ') |
| }; |
| |
| const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment); |
| |
| if (response.status === 200) { |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' }); |
| // 更新评论列表 |
| fetchComments(); |
| // 清空输入框 |
| setCommentValue(''); |
| } |
| } catch (error) { |
| console.error('发布评论失败:', error); |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' }); |
| } |
| }; |
| |
| // 删除评论接口 |
| const deleteComment = async (commentId: number) => { |
| if (!resourceId) return; |
| |
| try { |
| // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId |
| const response = await axios.delete( |
| process.env.PUBLIC_URL + `/comment?commentId=${commentId}` |
| ); |
| |
| if (response.status === 200) { |
| fetchComments(); |
| toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' }); |
| } else { |
| toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' }); |
| console.error('删除评论失败,状态码:', response.status); |
| } |
| } catch (error) { |
| console.error('删除评论接口报错:', error); |
| } |
| }; |
| |
| const ReplyHeader = ( |
| <div className="flex align-items-center gap-1"> |
| <h3>回复评论</h3> |
| </div> |
| ); |
| if (!resourceId || !userInfo) return <div>Loading...</div>; |
| |
| return ( |
| <div className="resource-detail-container"> |
| <Toast ref={toast}></Toast> |
| {/*资源标题*/} |
| <div className="resource-header"> |
| {resource?.resourceName} |
| </div> |
| {/*资源详细信息*/} |
| <div className="resource-info"> |
| <Image |
| src={(process.env.NEXT_PUBLIC_NGINX_URL! + resource?.resourcePicture)} |
| alt={resource?.resourceName} |
| width="540px" |
| height="300px" |
| /> |
| <div className="resource-info-detail"> |
| {/*资源热度*/} |
| <div className="resource-hot"> |
| <Fire theme="outline" size="50" fill="#f5a623" strokeWidth={3} /> |
| <span className="resource-hot-data">{resource?.hot}</span> |
| </div> |
| |
| {/*资源标签*/} |
| <div className="resource-label"> |
| {resource?.gameplayList.map((tag, index) => ( |
| <Button |
| key={index} label={tag} |
| className="resource-label-button" |
| onClick={() => { |
| router.push(`/resource/classification/`) |
| }} |
| /> |
| ))} |
| </div> |
| |
| {/*资源浏览量和下载量*/} |
| <div className="resource-data"> |
| <div className="resource-data-container"> |
| <i className="pi pi-download" /> |
| <span className="resource-data-container-number">下载量:{formatCount(resource?.downloads)}</span> |
| </div> |
| </div> |
| |
| {/*资源发布时间和更新时间*/} |
| <div className="resource-time"> |
| <div className="resource-time-data"> |
| 发布时间:{resource?.uploadTime} |
| </div> |
| <div className="resource-time-data"> |
| 更新时间:{resource?.lastUpdateTime} |
| </div> |
| </div> |
| </div> |
| </div> |
| {/*资源总结*/} |
| <div className="resource-summary"> |
| {resource?.resourceSummary} |
| </div> |
| {/*关注作者、点赞、抽藏、购买资源*/} |
| <div className="resource-operation"> |
| <div className="resource-author"> |
| <Avatar |
| image={`${process.env.NEXT_PUBLIC_NGINX_URL}/users/${resourceAuthor?.avatar}`} |
| shape="circle" |
| style={{ width: "60px", height: "60px" }} |
| /> |
| <span className="resource-author-name">{resourceAuthor?.username}</span> |
| |
| {handleSubscribe()} |
| </div> |
| |
| <div className="resource-operation-detail"> |
| <div className="resource-operation-detail-data"> |
| <i |
| className={isCollected ? "pi pi-star-fill" : "pi pi-star"} |
| onClick={handleCollection} |
| style={{ |
| cursor: 'pointer', |
| fontSize: '30px', |
| color: isCollected ? 'rgba(82, 102, 101, 1)' : 'inherit', |
| transition: 'color 0.3s ease' |
| }} |
| /> |
| <span>{formatCount(collectionCount)}</span> |
| </div> |
| |
| |
| |
| <div className="resource-operation-detail-data"> |
| {isLiked ? <SmilingFace |
| theme="filled" |
| size="30" |
| fill="#526665" |
| strokeWidth={5} |
| onClick={handleLike} |
| style={{ cursor: 'pointer' }} |
| /> |
| : <SmilingFace |
| theme="outline" |
| size="30" |
| fill="#526665" |
| strokeWidth={5} |
| onClick={handleLike} |
| style={{ cursor: 'pointer' }} |
| /> |
| } |
| <span>{formatCount(likeCount)}</span> |
| </div> |
| |
| <ButtonGroup > |
| <Button label={"$" + resource?.price} style={{ |
| height: "44px", background: "rgba(82, 102, 101, 1)", |
| borderStyle: "solid", borderWidth: "1px", borderColor: "rgba(82, 102, 101, 1)", |
| borderRadius: "0 20px 20px 0", fontSize: "26px", |
| }} onClick={buyResource} /> |
| </ButtonGroup> |
| </div> |
| </div> |
| {/*资源详情*/} |
| <div className="resource-detail"> |
| <h1 className="resource-detail-title">资源详情</h1> |
| <div className="resource-detail-text"> |
| {resource?.resourceDetail} |
| </div> |
| </div> |
| {/* 评论列表 */} |
| <div className="comments-section"> |
| <div className="comments-header"> |
| <h2>评论 ({totalComments})</h2> |
| <Link href="/community" className="no-underline">进入社区</Link> |
| </div> |
| <div className="comments-input"> |
| <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" /> |
| <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} /> |
| <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} /> |
| </div> |
| <div className="comments-list"> |
| {comments.map((comment, index) => ( |
| <div key={comment.commentId} className="comment-item"> |
| <div className="comment-user"> |
| <Avatar |
| image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'} |
| size="normal" |
| shape="circle" |
| /> |
| <div className="comment-meta"> |
| <span className="username"> |
| {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'} |
| </span> |
| <div className="comment-time"> |
| <span className="floor">#{first + index + 1}楼</span> |
| <span className="time">{comment.createAt}</span> |
| </div> |
| </div> |
| <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} /> |
| </div> |
| <div className="comment-content"> |
| {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>} |
| <p>{comment.content}</p> |
| </div> |
| <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置 |
| ref={el => { |
| if (el) ops.current[index] = el; |
| }}> |
| <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} /> |
| {comment.userId === userId && |
| <Button |
| label="删除" |
| text |
| size="small" |
| onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }} |
| /> |
| } |
| </OverlayPanel> |
| <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}> |
| <div className="reply-input"> |
| <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" /> |
| <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} /> |
| <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} /> |
| </div> |
| </Sidebar> |
| </div> |
| ))} |
| {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)} |
| </div> |
| </div> |
| |
| {/*用户购买资源弹窗*/} |
| <Dialog |
| header="购买资源" |
| visible={visible} |
| onHide={() => setVisible(false)} |
| className="purchase-dialog" |
| modal |
| footer={ |
| <div className="dialog-footer"> |
| <Button label="购买" icon="pi pi-check" onClick={handlePurchase} autoFocus /> |
| <Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" /> |
| </div> |
| } |
| > |
| <div className="form-text"> |
| 购买该资源需要{resource?.price}积分,是否购买? |
| </div> |
| </Dialog> |
| </div> |
| ) |
| } |
| |