Merge "add resource recommend" into main
diff --git a/Dockerfile b/Dockerfile
index af2bef1..e901b14 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,10 +30,12 @@
# 设置环境变量
ENV NODE_ENV=production
-ENV PORT=3000
+ENV PORT=3009
+ENV NEXT_PUBLIC_NGINX_URL=http://team9.10813352.xyz:5009/upload
+ENV PUBLIC_URL=http://team9.10813352.xyz:5009/api
# 暴露端口
-EXPOSE 3000
+EXPOSE 3009
# 启动命令
CMD ["node", "server.js"]
\ No newline at end of file
diff --git "a/src/app/community/community-detail/\133communityId\135/page.tsx" "b/src/app/community/community-detail/\133communityId\135/page.tsx"
index e366843..33011eb 100644
--- "a/src/app/community/community-detail/\133communityId\135/page.tsx"
+++ "b/src/app/community/community-detail/\133communityId\135/page.tsx"
@@ -142,12 +142,13 @@
const handleSubmit = async () => {
try {
const currentDate = new Date().toISOString();
+ console.log(formData);
const postData = {
userId, // 记得用户登录状态获取
threadPicture: formData.threadPicture,
title: formData.title,
content: formData.content,
- createdAt: currentDate,
+ createAt: currentDate.slice(0, 10),
communityId: communityId // 从URL参数获取的社区ID
};
// 发送POST请求
@@ -213,7 +214,7 @@
{threads.map((thread) => (
<Card key={thread.threadId} className="resource-card" onClick={() => router.push(`/community/thread-detail/${thread.threadId}`)}>
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + thread.threadPicture}
+ src={ thread.threadPicture}
alt={thread.title}
width="368"
height="200"
@@ -285,8 +286,9 @@
try {
const res = await axios.post(`${process.env.PUBLIC_URL}/file`, formData);
- const fileUrl = res.data.url;
+ const fileUrl = res.data;
console.log(fileUrl);
+ setFormData(prev => ({ ...prev, threadPicture: fileUrl }))
toast.current?.show({ severity: 'success', summary: '上传成功' });
} catch (error) {
console.log(error);
diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx
index 81c8a6b..b24242a 100644
--- a/src/app/community/page.tsx
+++ b/src/app/community/page.tsx
@@ -106,7 +106,7 @@
width="24"
height="24"
/></div>
- <Image src={process.env.NEXT_PUBLIC_NGINX_URL + item.communityPicture} alt={item.communityName} height="200" className="w-full h-48 object-cover" />
+ <Image src={item.communityPicture} alt={item.communityName} height="200" className="w-full h-48 object-cover" />
</div>
}
>
@@ -165,7 +165,7 @@
<div className="all-communities-list">
{communities.map((community) => (
<Card key={community.communityId} className="all-communities-card" onClick={() => router.push(`/community/community-detail/${community.communityId}`)}>
- <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + community.communityPicture} className="community-avatar" width="250" height="140" />
+ <Image alt="avatar" src={ community.communityPicture} className="community-avatar" width="250" height="140" />
<div className="community-header">
<div className="community-content">
<h3>{community.communityName}</h3>
diff --git "a/src/app/community/resource-community-list/\133category\135/page.tsx" "b/src/app/community/resource-community-list/\133category\135/page.tsx"
index 16f6bd6..cb3e3b0 100644
--- "a/src/app/community/resource-community-list/\133category\135/page.tsx"
+++ "b/src/app/community/resource-community-list/\133category\135/page.tsx"
@@ -111,7 +111,7 @@
<div className="resource-communities-list">
{communities.map((community) => (
<Card key={community.communityId} className="resource-communities-list-card" onClick={() => router.push(`/community/community-detail/${community.communityId}`)}>
- <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + community.communityPicture} className="community-avatar" width="250" height="140" />
+ <Image alt="avatar" src={ community.communityPicture} className="community-avatar" width="250" height="140" />
<div className="community-header">
<div className="community-content">
<h3>{community.communityName}</h3>
diff --git "a/src/app/community/thread-detail/\133threadId\135/page.tsx" "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
index 0f3e5b2..097c35a 100644
--- "a/src/app/community/thread-detail/\133threadId\135/page.tsx"
+++ "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
@@ -59,9 +59,9 @@
// 新评论接口
interface NewComment {
userId: number;
- threadId: number;
- resourceId: number;
- replyId: number;
+ threadId: number | null;
+ resourceId: number | null;
+ replyId: number | null;
content: string;
createdAt: string;
}
@@ -139,7 +139,7 @@
try {
const response = await axios.post(
process.env.PUBLIC_URL + `/thread/like`, {
- params: { threadId, userId }
+ threadId, userId
}
);
fetchThreadInfo(); // 刷新帖子信息
@@ -194,7 +194,7 @@
const pageNumber = first / rows + 1;
console.log("当前页" + pageNumber + "size" + rows);
const response = await axios.get<CommentList>(
- process.env.PUBLIC_URL + `/comments`, {
+ process.env.PUBLIC_URL + `/comment`, {
params: { threadId, pageNumber, rows }
}
);
@@ -225,10 +225,10 @@
const newComment: NewComment = {
userId,
threadId: threadInfo.threadId,
- resourceId: 0,
+ resourceId: null,
replyId: commentId,
content: commentValue,
- createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ createdAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
};
const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
@@ -255,10 +255,10 @@
const newComment: NewComment = {
userId,
threadId: threadInfo.threadId,
- resourceId: 0,
- replyId: 0, // 直接评论,不是回复
+ resourceId: null,
+ replyId: null, // 直接评论,不是回复
content: commentValue,
- createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ createdAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
};
const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
@@ -310,7 +310,7 @@
{/* 帖子头部 */}
<div className="thread-header">
<div className="user-info">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" />
<div className="user-meta">
<h3>{userInfo.username}</h3>
<span>{userInfo.signature}</span>
@@ -324,7 +324,7 @@
<h1>{threadInfo.title}</h1>
<div className="content-body">
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + threadInfo.threadPicture}
+ src={ threadInfo.threadPicture}
alt={threadInfo.title}
width="800"
height="400"
@@ -347,7 +347,7 @@
<h2>评论 ({totalComments})</h2>
</div>
<div className="comments-input">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <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>
@@ -356,7 +356,7 @@
<div key={comment.commentId} className="comment-item">
<div className="comment-user">
<Avatar
- image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+ image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
size="normal"
shape="circle"
/>
@@ -391,7 +391,7 @@
</OverlayPanel>
<Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
<div className="reply-input">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <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>
diff --git a/src/app/hook/useLocalStorage.ts b/src/app/hook/useLocalStorage.ts
index 2b28b01..7628f37 100644
--- a/src/app/hook/useLocalStorage.ts
+++ b/src/app/hook/useLocalStorage.ts
@@ -1,19 +1,51 @@
import { useEffect, useState } from 'react';
-export const useLocalStorage = <T>(key: string): T | null => {
- const [value, setValue] = useState<T | null>(null);
+// export const useLocalStorage = <T>(key: string): T | null => {
+// const [value, setValue] = useState<T | null>(null);
- useEffect(() => {
- if (typeof window !== 'undefined') {
+// useEffect(() => {
+// if (typeof window !== 'undefined') {
+// const item = localStorage.getItem(key);
+// if (item) {
+// try {
+// setValue(JSON.parse(item));
+// console.log(JSON.parse(item));
+// } catch (e) {
+// console.error(`解析 localStorage ${key} 失败`, e);
+// }
+// }
+// }
+// }, [key]);
+
+// return value;
+// };
+
+export const useLocalStorage = <T>(key: string): T | null => {
+ const isClient = typeof window !== 'undefined';
+ const [value, setValue] = useState<T | null>(() => {
+ if (isClient) {
const item = localStorage.getItem(key);
if (item) {
try {
- setValue(JSON.parse(item));
+ return JSON.parse(item);
} catch (e) {
console.error(`解析 localStorage ${key} 失败`, e);
}
}
}
+ return null;
+ });
+
+ useEffect(() => {
+ if (!isClient) return;
+ const item = localStorage.getItem(key);
+ if (item) {
+ try {
+ setValue(JSON.parse(item));
+ } catch (e) {
+ console.error(`解析 localStorage ${key} 失败`, e);
+ }
+ }
}, [key]);
return value;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 5076a0a..f3555da 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -233,7 +233,7 @@
className="custom-carousel"
itemTemplate={(hotResource) => (
<div className="carousel-item" onClick={() => router.push(`/resource/resource-detail/${hotResource.resourceId}`)}>
- <Image alt="slide" src={process.env.NEXT_PUBLIC_NGINX_URL + hotResource.resourcePicture} className="carousel-avatar" width="700" height="350" />
+ <Image alt="slide" src={hotResource.resourcePicture} className="carousel-avatar" width="700" height="350" />
</div>
)}
/>
@@ -288,7 +288,7 @@
{mods.map((mod) => (
<Card key={mod.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${mod.resourceId}`)}>
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + mod.resourcePicture}
+ src={mod.resourcePicture}
alt={mod.resourceName}
width="368"
height="200"
@@ -318,7 +318,7 @@
{maps.map((map) => (
<Card key={map.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${map.resourceId}`)}>
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + map.resourcePicture}
+ src={map.resourcePicture}
alt={map.resourceName}
width="368"
height="200"
@@ -348,7 +348,7 @@
{textures.map((texture) => (
<Card key={texture.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${texture.resourceId}`)}>
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + texture.resourcePicture}
+ src={texture.resourcePicture}
alt={texture.resourceName}
width="368"
height="200"
@@ -378,7 +378,7 @@
{modpacks.map((modpack) => (
<Card key={modpack.resourceId} className="resource-card" onClick={() => router.push(`/resource/${modpack.resourceId}`)}>
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + modpack.resourcePicture}
+ src={modpack.resourcePicture}
alt={modpack.resourceName}
width="368"
height="200"
diff --git a/src/app/resource/classification/page.tsx b/src/app/resource/classification/page.tsx
index f147ed2..6b41969 100644
--- a/src/app/resource/classification/page.tsx
+++ b/src/app/resource/classification/page.tsx
@@ -61,6 +61,7 @@
const [selectedGameplay, setSelectedGameplay] = useState<string[]>([]);
const [selectedVersions, setSelectedVersions] = useState<string[]>([]);
const [searchValue, setSearchValue] = useState<string>('');
+
const debouncedSearch = useRef(
debounce((value: string) => {
setSearchValue(value);
@@ -88,8 +89,8 @@
pageNumber,
rows,
classify: selectedClassify,
- gameplayList: selectedGameplay.join(','),
- versionList: selectedVersions.join(','),
+ gameplayList: selectedGameplay.join(',') ,
+ gameVersionList: selectedVersions.join(','),
searchValue
}
});
@@ -115,7 +116,7 @@
<div className="all-resources-list">
{hotResources.map((hotResource) => (
<Card key={hotResource.resourceId} className="all-resources-card" onClick={() => router.push(`/resource/resource-detail/${hotResource.resourceId}`)}>
- <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + "hotResource/" + hotResource.resourcePicture} className="resource-avatar" width="250" height="140" />
+ <Image alt="avatar" src={ "hotResource/" + hotResource.resourcePicture} className="resource-avatar" width="250" height="140" />
<div className="resource-header">
<div className="resource-content">
<h3>{hotResource.resourceName}</h3>
diff --git a/src/app/resource/hot-resource/page.tsx b/src/app/resource/hot-resource/page.tsx
index 4990638..57f1b26 100644
--- a/src/app/resource/hot-resource/page.tsx
+++ b/src/app/resource/hot-resource/page.tsx
@@ -216,7 +216,7 @@
className="custom-carousel"
itemTemplate={(hotResource) => (
<div className="carousel-item" onClick={() => router.push(`/resource/resource-detail/${hotResource.resourceId}`)}>
- <Image alt="slide" src={process.env.NEXT_PUBLIC_NGINX_URL + hotResource.resourcePicture} className="carousel-avatar" width="480" height="350" />
+ <Image alt="slide" src={ hotResource.resourcePicture} className="carousel-avatar" width="480" height="350" />
<h3>{hotResource.resourceName}</h3>
</div>
)}
@@ -238,7 +238,7 @@
<div className="all-resources-list">
{hotResources.map((hotResource) => (
<Card key={hotResource.resourceId} className="all-resources-card" onClick={() => router.push(`/resource/resource-detail/${hotResource.resourceId}`)}>
- <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + "hotResource/" + hotResource.resourcePicture} className="resource-avatar" width="250" height="140" />
+ <Image alt="avatar" src={ "hotResource/" + hotResource.resourcePicture} className="resource-avatar" width="250" height="140" />
<div className="resource-header">
<div className="resource-content">
<h3>{hotResource.resourceName}</h3>
diff --git "a/src/app/resource/resource-detail/\133resourceId\135/page.tsx" "b/src/app/resource/resource-detail/\133resourceId\135/page.tsx"
index 5e0a634..baec9a6 100644
--- "a/src/app/resource/resource-detail/\133resourceId\135/page.tsx"
+++ "b/src/app/resource/resource-detail/\133resourceId\135/page.tsx"
@@ -61,7 +61,6 @@
likes: number; // 点赞数
collections: number; // 收藏数
comments: number; // 评论数
- seeds: number; // 种子数
classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map
hot: number; // 资源热度
gameplayList: string[]; // 资源标签
@@ -70,7 +69,7 @@
isLike: boolean; // 是否被点赞
isPurchase: boolean; // 是否被购买
isUpload: boolean; // 是否是该用户上传的
- userId: number; // 资源上传者的id
+ uploaderId: number; // 资源上传者的id
}
// 评论信息
@@ -99,10 +98,10 @@
// 新评论接口
interface NewComment {
userId: number;
- threadId: number;
- resourceId: number;
- rewardId: number;
- replyId: number;
+ threadId: number | null;
+ resourceId: number | null;
+ rewardId: number | null;
+ replyId: number | null;
content: string;
createAt: string;
}
@@ -131,7 +130,7 @@
const userId: number = user?.Id ?? -1;
// 获取URL参数
const params = useParams<{ resourceId: string }>();
- const resourceId = decodeURIComponent(params.resourceId); // 防止中文路径乱码
+ const resourceId = Number(decodeURIComponent(params.resourceId)); // 防止中文路径乱码
// 页面跳转
const router = useRouter();
@@ -198,7 +197,7 @@
setCollectionCount(response.data.collections);
}
- setResourceAuthorId(response.data.userId);
+ setResourceAuthorId(response.data.uploaderId);
} catch (err) {
console.error('获取资源信息失败', err);
toast.current?.show({ severity: 'error', summary: 'error', detail: "获取资源信息失败" });
@@ -246,9 +245,9 @@
}
useEffect(() => {
- if (!resource?.userId || !subscriberList?.userList) return;
+ if (!resource?.uploaderId || !subscriberList?.userList) return;
- const authorId = resource.userId;
+ const authorId = resource.uploaderId;
// 设置 isSubscribed 状态
const subscribed = subscriberList.userList.some(user => user.userId === authorId);
setIsSubscribed(subscribed);
@@ -257,7 +256,7 @@
// 若浏览用户与资源作者是同一人,则不显示关注按钮。若不是同一人,则显示按钮
const handleSubscribe = () => {
// 资源作者 ID
- const authorId = resource?.userId;
+ const authorId = resource?.uploaderId;
// 当前登录用户 ID
const currentUserId = userId;
@@ -340,7 +339,7 @@
// 判断该资源是否已被购买, 返回不同的购买按钮
const isPurchase = () => {
// 作者本人查看资源,不显示购买按钮
- if (resource?.userId == userId) {
+ if (resource?.uploaderId == userId) {
return;
}
@@ -405,7 +404,7 @@
if (newCollectionState) {
// 收藏操作
await axios.post(process.env.PUBLIC_URL + `/resource/collection`, {
- params: { resourceId: resourceId, userId }
+ resourceId: resourceId, userId
});
console.log('收藏资源');
} else {
@@ -442,7 +441,7 @@
if (newLikeState) {
// 点赞操作
await axios.post(process.env.PUBLIC_URL + `/resource/like`, {
- params: { resourceId: resourceId, userId }
+ resourceId: resourceId, userId
});
console.log('点赞资源');
} else {
@@ -488,7 +487,7 @@
useEffect(() => {
if (!resource) return;
// 发帖人
- axios.get(process.env.PUBLIC_URL + `/user/info?userId=${resource?.userId}`)
+ axios.get(process.env.PUBLIC_URL + `/user/info?userId=${resource?.uploaderId}`)
.then(res => setUserInfo(res.data))
.catch(console.error);
}, [resource]);
@@ -515,7 +514,7 @@
const pageNumber = first / rows + 1;
console.log("当前页" + pageNumber + "size" + rows);
const response = await axios.get<CommentList>(
- process.env.PUBLIC_URL + `/comments`, {
+ process.env.PUBLIC_URL + `/comment`, {
params: { id: resourceId, pageNumber, rows, type: 'resource' }
}
);
@@ -547,12 +546,12 @@
try {
const newComment: NewComment = {
userId,
- rewardId: 0,
- threadId: 0,
+ rewardId: null,
+ threadId: null,
resourceId: resource.resourceId,
replyId: commentId,
content: commentValue,
- createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
};
const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
@@ -571,6 +570,26 @@
}
};
+ 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;
@@ -578,12 +597,12 @@
try {
const newComment: NewComment = {
userId,
- rewardId: 0,
- threadId: 0,
+ rewardId: null,
+ threadId: null,
resourceId: resource.resourceId,
- replyId: 0, // 直接评论,不是回复
+ replyId: null, // 直接评论,不是回复
content: commentValue,
- createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
};
const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
@@ -740,12 +759,11 @@
</div>
<ButtonGroup >
- {isPurchase()}
<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",
- }} disabled={true} />
+ }} onClick={buyResource} />
</ButtonGroup>
</div>
</div>
@@ -763,7 +781,7 @@
<Link href="/community" className="no-underline">进入社区</Link>
</div>
<div className="comments-input">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <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>
@@ -772,7 +790,7 @@
<div key={comment.commentId} className="comment-item">
<div className="comment-user">
<Avatar
- image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+ image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
size="normal"
shape="circle"
/>
@@ -807,7 +825,7 @@
</OverlayPanel>
<Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
<div className="reply-input">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <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>
diff --git a/src/app/reward/page.tsx b/src/app/reward/page.tsx
index b53d93c..77d11a6 100644
--- a/src/app/reward/page.tsx
+++ b/src/app/reward/page.tsx
@@ -1,268 +1,392 @@
-'use client';
+"use client";
import React, { useEffect, useState, useRef } from "react";
-import { InputText } from 'primereact/inputtext';
-import { Button } from 'primereact/button';
-import { Card } from 'primereact/card';
-import { Image } from 'primereact/image';
+import { InputText } from "primereact/inputtext";
+import { Button } from "primereact/button";
+import { Card } from "primereact/card";
+import { Image } from "primereact/image";
// 页面跳转
-import { useRouter } from 'next/navigation';
+import { useRouter } from "next/navigation";
// 分页
-import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+import { Paginator, type PaginatorPageChangeEvent } from "primereact/paginator";
// 消息提醒
-import { Toast } from 'primereact/toast';
+import { Toast } from "primereact/toast";
// 发布帖子
-import { Dialog } from 'primereact/dialog';
-import { FileUpload } from 'primereact/fileupload';
-import { InputTextarea } from 'primereact/inputtextarea';
+import { Dialog } from "primereact/dialog";
+import { FileUpload } from "primereact/fileupload";
+import { InputTextarea } from "primereact/inputtextarea";
// 接口传输
-import axios from 'axios';
+import axios from "axios";
// 防抖函数
-import { debounce } from 'lodash';
-import { TabView, TabPanel } from 'primereact/tabview';
-import { useLocalStorage } from '../hook/useLocalStorage';
+import { debounce } from "lodash";
+import { TabView, TabPanel } from "primereact/tabview";
+import { useLocalStorage } from "../hook/useLocalStorage";
// 样式
-import './reward.scss';
+import "./reward.scss";
interface User {
- Id: number;
+ Id: number;
}
// 悬赏列表数据
interface Reward {
- rewardId: number;
- userId: number;
- rewardPicture: string;
- rewardName: string;
- createAt: string;
- rewardDescription: string;
- price: number;
+ rewardId: number;
+ userId: number;
+ rewardPicture: string;
+ rewardName: string;
+ createAt: string;
+ rewardDescription: string;
+ price: number;
}
interface RewardList {
- total: number; // 总记录数
- records: Reward[];
+ total: number; // 总记录数
+ records: Reward[];
}
-
// 社区详情页面
export default function RewardDetailPage() {
- const user = useLocalStorage<User>('user');
- const userId: number = user?.Id ?? -1;
- // 页面跳转
- const router = useRouter();
- // 帖子列表数据
- const [rewards, setRewards] = useState<Reward[]>([]);
- const [totalRewards, setTotalRewards] = useState<number>(0);
- const [activeOption, setActiveOption] = useState<number>(0); // 0表示"赏金最高",1表示"最新发布"
- const options = ["赏金最高", "最新发布"];
- // 搜索框
- const [searchValue, setSearchValue] = useState("");
- const debouncedSearch = useRef(
- debounce((value: string) => {
- setSearchValue(value);
- }, 600)
- ).current;
- // 消息提醒
- const toast = useRef<Toast>(null);
- // 分页
- const [first, setFirst] = useState(0);
- const [rows, setRows] = useState(5);
- const onPageChange = (event: PaginatorPageChangeEvent) => {
- setFirst(event.first);
- setRows(event.rows);
- };
+ const user = useLocalStorage<User>("user");
+ const userId: number = user?.Id ?? -1;
+ // 页面跳转
+ const router = useRouter();
+ // 帖子列表数据
+ const [rewards, setRewards] = useState<Reward[]>([]);
+ const [totalRewards, setTotalRewards] = useState<number>(0);
+ const [activeOption, setActiveOption] = useState<number>(0); // 0表示"赏金最高",1表示"最新发布"
+ const options = ["赏金最高", "最新发布"];
+ // 搜索框
+ const [searchValue, setSearchValue] = useState("");
+ const debouncedSearch = useRef(
+ debounce((value: string) => {
+ setSearchValue(value);
+ }, 600)
+ ).current;
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 分页
+ const [first, setFirst] = useState(0);
+ const [rows, setRows] = useState(5);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
- // 获取悬赏列表
- useEffect(() => {
+ // 获取悬赏列表
+ useEffect(() => {
+ fetchRewards();
+ }, [first, rows, searchValue, activeOption]);
+
+ const fetchRewards = async () => {
+ try {
+ const pageNumber = first / rows + 1;
+ console.log(
+ "当前页" +
+ pageNumber +
+ "size" +
+ rows +
+ "搜索内容" +
+ searchValue +
+ "排序方式" +
+ activeOption
+ );
+ const response = await axios.get<RewardList>(
+ process.env.PUBLIC_URL + `/reward`,
+ {
+ params: {
+ pageNumber,
+ rows,
+ searchValue,
+ option: options[activeOption],
+ },
+ }
+ );
+ console.log("获取悬赏列表:", response.data.records);
+ setRewards(response.data.records);
+ setTotalRewards(response.data.total); // 假设返回的总数
+ } catch (err) {
+ console.error("获取悬赏失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取悬赏失败",
+ });
+ }
+ };
+
+ // 发布悬赏弹窗
+ const [visible, setVisible] = useState(false);
+ const [formData, setFormData] = useState({
+ rewardName: "",
+ rewardDescription: "",
+ rewardPicture: "",
+ price: "",
+ });
+
+ // 发布悬赏接口
+ const handleSubmit = async () => {
+ try {
+ const currentDate = new Date().toISOString();
+ const postData = {
+ userId, // 记得用户登录状态获取
+ rewardPicture: formData.rewardPicture,
+ rewardName: formData.rewardName,
+ rewardDescription: formData.rewardDescription,
+ createAt: currentDate.slice(0, 10),
+ lastUpdateAt: currentDate.slice(0, 10),
+ price: Number(formData.price),
+ };
+ // 发送POST请求
+ const response = await axios.post(
+ process.env.PUBLIC_URL + "/reward",
+ postData
+ );
+
+ if (response.status === 200) {
+ toast.current?.show({
+ severity: "success",
+ summary: "Success",
+ detail: "悬赏发布成功",
+ });
+ // 发帖成功
+ setVisible(false);
+ // 重置表单
+ setFormData({
+ rewardName: "",
+ rewardDescription: "",
+ rewardPicture: "",
+ price: "",
+ });
+ // 可以刷新帖子列表
fetchRewards();
- }, [first, rows, searchValue, activeOption]);
-
- const fetchRewards = async () => {
- try {
- const pageNumber = first / rows + 1;
- console.log("当前页" + pageNumber + "size" + rows + "搜索内容" + searchValue + "排序方式" + activeOption);
- const response = await axios.get<RewardList>(
- process.env.PUBLIC_URL + `/reward`, {
- params: { pageNumber, rows, searchValue, option: options[activeOption] }
- }
- );
- console.log('获取悬赏列表:', response.data.records);
- setRewards(response.data.records);
- setTotalRewards(response.data.total); // 假设返回的总数
- } catch (err) {
- console.error('获取悬赏失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取悬赏失败' });
- }
- };
-
- // 发布悬赏弹窗
- const [visible, setVisible] = useState(false);
- const [formData, setFormData] = useState({
- rewardName: '',
- rewardDescription: '',
- rewardPicture: '',
- price: ''
- });
- // 图片上传消息通知
- const onUpload = () => {
- toast.current?.show({ severity: 'info', summary: 'Success', detail: 'File Uploaded' });
- };
-
- // 发布悬赏接口
- const handleSubmit = async () => {
- try {
- const currentDate = new Date().toISOString();
- const postData = {
- userId, // 记得用户登录状态获取
- rewardPicture: formData.rewardPicture,
- rewardName: formData.rewardName,
- rewardDescription: formData.rewardDescription,
- createdAt: currentDate,
- price: formData.price
- };
- // 发送POST请求
- const response = await axios.post(process.env.PUBLIC_URL + '/reward', postData);
-
- if (response.status === 200) {
- toast.current?.show({ severity: 'success', summary: 'Success', detail: '悬赏发布成功' });
- // 发帖成功
- setVisible(false);
- // 重置表单
- setFormData({
- rewardName: '',
- rewardDescription: '',
- rewardPicture: '',
- price: ''
- });
- // 可以刷新帖子列表
- fetchRewards();
- }
- } catch (error) {
- console.error('发布悬赏失败:', error);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '悬赏发布失败' });
- }
- };
- return (
- <div className="reward">
- <Toast ref={toast}></Toast>
- {/* 悬赏标题和介绍 */}
- <div className="reward-header">
- <div className="title-section">
- <h1>悬赏排行</h1>
- </div>
- <div className="input">
- <div className="searchBar">
- <i className="pi pi-search" />
- <InputText type="search" className="search-helper" placeholder="搜索你的目标悬赏" onChange={(e) => { const target = e.target as HTMLInputElement; debouncedSearch(target.value); }} />
- </div>
- <div className="reward-buttons">
- <Button label="我的悬赏" onClick={() => router.push(`/user/悬赏`)} />
- <Button label="发布悬赏" onClick={() => setVisible(true)} />
- </div>
- </div>
- </div>
-
- {/* 悬赏列表 */}
- <TabView activeIndex={activeOption} onTabChange={(e) => setActiveOption(e.index)}>
- <TabPanel header={options[0]} >
- <div className="rewards-list">
- {rewards.map((reward) => (
- <Card key={reward.rewardId} className="rewards-list-card" onClick={() => router.push(`/reward/reward-detail/${reward.rewardId}`)}>
- <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + reward.rewardPicture} className="reward-avatar" width="250" height="140" />
- <div className="reward-header">
- <div className="reward-content">
- <h3>{reward.rewardName}</h3>
- </div>
- <div className="reward-states">
- <span className="price">$: {reward.price}</span>
- <Button label="提交悬赏" />
- </div>
- </div>
- </Card>
- ))}
- {totalRewards > 5 && <Paginator className="Paginator" first={first} rows={rows} totalRecords={totalRewards} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />}
- </div>
- </TabPanel>
- <TabPanel header={options[1]}>
- <div className="rewards-list">
- {rewards.map((reward) => (
- <Card key={reward.rewardId} className="rewards-list-card" onClick={() => router.push(`/reward/reward-detail/${reward.rewardId}`)}>
- <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + reward.rewardPicture} className="reward-avatar" width="250" height="140" />
- <div className="reward-header">
- <div className="reward-content">
- <h3>{reward.rewardName}</h3>
- </div>
- <div className="reward-states">
- <span className="price">$: {reward.price}</span>
- <Button label="提交悬赏" />
- </div>
- </div>
- </Card>
- ))}
- {totalRewards > 5 && <Paginator className="Paginator" first={first} rows={rows} totalRecords={totalRewards} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />}
- </div>
- </TabPanel>
- </TabView>
- {/* 发布悬赏弹窗 */}
- <Dialog
- header="发布新悬赏"
- visible={visible}
- onHide={() => setVisible(false)}
- className="publish-dialog"
- modal
- footer={
- <div className="dialog-footer">
- <Button label="发布" icon="pi pi-check" onClick={handleSubmit} autoFocus />
- <Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" />
- </div>
- }
- >
- <div className="publish-form">
- <div className="form-field">
- <label htmlFor="title">标题</label>
- <InputText
- id="title"
- value={formData.rewardName}
- onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
- placeholder="请输入悬赏标题"
- className="w-full"
- />
- </div>
-
- <div className="form-field">
- <label htmlFor="content">内容</label>
- <InputTextarea
- id="content"
- value={formData.rewardDescription}
- onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
- rows={5}
- placeholder="请输入悬赏需求"
- className="w-full"
- />
- </div>
- <div className="form-field">
- <label htmlFor="price">赏金</label>
- <InputText
- id="price"
- value={formData.price}
- onChange={(e) => setFormData(prev => ({ ...prev, price: e.target.value }))}
- placeholder="请输入赏金金额"
- className="w-full"
- />
- </div>
- <div className="form-field">
- <label>封面图片</label>
- <FileUpload
- mode="basic"
- name="thread-image"
- url={process.env.PUBLIC_URL + "/file"} // 与后端交互的URL
- accept="image/*"
- maxFileSize={10000000000}
- chooseLabel="选择悬赏封面"
- className="w-full"
- onUpload={onUpload}
- />
- </div>
- </div>
- </Dialog>
+ }
+ } catch (error) {
+ console.error("发布悬赏失败:", error);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "悬赏发布失败",
+ });
+ }
+ };
+ return (
+ <div className="reward">
+ <Toast ref={toast}></Toast>
+ {/* 悬赏标题和介绍 */}
+ <div className="reward-header">
+ <div className="title-section">
+ <h1>悬赏排行</h1>
</div>
- );
-}
\ No newline at end of file
+ <div className="input">
+ <div className="searchBar">
+ <i className="pi pi-search" />
+ <InputText
+ type="search"
+ className="search-helper"
+ placeholder="搜索你的目标悬赏"
+ onChange={(e) => {
+ const target = e.target as HTMLInputElement;
+ debouncedSearch(target.value);
+ }}
+ />
+ </div>
+ <div className="reward-buttons">
+ <Button
+ label="我的悬赏"
+ onClick={() => router.push(`/user/悬赏`)}
+ />
+ <Button label="发布悬赏" onClick={() => setVisible(true)} />
+ </div>
+ </div>
+ </div>
+
+ {/* 悬赏列表 */}
+ <TabView
+ activeIndex={activeOption}
+ onTabChange={(e) => setActiveOption(e.index)}
+ >
+ <TabPanel header={options[0]}>
+ <div className="rewards-list">
+ {rewards.map((reward) => (
+ <Card
+ key={reward.rewardId}
+ className="rewards-list-card"
+ onClick={() =>
+ router.push(`/reward/reward-detail/${reward.rewardId}`)
+ }
+ >
+ <Image
+ alt="avatar"
+ src={reward.rewardPicture}
+ className="reward-avatar"
+ width="250"
+ height="140"
+ />
+ <div className="reward-header">
+ <div className="reward-content">
+ <h3>{reward.rewardName}</h3>
+ </div>
+ <div className="reward-states">
+ <span className="price">$: {reward.price}</span>
+ <Button label="提交悬赏" />
+ </div>
+ </div>
+ </Card>
+ ))}
+ {totalRewards > 5 && (
+ <Paginator
+ className="Paginator"
+ first={first}
+ rows={rows}
+ totalRecords={totalRewards}
+ rowsPerPageOptions={[5, 10]}
+ onPageChange={onPageChange}
+ />
+ )}
+ </div>
+ </TabPanel>
+ <TabPanel header={options[1]}>
+ <div className="rewards-list">
+ {rewards.map((reward) => (
+ <Card
+ key={reward.rewardId}
+ className="rewards-list-card"
+ onClick={() =>
+ router.push(`/reward/reward-detail/${reward.rewardId}`)
+ }
+ >
+ <Image
+ alt="avatar"
+ src={reward.rewardPicture}
+ className="reward-avatar"
+ width="250"
+ height="140"
+ />
+ <div className="reward-header">
+ <div className="reward-content">
+ <h3>{reward.rewardName}</h3>
+ </div>
+ <div className="reward-states">
+ <span className="price">$: {reward.price}</span>
+ <Button label="提交悬赏" />
+ </div>
+ </div>
+ </Card>
+ ))}
+ {totalRewards > 5 && (
+ <Paginator
+ className="Paginator"
+ first={first}
+ rows={rows}
+ totalRecords={totalRewards}
+ rowsPerPageOptions={[5, 10]}
+ onPageChange={onPageChange}
+ />
+ )}
+ </div>
+ </TabPanel>
+ </TabView>
+ {/* 发布悬赏弹窗 */}
+ <Dialog
+ header="发布新悬赏"
+ visible={visible}
+ onHide={() => setVisible(false)}
+ className="publish-dialog"
+ modal
+ footer={
+ <div className="dialog-footer">
+ <Button
+ label="发布"
+ icon="pi pi-check"
+ onClick={handleSubmit}
+ autoFocus
+ />
+ <Button
+ label="取消"
+ icon="pi pi-times"
+ onClick={() => setVisible(false)}
+ className="p-button-text"
+ />
+ </div>
+ }
+ >
+ <div className="publish-form">
+ <div className="form-field">
+ <label htmlFor="title">标题</label>
+ <InputText
+ id="title"
+ value={formData.rewardName}
+ onChange={(e) =>
+ setFormData((prev) => ({ ...prev, rewardName: e.target.value }))
+ }
+ placeholder="请输入悬赏标题"
+ className="w-full"
+ />
+ </div>
+
+ <div className="form-field">
+ <label htmlFor="content">内容</label>
+ <InputTextarea
+ id="content"
+ value={formData.rewardDescription}
+ onChange={(e) =>
+ setFormData((prev) => ({
+ ...prev,
+ rewardDescription: e.target.value,
+ }))
+ }
+ rows={5}
+ placeholder="请输入悬赏需求"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <label htmlFor="price">赏金</label>
+ <InputText
+ id="price"
+ value={formData.price}
+ onChange={(e) =>
+ setFormData((prev) => ({ ...prev, price: e.target.value }))
+ }
+ placeholder="请输入赏金金额"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <label>封面图片</label>
+ <FileUpload
+ mode="advanced"
+ name="file"
+ customUpload
+ uploadHandler={async (e) => {
+ const formData = new FormData();
+ formData.append("file", e.files[0]);
+
+ try {
+ const res = await axios.post(
+ `${process.env.PUBLIC_URL}/file`,
+ formData
+ );
+
+ const fileUrl = res.data;
+ console.log(fileUrl);
+ setFormData((prev) => ({ ...prev, rewardPicture: fileUrl }));
+ toast.current?.show({
+ severity: "success",
+ summary: "上传成功",
+ });
+ } catch (error) {
+ console.log(error);
+ toast.current?.show({
+ severity: "error",
+ summary: "上传失败",
+ });
+ }
+ }}
+ auto
+ accept="image/*"
+ chooseLabel="上传帖子封面"
+ />
+ </div>
+ </div>
+ </Dialog>
+ </div>
+ );
+}
diff --git "a/src/app/reward/reward-detail/\133rewardId\135/page.tsx" "b/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
index 0ba1a04..b6c60d4 100644
--- "a/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
+++ "b/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
@@ -61,10 +61,10 @@
// 新评论接口
interface NewComment {
userId: number;
- threadId: number;
- resourceId: number;
- rewardId: number;
- replyId: number;
+ threadId: number | null;
+ resourceId: number | null;
+ rewardId: number | null;
+ replyId: number | null;
content: string;
createAt: string;
}
@@ -150,7 +150,7 @@
const pageNumber = first / rows + 1;
console.log("当前页" + pageNumber + "size" + rows);
const response = await axios.get<CommentList>(
- process.env.PUBLIC_URL + `/comments`, {
+ process.env.PUBLIC_URL + `/comment`, {
params: { id: rewardId, pageNumber, rows, type: 'reward' }
}
);
@@ -182,11 +182,11 @@
const newComment: NewComment = {
userId,
rewardId: rewardInfo.rewardId,
- threadId: 0,
- resourceId: 0,
+ threadId: null,
+ resourceId: null,
replyId: commentId,
content: commentValue,
- createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
};
const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
@@ -213,11 +213,11 @@
const newComment: NewComment = {
userId,
rewardId: rewardInfo.rewardId,
- threadId: 0,
- resourceId: 0,
- replyId: 0, // 直接评论,不是回复
+ threadId: null,
+ resourceId: null,
+ replyId: null, // 直接评论,不是回复
content: commentValue,
- createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ createAt: new Date().toISOString().slice(0, 10).replace('T', ' ')
};
const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
@@ -276,7 +276,7 @@
<div className="reward-content">
<div className="reward-info-container">
<div className="user-info">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <Avatar image={ "users/" + userInfo.avatar} size="large" shape="circle" />
<div className="user-meta">
<h3>{userInfo.username}</h3>
<span>{userInfo.signature}</span>
@@ -293,7 +293,7 @@
{/* 右侧图片+价格+按钮 */}
<div className="reward-media">
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + rewardInfo.rewardPicture}
+ src={ rewardInfo.rewardPicture}
alt={rewardInfo.rewardName}
width="500"
height="400"
@@ -310,7 +310,7 @@
<h2>评论 ({totalComments})</h2>
</div>
<div className="comments-input">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <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>
@@ -319,7 +319,7 @@
<div key={comment.commentId} className="comment-item">
<div className="comment-user">
<Avatar
- image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+ image={comment.userId ? "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
size="normal"
shape="circle"
/>
@@ -354,7 +354,7 @@
</OverlayPanel>
<Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
<div className="reply-input">
- <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <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>
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx
index 5f6642b..65d2465 100644
--- a/src/app/search/page.tsx
+++ b/src/app/search/page.tsx
@@ -81,7 +81,7 @@
<Card key={item.resourceId} className="all-resources-card" onClick={() => router.push(`/resource/resource-detail/${item.resourceId}`)}>
{/* 左侧图片 */}
<Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + item.resourcePicture}
+ src={ item.resourcePicture}
alt={item.resourceName}
width="250" height="140"
preview
diff --git a/src/app/user/component/userAvatar.tsx b/src/app/user/component/userAvatar.tsx
index ba164de..abc1b66 100644
--- a/src/app/user/component/userAvatar.tsx
+++ b/src/app/user/component/userAvatar.tsx
@@ -1,111 +1,159 @@
-'use client';
+"use client";
-import { useRef, useState } from 'react';
-import { Avatar } from 'primereact/avatar';
-import { Button } from 'primereact/button';
+import { useRef, useState } from "react";
+import { Avatar } from "primereact/avatar";
+import { Button } from "primereact/button";
// 弹窗
-import { Dialog } from 'primereact/dialog';
+import { Dialog } from "primereact/dialog";
// 头像下拉框
-import { OverlayPanel } from 'primereact/overlaypanel';
+import { OverlayPanel } from "primereact/overlaypanel";
// 输入框
import { FloatLabel } from "primereact/floatlabel";
-import { InputText } from 'primereact/inputtext';
+import { InputText } from "primereact/inputtext";
// 页面跳转
-import Link from 'next/link';
+import Link from "next/link";
// 文件上传
-import { FileUpload } from 'primereact/fileupload';
+import { FileUpload } from "primereact/fileupload";
// 通知
-import { Toast } from 'primereact/toast';
+import { Toast } from "primereact/toast";
// 接口传输
-import axios from 'axios';
-import { useLocalStorage } from '../../hook/useLocalStorage';
+import axios from "axios";
+import { useLocalStorage } from "../../hook/useLocalStorage";
// 样式
-import './user-avatar.scss';
+import "./user-avatar.scss";
+
interface User {
- Id: number;
+ Id: number;
+ Avatar: string,
}
+
// 用户下拉框
export default function UserAvatar() {
- const user = useLocalStorage<User>('user');
- const userId: number = user?.Id ?? -1;
+ const user = useLocalStorage<User>("user");
+ const userId: number = user?.Id ?? -1;
+ // 功能选项
+ const op = useRef<OverlayPanel>(null);
+ let hoverTimeout: NodeJS.Timeout;
+ // 通知
+ const toast = useRef<Toast>(null);
+ // 控制三个弹窗可见性
+ const [showEditSignature, setShowEditSignature] = useState(false);
+ const [showEditAvatar, setShowEditAvatar] = useState(false);
+ const [showEditPassword, setShowEditPassword] = useState(false);
+ // 头像URL
+ const [avatarUrl, setAvatar] = useState<string>("");
+ // 签名
+ const [signValue, setSignValue] = useState<string>("");
+ // 新密码
+ const [passwardValue, setPasswardValue] = useState<string>("");
+ const [newPasswardValue, setNewPasswardValue] = useState<string>("");
+ // 老密码
+ const [oldPasswardValue, setOldPasswardValue] = useState<string>("");
- // 功能选项
- const op = useRef<OverlayPanel>(null);
- let hoverTimeout: NodeJS.Timeout;
- // 通知
- const toast = useRef<Toast>(null);
- // 控制三个弹窗可见性
- const [showEditSignature, setShowEditSignature] = useState(false);
- const [showEditAvatar, setShowEditAvatar] = useState(false);
- const [showEditPassword, setShowEditPassword] = useState(false);
- // 头像URL
- const [avatarUrl, setAvatar] = useState<string>('');
- // 签名
- const [signValue, setSignValue] = useState<string>('');
- // 新密码
- const [passwardValue, setPasswardValue] = useState<string>('');
- const [newPasswardValue, setNewPasswardValue] = useState<string>('');
- // 老密码
- const [oldPasswardValue, setOldPasswardValue] = useState<string>('');
+ const handleMouseEnter = (event: React.MouseEvent) => {
+ clearTimeout(hoverTimeout);
+ op.current?.show(event, event.currentTarget);
+ };
+ const handleMouseLeave = () => {
+ hoverTimeout = setTimeout(() => {
+ op.current?.hide();
+ }, 300);
+ };
- const handleMouseEnter = (event: React.MouseEvent) => {
- clearTimeout(hoverTimeout);
- op.current?.show(event, event.currentTarget);
- };
-
- const handleMouseLeave = () => {
- hoverTimeout = setTimeout(() => {
- op.current?.hide();
- }, 300);
- };
-
-
- // 修改密码接口
- const editPassward = async () => {
- try {
- await axios.put(process.env.PUBLIC_URL + `/user/password`, {
- params: { userId, password: oldPasswardValue, newPassword: passwardValue }
- });
- toast.current?.show({ severity: 'success', summary: 'success', detail: '修改密码成功' });
- setShowEditPassword(false);
- } catch (err) {
- console.error('修改密码失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '修改密码失败' });
- }
+ // 修改密码接口
+ const editPassward = async () => {
+ try {
+ await axios.put(process.env.PUBLIC_URL + `/user/password`, {
+ body: {
+ userId,
+ password: oldPasswardValue,
+ newPassword: passwardValue,
+ },
+ });
+ toast.current?.show({
+ severity: "success",
+ summary: "success",
+ detail: "修改密码成功",
+ });
+ setShowEditPassword(false);
+ } catch (err) {
+ console.error("修改密码失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "修改密码失败",
+ });
}
- // 修改签名接口
- const editSign = async () => {
- try {
- await axios.put(process.env.PUBLIC_URL + `/user/signature`, {
- params: { userId, signature: signValue }
- });
- toast.current?.show({ severity: 'success', summary: 'success', detail: '修改签名成功' });
- setShowEditSignature(false);
- } catch (err) {
- console.error('修改签名失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '修改签名失败' });
- }
- }
+ };
- // 修改头像接口
- const editAvatar = async () => {
- try {
- await axios.put(process.env.PUBLIC_URL + `/user/avatar`, {
- params: { userId, avatar: avatarUrl }
- });
- toast.current?.show({ severity: 'success', summary: 'success', detail: '修改头像成功' });
- setShowEditAvatar(false);
- } catch (err) {
- console.error('修改头像失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '修改头像失败' });
- }
+ // 修改签名接口
+ const editSign = async () => {
+ try {
+ await axios.put(process.env.PUBLIC_URL + `/user/signature`, {
+ params: { userId, signature: signValue },
+ });
+ toast.current?.show({
+ severity: "success",
+ summary: "success",
+ detail: "修改签名成功",
+ });
+ setShowEditSignature(false);
+ } catch (err) {
+ console.error("修改签名失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "修改签名失败",
+ });
}
- return (
+ };
+
+ // 修改头像接口
+ const editAvatar = async () => {
+ try {
+ await axios.put(process.env.PUBLIC_URL + `/user/avatar`, {
+ userId,
+ avatar: avatarUrl,
+ });
+ toast.current?.show({
+ severity: "success",
+ summary: "success",
+ detail: "修改头像成功",
+ });
+ setAvatar(avatarUrl);
+ setShowEditAvatar(false);
+ localStorage.setItem("user", JSON.stringify({...user, Avatar: avatarUrl}))
+ } catch (err) {
+ console.error("修改头像失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "修改头像失败",
+ });
+ }
+ };
+ return (
+ <div
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ className="user-avatar-wrapper"
+ >
+ <Toast ref={toast}></Toast>
+ <Link href="/user" className="no-underline">
+ <Avatar
+ image={useLocalStorage<User>("user")?.Avatar}
+ size="large"
+ shape="circle"
+ className="user-avatar-link"
+ />
+ </Link>
+
+ <OverlayPanel ref={op} dismissable={false}>
<div
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}
- className="user-avatar-wrapper"
+ onMouseEnter={() => clearTimeout(hoverTimeout)}
+ onMouseLeave={handleMouseLeave}
+ className="user-overlay-panel"
>
<Toast ref={toast}></Toast>
<Link href="/user" className="no-underline">
@@ -199,7 +247,7 @@
try {
const res = await axios.post(`${process.env.PUBLIC_URL}/file`, formData);
- const fileUrl = res.data.url;
+ const fileUrl = res.data;
console.log(fileUrl);
setAvatar(fileUrl);
toast.current?.show({ severity: 'success', summary: '上传成功' });
@@ -283,6 +331,179 @@
</div>
</Dialog>
</div>
- );
+ </OverlayPanel>
+ {/* 修改签名弹窗 */}
+ <Dialog
+ header="修改签名"
+ visible={showEditSignature}
+ style={{ width: "30vw" }}
+ onHide={() => {
+ setSignValue("");
+ setShowEditSignature(false);
+ }}
+ modal
+ >
+ <div className="dialog-container">
+ <div className="dialog-input-group">
+ <FloatLabel>
+ <InputText
+ id="username"
+ value={signValue}
+ onChange={(e) => setSignValue(e.target.value)}
+ />
+ <label htmlFor="username">个性签名</label>
+ </FloatLabel>
+ </div>
+ <div className="dialog-button-group">
+ <Button
+ label="确定"
+ className="p-button-sm"
+ onClick={() => editSign()}
+ />
+ <Button
+ label="取消"
+ className="p-button-secondary p-button-sm"
+ onClick={() => {
+ setSignValue("");
+ setShowEditSignature(false);
+ }}
+ />
+ </div>
+ </div>
+ </Dialog>
+
+ {/* 修改头像弹窗 */}
+ <Dialog
+ header="修改头像"
+ visible={showEditAvatar}
+ style={{ display: "flex", flexDirection: "column", width: "30vw" }}
+ onHide={() => {
+ setAvatar("");
+ setShowEditAvatar(false);
+ }}
+ modal
+ >
+ <div className="dialog-container">
+ <FileUpload
+ mode="advanced"
+ name="file"
+ customUpload
+ uploadHandler={async (e) => {
+ const formData = new FormData();
+ formData.append("file", e.files[0]);
+
+ try {
+ const res = await axios.post(
+ `${process.env.PUBLIC_URL}/file`,
+ formData
+ );
+ const fileUrl = res.data;
+ console.log(fileUrl);
+ setAvatar(fileUrl);
+ toast.current?.show({
+ severity: "success",
+ summary: "上传成功",
+ });
+ } catch (error) {
+ console.log(error);
+ toast.current?.show({ severity: "error", summary: "上传失败" });
+ }
+ }}
+ auto
+ accept="image/*"
+ chooseLabel="上传头像"
+ />
+
+ <div className="dialog-button-group">
+ <Button
+ label="确定"
+ className="p-button-sm"
+ onClick={() => editAvatar()}
+ />
+ <Button
+ label="取消"
+ className="p-button-secondary p-button-sm"
+ onClick={() => setShowEditAvatar(false)}
+ />
+ </div>
+ </div>
+ </Dialog>
+
+ {/* 修改密码弹窗 */}
+ <Dialog
+ header="修改密码"
+ visible={showEditPassword}
+ style={{ width: "30vw" }}
+ onHide={() => {
+ setOldPasswardValue("");
+ setPasswardValue("");
+ setNewPasswardValue("");
+ setShowEditPassword(false);
+ }}
+ modal
+ >
+ <div className="dialog-container">
+ <div className="dialog-input-group">
+ <FloatLabel>
+ <InputText
+ id="username"
+ value={oldPasswardValue}
+ onChange={(e) => setOldPasswardValue(e.target.value)}
+ />
+ <label htmlFor="username">输入旧密码</label>
+ </FloatLabel>
+ </div>
+ <div className="dialog-input-group">
+ <FloatLabel>
+ <InputText
+ id="username"
+ value={passwardValue}
+ onChange={(e) => setPasswardValue(e.target.value)}
+ />
+ <label htmlFor="username">更新密码</label>
+ </FloatLabel>
+ </div>
+ <div className="dialog-input-group">
+ <FloatLabel>
+ <InputText
+ id="username"
+ value={newPasswardValue}
+ onChange={(e) => setNewPasswardValue(e.target.value)}
+ />
+ <label htmlFor="username">确认密码</label>
+ </FloatLabel>
+ </div>
+ <div className="dialog-button-group">
+ <Button
+ label="确定"
+ className="p-button-sm"
+ onClick={() => {
+ if (passwardValue !== newPasswardValue) {
+ toast.current?.show({
+ severity: "warn",
+ summary: "两次密码不一致",
+ detail: "请确保新密码和确认密码一致",
+ });
+ return;
+ } else {
+ editPassward();
+ }
+ }}
+ />
+ <Button
+ label="取消"
+ className="p-button-secondary p-button-sm"
+ onClick={() => {
+ setOldPasswardValue("");
+ setPasswardValue("");
+ setNewPasswardValue("");
+ setShowEditPassword(false);
+ }}
+ />
+ </div>
+ </div>
+ </Dialog>
+ </div>
+ );
}
diff --git a/src/app/user/manage/resources/page.tsx b/src/app/user/manage/resources/page.tsx
index 4d0d03f..0bfa184 100644
--- a/src/app/user/manage/resources/page.tsx
+++ b/src/app/user/manage/resources/page.tsx
@@ -195,7 +195,7 @@
<Card key={resourceList.resourceId} className="resources-list-card"
onClick={() => router.push(`/resource/resource-detail/${resourceList.resourceId}`)}>
<Image alt="avatar"
- src={process.env.NEXT_PUBLIC_NGINX_URL + "resource/" + resourceList.resourcePicture}
+ src={ "resource/" + resourceList.resourcePicture}
className="resource-avatar" width="250" height="140" />
<div className="resource-header">
<div className="resource-content">
diff --git a/src/app/user/page.tsx b/src/app/user/page.tsx
index d534cd5..0402059 100644
--- a/src/app/user/page.tsx
+++ b/src/app/user/page.tsx
@@ -1,1076 +1,1382 @@
-'use client';
+"use client";
-import React, { useEffect, useState, useRef } from 'react';
-import { TabView, TabPanel } from 'primereact/tabview';
-import { Avatar } from 'primereact/avatar';
-import { Button } from 'primereact/button';
-import { Card } from 'primereact/card';
+import React, { useEffect, useState, useRef } from "react";
+import { TabView, TabPanel } from "primereact/tabview";
+import { Avatar } from "primereact/avatar";
+import { Button } from "primereact/button";
+import { Card } from "primereact/card";
import { Image } from "primereact/image";
// 发布资源
-import { Dialog } from 'primereact/dialog';
+import { Dialog } from "primereact/dialog";
import { InputText } from "primereact/inputtext";
import { InputTextarea } from "primereact/inputtextarea";
import { FileUpload } from "primereact/fileupload";
// 资源分类
import { RadioButton, RadioButtonChangeEvent } from "primereact/radiobutton";
// 资源标签
-import { MultiSelect, MultiSelectChangeEvent } from 'primereact/multiselect';
+import { MultiSelect, MultiSelectChangeEvent } from "primereact/multiselect";
// 浮动按钮
-import { SpeedDial } from 'primereact/speeddial';
+import { SpeedDial } from "primereact/speeddial";
// 评分图标
-import { Fire } from '@icon-park/react';
+import { Fire } from "@icon-park/react";
// 消息提醒
-import { Toast } from 'primereact/toast';
+import { Toast } from "primereact/toast";
// 页面跳转
import { useRouter } from "next/navigation";
// 类型转换
-import {toNumber} from "lodash";
+import { toNumber } from "lodash";
// 分页
-import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+import { Paginator, type PaginatorPageChangeEvent } from "primereact/paginator";
// 接口传输
import axios from "axios";
-import { useLocalStorage } from '../hook/useLocalStorage';
+import { useLocalStorage } from "../hook/useLocalStorage";
// 样式
-import './user.scss';
+import "./user.scss";
interface User {
- Id: number;
-};
+ Id: number;
+}
// 用户信息
interface UserInfo {
- userId: number;
- username: string;
- password: string;
- avatar: string;
- followerCount: number;// 粉丝数
- subscriberCount: number;// 关注数
- signature: string;// 个性签名
- uploadAmount: number;
- purchaseAmount: number;
- credits: number;
+ userId: number;
+ username: string;
+ password: string;
+ avatar: string;
+ followerCount: number; // 粉丝数
+ subscriberCount: number; // 关注数
+ signature: string; // 个性签名
+ uploadAmount: number;
+ purchaseAmount: number;
+ credits: number;
}
// 用户数据
interface UserData {
- subscriberCount: number; // 关注数
- uploadAmount: number; // 上传量(资源个数)
- beDownloadedAmount: number; // 上传资源被下载量
- seedPercentageList: number[]; // 上传资源类型百分比列表,按材质包、模组、整合包、地图的顺序返回
+ subscriberCount: number; // 关注数
+ uploadAmount: number; // 上传量(资源个数)
+ beDownloadedAmount: number; // 上传资源被下载量
+ seedPercentageList: number[]; // 上传资源类型百分比列表,按材质包、模组、整合包、地图的顺序返回
}
// 用户发布过的资源
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;
- seeds: number; // 种子数
- classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ resourceSummary: string; // 资源简介(一句话)
+ resourceDetail: string; // 资源介绍
+ uploadTime: string; // 上传时间
+ lastUpdateTime: string; // 最近更新时间
+ price: number;
+ downloads: number;
+ likes: number;
+ collections: number;
+ comments: number;
+ seeds: number; // 种子数
+ classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map
}
// 用户发布过的资源列表
interface ResourceList {
- records: Resource[];
+ records: Resource[];
}
// 帖子
interface Thread {
- threadId: number;
- userId: number;
- threadPicture: string;
- title: string;
- likes: number;
- createAt: string;
+ threadId: number;
+ userId: number;
+ threadPicture: string;
+ title: string;
+ likes: number;
+ createAt: string;
}
// 发布帖子列表
interface ThreadList {
- records: Thread[];
- total: number;
- pages: number;
- current: number;
- size: number;
+ records: Thread[];
+ total: number;
+ pages: number;
+ current: number;
+ size: number;
}
// 悬赏
interface Reward {
- rewardId: number;
- userId: number;
- rewardPicture: string;
- rewardName: string;
- createAt: string;
- rewardDescription: string;
- price: number;
+ rewardId: number;
+ userId: number;
+ rewardPicture: string;
+ rewardName: string;
+ createAt: string;
+ rewardDescription: string;
+ price: number;
}
// 我的悬赏列表
interface RewardList {
- rewardList: Reward[];
- total: number; // 总记录数
+ rewardList: Reward[];
+ total: number; // 总记录数
}
// 资源标签
interface GameplayOption {
- name: string;
- code: number;
+ name: string;
+ code: number;
}
// 资源标签选项
const gameplayOptions: GameplayOption[] = [
- { name: '科技', code: 1 },
- { name: '魔法', code: 2 },
- { name: '建筑', code: 3 },
- { name: '风景', code: 4 },
- { name: '竞技', code: 5 },
- { name: '生存', code: 6 },
- { name: '冒险', code: 7 },
- { name: '跑酷', code: 8 },
- { name: '艺术', code: 9 },
- { name: '剧情', code: 10 },
- { name: '社交', code: 11 },
- { name: '策略', code: 12 },
- { name: '极限', code: 13 }
+ { name: "科技", code: 1 },
+ { name: "魔法", code: 2 },
+ { name: "建筑", code: 3 },
+ { name: "风景", code: 4 },
+ { name: "竞技", code: 5 },
+ { name: "生存", code: 6 },
+ { name: "冒险", code: 7 },
+ { name: "跑酷", code: 8 },
+ { name: "艺术", code: 9 },
+ { name: "剧情", code: 10 },
+ { name: "社交", code: 11 },
+ { name: "策略", code: 12 },
+ { name: "极限", code: 13 },
];
export default function UserPage() {
- const user = useLocalStorage<User>('user');
- const userId: number = user?.Id ?? -1;
+ const user = useLocalStorage<User>("user");
+ const userId: number = user?.Id ?? -1;
- // 路由
- const router = useRouter();
- // 发布资源列表
- const [resourceList, setResourceList] = useState<Resource[]>([]);
- // 用户信息
- const [userInfo, setUserInfo] = useState<UserInfo>();
- // 用户数据
- const [userData, setUserData] = useState<UserData>();
- // 消息提醒
- const toast = useRef<Toast>(null);
- // 资源标签
- const [selectedGameplay, setSelectedGameplay] = useState<GameplayOption[]>([]);
- // 资源封面路径
- const [resourcePictureUrl, setResourcePictureUrl] = useState<string>('');
- // 主页发布帖子列表
- const [homePageThread, setHomePageThread] = useState<ThreadList>();
- // 我的帖子列表
- const [threadList, setThreadList] = useState<Thread[]>([]);
- // 我的悬赏列表
- const [rewardList, setRewardList] = useState<Reward[]>([]);
- // 控制Tab切换
- const [activeIndex, setActiveIndex] = useState(0);
- // 帖子分页
- const [threadFirst, setThreadFirst] = useState(0);
- const [threadRows, setThreadRows] = useState(6);
- const [totalThreads, setTotalThreads] = useState<number>(0);
- const onThreadPageChange = (event: PaginatorPageChangeEvent) => {
- setThreadFirst(event.first);
- setThreadRows(event.rows);
- };
- // 悬赏分页
- const [rewardFirst, setRewardFirst] = useState(0);
- const [rewardRows, setRewardRows] = useState(5);
- const [totalRewards, setTotalRewards] = useState<number>(0);
- const onRewardPageChange = (event: PaginatorPageChangeEvent) => {
- setRewardFirst(event.first);
- setRewardRows(event.rows);
- };
+ // 路由
+ const router = useRouter();
+ // 发布资源列表
+ const [resourceList, setResourceList] = useState<Resource[]>([]);
+ // 用户信息
+ const [userInfo, setUserInfo] = useState<UserInfo>();
+ // 用户数据
+ const [userData, setUserData] = useState<UserData>();
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 资源标签
+ const [selectedGameplay, setSelectedGameplay] = useState<GameplayOption[]>(
+ []
+ );
+ // 资源封面路径
+ const [resourcePictureUrl, setResourcePictureUrl] = useState<string>("");
+ // 主页发布帖子列表
+ const [homePageThread, setHomePageThread] = useState<ThreadList>();
+ // 我的帖子列表
+ const [threadList, setThreadList] = useState<Thread[]>([]);
+ // 我的悬赏列表
+ const [rewardList, setRewardList] = useState<Reward[]>([]);
+ // 控制Tab切换
+ const [activeIndex, setActiveIndex] = useState(0);
+ // 帖子分页
+ const [threadFirst, setThreadFirst] = useState(0);
+ const [threadRows, setThreadRows] = useState(6);
+ const [totalThreads, setTotalThreads] = useState<number>(0);
+ const onThreadPageChange = (event: PaginatorPageChangeEvent) => {
+ setThreadFirst(event.first);
+ setThreadRows(event.rows);
+ };
+ const [torrentUrl, setTorrentUrl] = useState<string>(""); // 上传 .torrent 后得到的 URL
- useEffect(() => {
- fetchUserInfo();
- fetchUserData();
- fetchResourceList();
- }, []);
+ // 悬赏分页
+ const [rewardFirst, setRewardFirst] = useState(0);
+ const [rewardRows, setRewardRows] = useState(5);
+ const [totalRewards, setTotalRewards] = useState<number>(0);
+ const onRewardPageChange = (event: PaginatorPageChangeEvent) => {
+ setRewardFirst(event.first);
+ setRewardRows(event.rows);
+ };
- // 获取用户信息
- const fetchUserInfo = async () => {
- try {
- const response = await axios.get<UserInfo>(process.env.PUBLIC_URL + `/user/info`, {
- params: { userId }
- });
- console.log('获取用户信息:', response.data);
- setUserInfo(response.data);
- } catch (err) {
- console.error('获取用户信息失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取用户信息失败' });
+ useEffect(() => {
+ fetchUserInfo();
+ fetchUserData();
+ fetchResourceList();
+ }, []);
+
+ // 获取用户信息
+ const fetchUserInfo = async () => {
+ try {
+ const response = await axios.get<UserInfo>(
+ process.env.PUBLIC_URL + `/user/info`,
+ {
+ params: { userId },
}
- };
-
- // 获取用户数据
- const fetchUserData = async () => {
- try {
- const response = await axios.get<UserData>(process.env.PUBLIC_URL + `/user/data`, {
- params: { userId }
- });
- console.log('获取用户数据:', response.data);
- setUserData(response.data);
- } catch (err) {
- console.error('获取用户数据失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取用户数据失败' });
- }
- };
-
- // 格式化数字显示 (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();
- };
-
- // 获取发布资源
- const fetchResourceList = async () => {
- try {
- const response = await axios.get<ResourceList>(process.env.PUBLIC_URL + `/user/upload`, {
- params: { userId, pageNumber: 1, rows: 3 }
- });
- console.log('获取发布资源列表:', response.data.records);
- setResourceList(response.data.records);
- } catch (err) {
- console.error('获取发布资源失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取发布资源失败' });
- }
- };
-
- useEffect(() => {
- fetchHomePageThread();
- }, []);
-
- // 获取主页部分的发布帖子
- const fetchHomePageThread = async () => {
- try {
- const response = await axios.get<ThreadList>(process.env.PUBLIC_URL + `/user/thread`, {
- params: { userId: userId, pageNumber: 1, rows: 3 }
- })
- console.log('获取主页发布帖子:', response.data);
- setHomePageThread(response.data);
-
- } catch (err) {
- console.error('获取主页发布帖子失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取主页发布帖子失败' });
- }
+ );
+ console.log("获取用户信息:", response.data);
+ setUserInfo(response.data);
+ } catch (err) {
+ console.error("获取用户信息失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取用户信息失败",
+ });
}
+ };
- useEffect(() => {
- fetchThreadList();
- }, [threadFirst, threadRows]);
-
- // 获取用户发布的所有帖子
- const fetchThreadList = async () => {
- try {
- const pageNumber = threadFirst / threadRows + 1;
- const response = await axios.get<ThreadList>(process.env.PUBLIC_URL + `/user/thread`, {
- params: { userId: userId, pageNumber: pageNumber, rows: threadRows }
- })
- console.log('获取我的帖子:', response.data);
- setThreadList(response.data.records);
- setTotalThreads(response.data.total)
- }catch (err) {
- console.error('获取我的帖子失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取我的帖子失败' });
+ // 获取用户数据
+ const fetchUserData = async () => {
+ try {
+ const response = await axios.get<UserData>(
+ process.env.PUBLIC_URL + `/user/data`,
+ {
+ params: { userId },
}
+ );
+ console.log("获取用户数据:", response.data);
+ setUserData(response.data);
+ } catch (err) {
+ console.error("获取用户数据失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取用户数据失败",
+ });
}
+ };
- useEffect(() => {
+ // 格式化数字显示 (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();
+ };
+
+ // 获取发布资源
+ const fetchResourceList = async () => {
+ try {
+ const response = await axios.get<ResourceList>(
+ process.env.PUBLIC_URL + `/user/upload`,
+ {
+ params: { userId, pageNumber: 1, rows: 3 },
+ }
+ );
+ console.log("获取发布资源列表:", response.data.records);
+ setResourceList(response.data.records);
+ } catch (err) {
+ console.error("获取发布资源失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取发布资源失败",
+ });
+ }
+ };
+
+ useEffect(() => {
+ fetchHomePageThread();
+ }, []);
+
+ // 获取主页部分的发布帖子
+ const fetchHomePageThread = async () => {
+ try {
+ const response = await axios.get<ThreadList>(
+ process.env.PUBLIC_URL + `/user/thread`,
+ {
+ params: { userId: userId, pageNumber: 1, rows: 3 },
+ }
+ );
+ console.log("获取主页发布帖子:", response.data);
+ setHomePageThread(response.data);
+ } catch (err) {
+ console.error("获取主页发布帖子失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取主页发布帖子失败",
+ });
+ }
+ };
+
+ useEffect(() => {
+ fetchThreadList();
+ }, [threadFirst, threadRows]);
+
+ // 获取用户发布的所有帖子
+ const fetchThreadList = async () => {
+ try {
+ const pageNumber = threadFirst / threadRows + 1;
+ const response = await axios.get<ThreadList>(
+ process.env.PUBLIC_URL + `/user/thread`,
+ {
+ params: { userId: userId, pageNumber: pageNumber, rows: threadRows },
+ }
+ );
+ console.log("获取我的帖子:", response.data);
+ setThreadList(response.data.records);
+ setTotalThreads(response.data.total);
+ } catch (err) {
+ console.error("获取我的帖子失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取我的帖子失败",
+ });
+ }
+ };
+
+ useEffect(() => {
+ fetchRewardList();
+ }, [rewardFirst, rewardRows]);
+
+ // 获取用户发布的所有悬赏
+ const fetchRewardList = async () => {
+ try {
+ const pageNumber = rewardFirst / rewardRows + 1;
+ const response = await axios.get<RewardList>(
+ process.env.PUBLIC_URL + `/user/reward`,
+ {
+ params: { userId: userId, pageNumber: pageNumber, rows: rewardRows },
+ }
+ );
+ console.log("获取我的悬赏:", response.data);
+ setRewardList(response.data.rewardList);
+ setTotalRewards(response.data.total);
+ } catch (err) {
+ console.error("获取我的悬赏失败", err);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "获取我的悬赏失败",
+ });
+ }
+ };
+
+ // 浮动按钮的子模块
+ const actions = [
+ {
+ template: () => (
+ <Button
+ label="管理资源"
+ onClick={() => router.push(`/user/manage/resources/`)}
+ />
+ ),
+ },
+ {
+ template: () => (
+ <Button
+ label="已购资源"
+ onClick={() => router.push(`/user/purchased-resources/`)}
+ />
+ ),
+ },
+ {
+ template: () => (
+ <Button label="发布资源" onClick={() => setVisible(true)} />
+ ),
+ },
+ {
+ template: () => <Button label="编辑悬赏" />,
+ },
+ ];
+
+ // 发布资源弹窗
+ const [visible, setVisible] = useState(false);
+ const [resourceFormData, setResourceFormData] = useState({
+ resource: {
+ resourceName: "",
+ resourcePicture: "",
+ resourceSummary: "",
+ resourceDetail: "",
+ uploadTime: "",
+ lastUpdateTime: "",
+ price: "",
+ classify: "",
+ },
+ resourceVersionName: "",
+ gameplayList: [""],
+ completeRewardId: null,
+ userId: 0,
+ });
+ const [ingredient, setIngredient] = useState<string>("");
+
+ // 删除悬赏弹窗
+ const [deleteVisible, setDeleteVisible] = useState(false);
+ // 要删除悬赏的id
+ const [deleteRewardId, setDeleteResourceId] = useState<number>(0);
+ // 处理删除悬赏接口
+ const handleDeleteSubmit = async () => {
+ try {
+ // 发送DELETE请求
+ const response = await axios.delete(process.env.PUBLIC_URL + `/reward`, {
+ params: { rewardId: deleteRewardId },
+ });
+ console.log("用户" + userId + "要删除" + deleteRewardId + "号悬赏");
+
+ if (response.status === 204) {
+ console.log("用户成功删除悬赏");
+ toast.current?.show({
+ severity: "success",
+ summary: "Success",
+ detail: "删除悬赏成功",
+ });
+ setDeleteVisible(false);
+ // 重新拉取资源列表
fetchRewardList();
- }, [rewardFirst, rewardRows]);
-
- // 获取用户发布的所有悬赏
- const fetchRewardList = async () => {
- try {
- const pageNumber = rewardFirst / rewardRows + 1;
- const response = await axios.get<RewardList>(process.env.PUBLIC_URL + `/user/reward`, {
- params: { userId: userId, pageNumber: pageNumber, rows: rewardRows }
- })
- console.log('获取我的悬赏:', response.data);
- setRewardList(response.data.rewardList);
- setTotalRewards(response.data.total)
- }catch (err) {
- console.error('获取我的悬赏失败', err);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '获取我的悬赏失败' });
- }
+ }
+ } catch (error) {
+ console.error("资源删除失败:", error);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "密码错误,资源删除失败",
+ });
}
+ };
- // 浮动按钮的子模块
- const actions = [
- {
- template: () => (
- <Button label="管理资源" onClick={() => router.push(`/user/manage/resources/`)} />
- )
- },
- {
- template: () => (
- <Button label="已购资源" onClick={() => router.push(`/user/purchased-resources/`)} />
- )
- },
- {
- template: () => (
- <Button label="发布资源" onClick={() => setVisible(true)} />
- )
- },
- {
- template: () => (
- <Button label="编辑悬赏" />
- )
- }
- ];
+ // 编辑悬赏弹窗
+ const [editVisible, setEditVisible] = useState(false);
+ // 悬赏封面路径
+ const [rewardPictureUrl, setRewardPictureUrl] = useState<string>("");
+ const [editRewardFormData, setEditRewardFormData] = useState({
+ rewardId: 0,
+ rewardName: "",
+ price: "",
+ rewardDescription: "",
+ });
- // 发布资源弹窗
- const [visible, setVisible] = useState(false);
- const [resourceFormData, setResourceFormData] = useState({
+ // 处理编辑资源接口
+ const handleEditSubmit = async () => {
+ try {
+ const postData = {
+ rewardId: editRewardFormData.rewardId,
+ rewardName: editRewardFormData.rewardName,
+ rewardPicture: rewardPictureUrl,
+ price: toNumber(editRewardFormData.price),
+ rewardDescription: editRewardFormData.rewardDescription,
+ };
+ // 发送POST请求
+ const response = await axios.put(
+ process.env.PUBLIC_URL + "/reward/info",
+ postData
+ );
+ console.log("编辑悬赏的信息:", postData);
+
+ if (response.status === 200) {
+ toast.current?.show({
+ severity: "success",
+ summary: "Success",
+ detail: "悬赏编辑成功",
+ });
+ // 编辑成功
+ setEditVisible(false);
+ fetchRewardList();
+ }
+ } catch (error) {
+ console.error("悬赏编辑失败:", error);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "悬赏编辑失败",
+ });
+ }
+ };
+
+ // 上传资源接口
+ const handleSubmit = async () => {
+ try {
+ // 规定用户必须输入的内容
+ if (resourceFormData.resource.resourceName == "") {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源名称",
+ });
+ return;
+ }
+ if (resourceFormData.resource.resourceSummary == "") {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源简介",
+ });
+ return;
+ }
+ if (resourceFormData.resourceVersionName == "") {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源版本名",
+ });
+ return;
+ }
+ if (resourceFormData.resource.price == "") {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源价格",
+ });
+ return;
+ }
+ if (resourceFormData.resource.classify == "") {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源分类",
+ });
+ return;
+ }
+ if (
+ resourceFormData.gameplayList.length === 1 &&
+ resourceFormData.gameplayList[0] === ""
+ ) {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源标签",
+ });
+ return;
+ }
+ if (resourcePictureUrl === "") {
+ toast.current?.show({
+ severity: "info",
+ summary: "error",
+ detail: "缺少资源封面",
+ });
+ return;
+ }
+
+ const currentDate = new Date().toISOString().split("T")[0];
+ const postData = {
resource: {
- resourceName: '',
- resourcePicture: '',
- resourceSummary: '',
- resourceDetail: '',
- uploadTime: '',
- lastUpdateTime: '',
- price: '',
- classify: '',
+ resourceName: resourceFormData.resource.resourceName,
+ resourcePicture: resourcePictureUrl,
+ resourceSummary: resourceFormData.resource.resourceSummary,
+ resourceDetail: resourceFormData.resource.resourceDetail,
+ uploadTime: currentDate,
+ lastUpdateTime: currentDate,
+ price: toNumber(resourceFormData.resource.price),
+ classify: resourceFormData.resource.classify,
},
- gameplayList: [''],
+ gameplayList: resourceFormData.gameplayList,
+ resourceVersionName: resourceFormData.resourceVersionName,
completeRewardId: null,
- userId: 0,
- });
- const [ingredient, setIngredient] = useState<string>('');
+ userId, // 记得用户登录状态获取
+ };
+ // 发送POST请求
+ const response = await axios.post(
+ process.env.PUBLIC_URL + "/resource",
+ postData
+ );
+ console.log("上传资源的信息:", postData);
- // 删除悬赏弹窗
- const [deleteVisible, setDeleteVisible] = useState(false);
- // 要删除悬赏的id
- const [deleteRewardId, setDeleteResourceId] = useState<number>(0);
- // 处理删除悬赏接口
- const handleDeleteSubmit = async () => {
+ if (response.status === 200) {
+ toast.current?.show({
+ severity: "success",
+ summary: "Success",
+ detail: "资源上传成功",
+ });
+ console.log(torrentUrl);
try {
- // 发送DELETE请求
- const response = await axios.delete(process.env.PUBLIC_URL + `/reward`, {
- params: {rewardId: deleteRewardId},
- });
- console.log("用户" + userId + "要删除" + deleteRewardId + "号悬赏");
-
- if (response.status === 204) {
- console.log("用户成功删除悬赏");
- toast.current?.show({severity: 'success', summary: 'Success', detail: '删除悬赏成功'});
- setDeleteVisible(false);
- // 重新拉取资源列表
- fetchRewardList();
+ const versionResponse = await axios.post(
+ `${process.env.PUBLIC_URL}/resource/version`,
+ {
+ resourceVersionName: postData.resourceVersionName,
+ resourceId: Number(response.data),
}
- } catch (error) {
- console.error('资源删除失败:', error);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '密码错误,资源删除失败' });
+ );
+ // 上传资源文件
+ const btPayload = {
+ torrentUrl: torrentUrl,
+ infoHash: "8",
+ uploadTime: currentDate,
+ uploaderUserId: userId,
+ resourceVersionId: Number(versionResponse.data),
+ };
+
+ await axios.post(`${process.env.PUBLIC_URL}/file/bt`, btPayload);
+ toast.current?.show({
+ severity: "success",
+ summary: "资源登记成功",
+ detail: "已同步种子信息",
+ });
+ } catch (btError) {
+ console.error("种子登记失败:", btError);
+ toast.current?.show({
+ severity: "warn",
+ summary: "资源已上传",
+ detail: "但种子登记失败",
+ });
}
- };
+ // 上传成功
+ setVisible(false);
+ // 重置表单
+ setResourceFormData({
+ resource: {
+ resourceName: "",
+ resourcePicture: "",
+ resourceSummary: "",
+ resourceDetail: "",
+ uploadTime: "",
+ lastUpdateTime: "",
+ price: "",
+ classify: "",
+ },
+ resourceVersionName: "",
+ gameplayList: [],
+ completeRewardId: null,
+ userId: 0,
+ });
+ // 重置资源分类
+ setIngredient("");
+ // 重置资源标签
+ setSelectedGameplay([]);
+ // 重置资源封面
+ setResourcePictureUrl("");
+ setTorrentUrl("");
+ }
+ } catch (error) {
+ console.error("资源上传失败:", error);
+ toast.current?.show({
+ severity: "error",
+ summary: "error",
+ detail: "资源上传失败",
+ });
+ }
+ };
- // 编辑悬赏弹窗
- const [editVisible, setEditVisible] = useState(false);
- // 悬赏封面路径
- const [rewardPictureUrl, setRewardPictureUrl] = useState<string>('');
- const [editRewardFormData, setEditRewardFormData] = useState({
- rewardId: 0,
- rewardName: '',
- price: '',
- rewardDescription: '',
- });
-
- // 处理编辑资源接口
- const handleEditSubmit = async () => {
- try {
- const postData = {
- rewardId: editRewardFormData.rewardId,
- rewardName: editRewardFormData.rewardName,
- rewardPicture: rewardPictureUrl,
- price: toNumber(editRewardFormData.price),
- rewardDescription: editRewardFormData.rewardDescription,
- };
- // 发送POST请求
- const response = await axios.put(process.env.PUBLIC_URL + '/reward/info', postData);
- console.log("编辑悬赏的信息:", postData);
-
- if (response.status === 200) {
- toast.current?.show({ severity: 'success', summary: 'Success', detail: '悬赏编辑成功' });
- // 编辑成功
- setEditVisible(false);
- fetchRewardList();
- }
- } catch (error) {
- console.error('悬赏编辑失败:', error);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '悬赏编辑失败' });
- }
- };
-
- // 上传资源接口
- const handleSubmit = async () => {
- try {
- // 规定用户必须输入的内容
- if (resourceFormData.resource.resourceName == '') {
- toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源名称' });
- return;
- }
- if (resourceFormData.resource.resourceSummary == '') {
- toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源简介' });
- return;
- }
- if (resourceFormData.resource.price == '') {
- toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源价格' });
- return;
- }
- if (resourceFormData.resource.classify == '') {
- toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源分类' });
- return;
- }
- if (
- resourceFormData.gameplayList.length === 1 &&
- resourceFormData.gameplayList[0] === ''
- ) {
- toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源标签' });
- return;
- }
- if (resourcePictureUrl === '') {
- toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源封面' });
- return;
- }
-
- const currentDate = new Date().toISOString().split('T')[0];
- const postData = {
- resource: {
- resourceName: resourceFormData.resource.resourceName,
- resourcePicture: resourcePictureUrl,
- resourceSummary: resourceFormData.resource.resourceSummary,
- resourceDetail: resourceFormData.resource.resourceDetail,
- uploadTime: currentDate,
- lastUpdateTime: currentDate,
- price: toNumber(resourceFormData.resource.price),
- classify: resourceFormData.resource.classify,
- },
- gameplayList: resourceFormData.gameplayList,
- completeRewardId: null,
- userId, // 记得用户登录状态获取
- };
- // 发送POST请求
- const response = await axios.post(process.env.PUBLIC_URL + '/resource', postData);
- console.log("上传资源的信息:", postData);
-
- if (response.status === 200) {
- toast.current?.show({ severity: 'success', summary: 'Success', detail: '资源上传成功' });
- // 上传成功
- setVisible(false);
- // 重置表单
- setResourceFormData({
- resource: {
- resourceName: '',
- resourcePicture: '',
- resourceSummary: '',
- resourceDetail: '',
- uploadTime: '',
- lastUpdateTime: '',
- price: '',
- classify: '',
- },
- gameplayList: [],
- completeRewardId: null,
- userId: 0,
- });
- // 重置资源分类
- setIngredient("");
- // 重置资源标签
- setSelectedGameplay([]);
- // 重置资源封面
- setResourcePictureUrl('');
- }
- } catch (error) {
- console.error('资源上传失败:', error);
- toast.current?.show({ severity: 'error', summary: 'error', detail: '资源上传失败' });
- }
- };
-
- return (
- <div className="user-container">
- <Toast ref={toast}></Toast>
- {/*个人信息*/}
- <div className="user-profile-card">
- <Avatar
- image={`${process.env.NEXT_PUBLIC_NGINX_URL}/users/${userInfo?.avatar}`}
- className="user-avatar"
- shape="circle"
- />
- <div className="user-info">
- <div className="user-detail-info">
- <div className="name-container">
- <h2 className="name">{userInfo?.username}</h2>
- <span className="signature">{userInfo?.signature}</span>
- </div>
-
- <div className="stats-container">
- <div className="stats">
- <span className="stats-label">粉丝:</span>
- <span className="stats-value">{userInfo?.followerCount}</span>
- </div>
- <div className="stats">
- <span className="stats-label">累计上传量:</span>
- <span className="stats-value">{formatCount(userData?.uploadAmount)}</span>
- </div>
- <div className="stats">
- <span className="stats-label">关注:</span>
- <span className="stats-value">{userInfo?.subscriberCount}</span>
- </div>
- <div className="stats">
- <span className="stats-label">累计被下载量:</span>
- <span className="stats-value">{formatCount(userData?.beDownloadedAmount)}</span>
- </div>
- </div>
- </div>
-
- <Button label="关注" className="action-button" />
- </div>
+ return (
+ <div className="user-container">
+ <Toast ref={toast}></Toast>
+ {/*个人信息*/}
+ <div className="user-profile-card">
+ <Avatar
+ image={userInfo?.avatar}
+ className="user-avatar"
+ shape="circle"
+ />
+ <div className="user-info">
+ <div className="user-detail-info">
+ <div className="name-container">
+ <h2 className="name">{userInfo?.username}</h2>
+ <span className="signature">{userInfo?.signature}</span>
</div>
- {/*个人内容*/}
- <TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
- <TabPanel header="主页" >
- {/*发布资源*/}
- <div className="homepage-item">
- <div className="section-header">
- <h1>发布资源</h1>
- <Button
- label="显示更多"
- link
- onClick={() => router.push('/user/manage/resources/')}
- />
- </div>
- <div className="resource-grid">
- {resourceList.map((resourceList) => (
- <Card key={resourceList.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${resourceList.resourceId}`)}>
- <Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + resourceList.resourcePicture}
- alt={resourceList.resourceName}
- width="368"
- height="200"
- />
- <div className="card-content">
- <h3>{resourceList.resourceName}</h3>
- <div className="view-count">
- <Fire theme="outline" size="16" fill="#FF8D1A" />
- <span>{resourceList.likes}</span>
- </div>
- </div>
- </Card>
- ))}
- </div>
- </div>
-
- {/*发布帖子*/}
- <div className="homepage-item">
- <div className="section-header">
- <h1>发布帖子</h1>
- <Button
- label="显示更多"
- link
- onClick={() => setActiveIndex(1)}
- />
- </div>
- <div className="resource-grid">
- {homePageThread?.records.map((homePageThread) => (
- <Card key={homePageThread.threadId} className="resource-card" onClick={() => router.push(`/community/thread-detail/${homePageThread.threadId}`)}>
- <Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + homePageThread.threadPicture}
- alt={homePageThread.title}
- width="368"
- height="200"
- />
- <div className="card-content">
- <h3>{homePageThread.title}</h3>
- <div className="view-count">
- <Fire theme="outline" size="16" fill="#FF8D1A" />
- <span>{homePageThread.likes}</span>
- </div>
- </div>
- </Card>
- ))}
- </div>
- </div>
- </TabPanel>
-
- <TabPanel header="帖子">
- {/*我的帖子*/}
- <div className="homepage-item">
- <div className="section-header">
- <h1>我的帖子</h1>
- </div>
- <div className="resource-grid">
- {threadList.map((threadList) => (
- <Card key={threadList.threadId} className="resource-card" onClick={() => router.push(`/community/thread-detail/${threadList.threadId}`)}>
- <Image
- src={process.env.NEXT_PUBLIC_NGINX_URL + threadList.threadPicture}
- alt={threadList.title}
- width="368"
- height="200"
- />
- <div className="card-content">
- <h3>{threadList.title}</h3>
- <div className="view-count">
- <Fire theme="outline" size="16" fill="#FF8D1A" />
- <span>{threadList.likes}</span>
- </div>
- </div>
- </Card>
- ))}
- </div>
- {totalThreads > 6 && <Paginator
- className="Paginator"
- first={threadFirst}
- rows={threadRows}
- totalRecords={totalThreads}
- rowsPerPageOptions={[6, 12]}
- onPageChange={onThreadPageChange}
- />}
- </div>
- </TabPanel>
-
- <TabPanel header="收藏">
-
- </TabPanel>
-
- <TabPanel header="数据">
-
- </TabPanel>
-
- <TabPanel header="悬赏">
- <div className="section-header">
- <h1>我的悬赏</h1>
- </div>
-
- <div className="resource-list">
- {rewardList.map((rewardItem) => (
- <Card key={rewardItem.rewardId} className="resources-list-card"
- onClick={() => router.push(`/reward/reward-detail/${rewardItem.rewardId}`)}>
- <Image alt="avatar"
- src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + rewardItem.rewardPicture}
- className="resource-avatar" width="250" height="140"/>
- <div className="resource-header">
- <div className="resource-content">
- <h3>{rewardItem.rewardName}</h3>
- </div>
-
- <div className="resource-operation">
- <Button
- label="编辑"
- onClick={(e) => {
- e.stopPropagation(); // 关键修复:阻止事件冒泡,避免触发Card的点击事件
- setEditVisible(true);
-
- setEditRewardFormData({
- rewardId: rewardItem.rewardId,
- rewardName: rewardItem.rewardName,
- price: rewardItem.price.toString(),
- rewardDescription: rewardItem.rewardDescription,
- });
- console.log("用户编辑悬赏");
- }}
- />
- <Button
- label="删除"
- onClick={(e) => {
- e.stopPropagation(); // 关键修复:阻止事件冒泡,避免触发Card的点击事件
- setDeleteResourceId(rewardItem.rewardId);
- setDeleteVisible(true);
- }}
- style={{backgroundColor: "rgba(255, 87, 51, 1)"}}
- />
- </div>
- </div>
- </Card>
- ))}
-
- {totalRewards > 5 && <Paginator
- className="Paginator"
- first={rewardFirst}
- rows={rewardRows}
- totalRecords={totalRewards}
- rowsPerPageOptions={[5, 10]}
- onPageChange={onRewardPageChange}
- />}
- </div>
- </TabPanel>
- </TabView>
-
- {/*浮动按钮*/}
- <div className="card">
- <SpeedDial
- model={actions}
- direction="up"
- style={{ position: 'fixed', bottom: '2rem', right: '2rem' }}
- showIcon="pi pi-plus"
- hideIcon="pi pi-times"
- buttonClassName="custom-speeddial-button"
- />
+ <div className="stats-container">
+ <div className="stats">
+ <span className="stats-label">粉丝:</span>
+ <span className="stats-value">{userInfo?.followerCount}</span>
+ </div>
+ <div className="stats">
+ <span className="stats-label">累计上传量:</span>
+ <span className="stats-value">
+ {formatCount(userData?.uploadAmount)}
+ </span>
+ </div>
+ <div className="stats">
+ <span className="stats-label">关注:</span>
+ <span className="stats-value">{userInfo?.subscriberCount}</span>
+ </div>
+ <div className="stats">
+ <span className="stats-label">累计被下载量:</span>
+ <span className="stats-value">
+ {formatCount(userData?.beDownloadedAmount)}
+ </span>
+ </div>
</div>
+ </div>
- {/*发布资源弹窗*/}
- <Dialog
- header="发布资源"
- visible={visible}
- onHide={() => setVisible(false)}
- className="publish-dialog"
- modal
- footer={
- <div className="dialog-footer">
- <Button label="发布" icon="pi pi-check" onClick={handleSubmit} autoFocus />
- <Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" />
- </div>
- }
- >
- <div className="publish-form">
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label htmlFor="name">资源名称</label>
- </div>
- <InputText
- id="name"
- value={resourceFormData.resource.resourceName}
- onChange={(e) => setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- resourceName: e.target.value // 只更新resourceName
- }
- }))}
- placeholder="请输入资源名称"
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label htmlFor="summary">资源简介</label>
- </div>
- <InputText
- id="summary"
- value={resourceFormData.resource.resourceSummary}
- onChange={(e) => setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- resourceSummary: e.target.value
- }
- }))}
- placeholder="请输入资源简介(一句话)"
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <label htmlFor="detail">资源介绍</label>
- </div>
- <InputTextarea
- id="detail"
- value={resourceFormData.resource.resourceDetail}
- onChange={(e) => setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- resourceDetail: e.target.value
- }
- }))}
- rows={5}
- placeholder="请输入资源介绍"
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label htmlFor="price">价格</label>
- </div>
- <InputText
- id="price"
- value={resourceFormData.resource.price}
- onChange={(e) => setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- price: e.target.value
- }
- }))}
- placeholder="请输入资源价格"
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label htmlFor="classify">资源分类(请选择一项)</label>
- </div>
- <div className="form-field-classify">
- <div className="flex align-items-center">
- <RadioButton
- inputId="ingredient1"
- name="pizza"
- value="resourcePack"
- onChange={(e: RadioButtonChangeEvent) => {
- setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- classify: e.target.value
- }
- }));
- setIngredient(e.value);
- console.log(ingredient);
- // console.log(resourceFormData.resource.classify);
- }}
- checked={ingredient === 'resourcePack'}
- />
- <label htmlFor="ingredient1" className="ml-2">材质包</label>
- </div>
- <div className="flex align-items-center">
- <RadioButton
- inputId="ingredient2"
- name="pizza"
- value="modPack"
- onChange={(e: RadioButtonChangeEvent) => {
- setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- classify: e.target.value
- }
- }));
- setIngredient(e.value);
- }}
- checked={ingredient === 'modPack'}
- />
- <label htmlFor="ingredient2" className="ml-2">整合包</label>
- </div>
- <div className="flex align-items-center">
- <RadioButton
- inputId="ingredient3"
- name="pizza"
- value="mod"
- onChange={(e: RadioButtonChangeEvent) => {
- setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- classify: e.target.value
- }
- }));
- setIngredient(e.value);
- }}
- checked={ingredient === 'mod'}
- />
- <label htmlFor="ingredient3" className="ml-2">模组</label>
- </div>
- <div className="flex align-items-center">
- <RadioButton
- inputId="ingredient4"
- name="pizza"
- value="map"
- onChange={(e: RadioButtonChangeEvent) => {
- setResourceFormData(prev => ({
- ...prev, // 复制顶层所有属性
- resource: {
- ...prev.resource, // 复制resource对象的所有属性
- classify: e.target.value
- }
- }));
- setIngredient(e.value);
- }}
- checked={ingredient === 'map'}
- />
- <label htmlFor="ingredient4" className="ml-2">地图</label>
- </div>
- </div>
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label htmlFor="gameplayList">资源标签</label>
- </div>
- <MultiSelect
- value={selectedGameplay}
- onChange={(e: MultiSelectChangeEvent) => {
- const selectedOptions = e.value as GameplayOption[];
- // 提取选中项的 name 属性组成字符串数组
- const selectedNames = selectedOptions.map(item => item.name);
-
- setResourceFormData(prev => ({
- ...prev,
- gameplayList: selectedNames
- }));
- setSelectedGameplay(selectedOptions);
- }}
- options={gameplayOptions}
- display="chip"
- optionLabel="name"
- placeholder="请选择资源标签"
- className="w-full md:w-20rem"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label>封面图片</label>
- </div>
- <FileUpload
- mode="advanced"
- name="resource-image"
- customUpload
- uploadHandler={async (e) => {
- const formData = new FormData();
- formData.append("file", e.files[0]);
-
- try {
- const res = await axios.post(`${process.env.PUBLIC_URL}/file`, formData);
-
- const fileUrl = res.data.url;
- console.log(fileUrl);
- setResourcePictureUrl(fileUrl);
- toast.current?.show({ severity: 'success', summary: '上传成功' });
- } catch (error) {
- console.log(error);
- toast.current?.show({ severity: 'error', summary: '上传失败' });
- }
- }}
- auto
- accept="image/*"
- chooseLabel="上传资源封面"
- />
- </div>
- </div>
- </Dialog>
-
- {/*删除悬赏弹窗*/}
- <Dialog
- header="删除悬赏"
- visible={deleteVisible}
- onHide={() => setDeleteVisible(false)}
- className="resource-delete-dialog"
- modal
- footer={
- <div className="dialog-footer">
- <Button label="确认" icon="pi pi-check" onClick={handleDeleteSubmit} autoFocus />
- <Button label="取消" icon="pi pi-times" onClick={() => setDeleteVisible(false)} className="p-button-text" />
- </div>
- }
- >
- <div className="dialog-form">
- <span style={{marginBottom: "10px"}}>
- 确认是否删除该悬赏?
- </span>
- </div>
- </Dialog>
-
- {/*编辑资源弹窗*/}
- <Dialog
- header="编辑资源"
- visible={editVisible}
- onHide={() => setEditVisible(false)}
- className="resource-edit-dialog"
- modal
- footer={
- <div className="dialog-footer">
- <Button label="确认" icon="pi pi-check" onClick={handleEditSubmit} autoFocus />
- <Button label="取消" icon="pi pi-times" onClick={() => setEditVisible(false)} className="p-button-text" />
- </div>
- }
- >
- <div className="dialog-form">
- <div className="form-field">
- <div className="form-field-header">
- <label htmlFor="name">更改标题</label>
- </div>
- <InputText
- id="name"
- value={editRewardFormData.rewardName}
- onChange={(e) => setEditRewardFormData(prev => ({
- ...prev,
- rewardName: e.target.value
- }))}
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <label htmlFor="price">更改定价</label>
- </div>
- <InputText
- id="price"
- value={editRewardFormData.price}
- onChange={(e) => setEditRewardFormData(prev => ({
- ...prev,
- price: e.target.value
- }))}
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <label htmlFor="description">更改需求</label>
- </div>
- <InputTextarea
- id="description"
- value={editRewardFormData.rewardDescription}
- onChange={(e) => setEditRewardFormData(prev => ({
- ...prev,
- rewardDescription: e.target.value
- }))}
- rows={5}
- className="w-full"
- />
- </div>
- <div className="form-field">
- <div className="form-field-header">
- <span className="form-field-sign">*</span>
- <label>封面图片</label>
- </div>
- <FileUpload
- mode="advanced"
- name="reward-image"
- customUpload
- uploadHandler={async (e) => {
- const formData = new FormData();
- formData.append("file", e.files[0]);
-
- try {
- const res = await axios.post(`${process.env.PUBLIC_URL}/file`, formData);
-
- const fileUrl = res.data.url;
- console.log(fileUrl);
- setRewardPictureUrl(fileUrl);
- toast.current?.show({ severity: 'success', summary: '上传成功' });
- } catch (error) {
- console.log(error);
- toast.current?.show({ severity: 'error', summary: '上传失败' });
- }
- }}
- auto
- accept="image/*"
- chooseLabel="选择悬赏封面"
- />
- </div>
- </div>
- </Dialog>
+ <Button label="关注" className="action-button" />
</div>
- );
-};
+ </div>
+ {/*个人内容*/}
+ <TabView
+ activeIndex={activeIndex}
+ onTabChange={(e) => setActiveIndex(e.index)}
+ >
+ <TabPanel header="主页">
+ {/*发布资源*/}
+ <div className="homepage-item">
+ <div className="section-header">
+ <h1>发布资源</h1>
+ <Button
+ label="显示更多"
+ link
+ onClick={() => router.push("/user/manage/resources/")}
+ />
+ </div>
+ <div className="resource-grid">
+ {resourceList.map((resourceList) => (
+ <Card
+ key={resourceList.resourceId}
+ className="resource-card"
+ onClick={() =>
+ router.push(
+ `/resource/resource-detail/${resourceList.resourceId}`
+ )
+ }
+ >
+ <Image
+ src={resourceList.resourcePicture}
+ alt={resourceList.resourceName}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{resourceList.resourceName}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{resourceList.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+
+ {/*发布帖子*/}
+ <div className="homepage-item">
+ <div className="section-header">
+ <h1>发布帖子</h1>
+ <Button label="显示更多" link onClick={() => setActiveIndex(1)} />
+ </div>
+ <div className="resource-grid">
+ {homePageThread?.records.map((homePageThread) => (
+ <Card
+ key={homePageThread.threadId}
+ className="resource-card"
+ onClick={() =>
+ router.push(
+ `/community/thread-detail/${homePageThread.threadId}`
+ )
+ }
+ >
+ <Image
+ src={homePageThread.threadPicture}
+ alt={homePageThread.title}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{homePageThread.title}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{homePageThread.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+ </TabPanel>
+
+ <TabPanel header="帖子">
+ {/*我的帖子*/}
+ <div className="homepage-item">
+ <div className="section-header">
+ <h1>我的帖子</h1>
+ </div>
+ <div className="resource-grid">
+ {threadList.map((threadList) => (
+ <Card
+ key={threadList.threadId}
+ className="resource-card"
+ onClick={() =>
+ router.push(
+ `/community/thread-detail/${threadList.threadId}`
+ )
+ }
+ >
+ <Image
+ src={threadList.threadPicture}
+ alt={threadList.title}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{threadList.title}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{threadList.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ {totalThreads > 6 && (
+ <Paginator
+ className="Paginator"
+ first={threadFirst}
+ rows={threadRows}
+ totalRecords={totalThreads}
+ rowsPerPageOptions={[6, 12]}
+ onPageChange={onThreadPageChange}
+ />
+ )}
+ </div>
+ </TabPanel>
+
+ <TabPanel header="收藏"></TabPanel>
+
+ <TabPanel header="数据"></TabPanel>
+
+ <TabPanel header="悬赏">
+ <div className="section-header">
+ <h1>我的悬赏</h1>
+ </div>
+
+ <div className="resource-list">
+ {rewardList.map((rewardItem) => (
+ <Card
+ key={rewardItem.rewardId}
+ className="resources-list-card"
+ onClick={() =>
+ router.push(`/reward/reward-detail/${rewardItem.rewardId}`)
+ }
+ >
+ <Image
+ alt="avatar"
+ src={"rewards/" + rewardItem.rewardPicture}
+ className="resource-avatar"
+ width="250"
+ height="140"
+ />
+ <div className="resource-header">
+ <div className="resource-content">
+ <h3>{rewardItem.rewardName}</h3>
+ </div>
+
+ <div className="resource-operation">
+ <Button
+ label="编辑"
+ onClick={(e) => {
+ e.stopPropagation(); // 关键修复:阻止事件冒泡,避免触发Card的点击事件
+ setEditVisible(true);
+
+ setEditRewardFormData({
+ rewardId: rewardItem.rewardId,
+ rewardName: rewardItem.rewardName,
+ price: rewardItem.price.toString(),
+ rewardDescription: rewardItem.rewardDescription,
+ });
+ console.log("用户编辑悬赏");
+ }}
+ />
+ <Button
+ label="删除"
+ onClick={(e) => {
+ e.stopPropagation(); // 关键修复:阻止事件冒泡,避免触发Card的点击事件
+ setDeleteResourceId(rewardItem.rewardId);
+ setDeleteVisible(true);
+ }}
+ style={{ backgroundColor: "rgba(255, 87, 51, 1)" }}
+ />
+ </div>
+ </div>
+ </Card>
+ ))}
+
+ {totalRewards > 5 && (
+ <Paginator
+ className="Paginator"
+ first={rewardFirst}
+ rows={rewardRows}
+ totalRecords={totalRewards}
+ rowsPerPageOptions={[5, 10]}
+ onPageChange={onRewardPageChange}
+ />
+ )}
+ </div>
+ </TabPanel>
+ </TabView>
+
+ {/*浮动按钮*/}
+ <div className="card">
+ <SpeedDial
+ model={actions}
+ direction="up"
+ style={{ position: "fixed", bottom: "2rem", right: "2rem" }}
+ showIcon="pi pi-plus"
+ hideIcon="pi pi-times"
+ buttonClassName="custom-speeddial-button"
+ />
+ </div>
+
+ {/*发布资源弹窗*/}
+ <Dialog
+ header="发布资源"
+ visible={visible}
+ onHide={() => setVisible(false)}
+ className="publish-dialog"
+ modal
+ footer={
+ <div className="dialog-footer">
+ <Button
+ label="发布"
+ icon="pi pi-check"
+ onClick={handleSubmit}
+ autoFocus
+ />
+ <Button
+ label="取消"
+ icon="pi pi-times"
+ onClick={() => setVisible(false)}
+ className="p-button-text"
+ />
+ </div>
+ }
+ >
+ <div className="publish-form">
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label htmlFor="name">资源名称</label>
+ </div>
+ <InputText
+ id="name"
+ value={resourceFormData.resource.resourceName}
+ onChange={(e) =>
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ resourceName: e.target.value, // 只更新resourceName
+ },
+ }))
+ }
+ placeholder="请输入资源名称"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label htmlFor="summary">资源简介</label>
+ </div>
+ <InputText
+ id="summary"
+ value={resourceFormData.resource.resourceSummary}
+ onChange={(e) =>
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ resourceSummary: e.target.value,
+ },
+ }))
+ }
+ placeholder="请输入资源简介(一句话)"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label htmlFor="summary">资源版本名</label>
+ </div>
+ <InputText
+ id="summary"
+ value={resourceFormData.resourceVersionName}
+ onChange={(e) =>
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resourceVersionName: e.target.value,
+ }))
+ }
+ placeholder="请输入资源版本名"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <label htmlFor="detail">资源介绍</label>
+ </div>
+ <InputTextarea
+ id="detail"
+ value={resourceFormData.resource.resourceDetail}
+ onChange={(e) =>
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ resourceDetail: e.target.value,
+ },
+ }))
+ }
+ rows={5}
+ placeholder="请输入资源介绍"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label htmlFor="price">价格</label>
+ </div>
+ <InputText
+ id="price"
+ value={resourceFormData.resource.price}
+ onChange={(e) =>
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ price: e.target.value,
+ },
+ }))
+ }
+ placeholder="请输入资源价格"
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label htmlFor="classify">资源分类(请选择一项)</label>
+ </div>
+ <div className="form-field-classify">
+ <div className="flex align-items-center">
+ <RadioButton
+ inputId="ingredient1"
+ name="pizza"
+ value="resourcePack"
+ onChange={(e: RadioButtonChangeEvent) => {
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ classify: e.target.value,
+ },
+ }));
+ setIngredient(e.value);
+ console.log(ingredient);
+ // console.log(resourceFormData.resource.classify);
+ }}
+ checked={ingredient === "resourcePack"}
+ />
+ <label htmlFor="ingredient1" className="ml-2">
+ 材质包
+ </label>
+ </div>
+ <div className="flex align-items-center">
+ <RadioButton
+ inputId="ingredient2"
+ name="pizza"
+ value="modPack"
+ onChange={(e: RadioButtonChangeEvent) => {
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ classify: e.target.value,
+ },
+ }));
+ setIngredient(e.value);
+ }}
+ checked={ingredient === "modPack"}
+ />
+ <label htmlFor="ingredient2" className="ml-2">
+ 整合包
+ </label>
+ </div>
+ <div className="flex align-items-center">
+ <RadioButton
+ inputId="ingredient3"
+ name="pizza"
+ value="mod"
+ onChange={(e: RadioButtonChangeEvent) => {
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ classify: e.target.value,
+ },
+ }));
+ setIngredient(e.value);
+ }}
+ checked={ingredient === "mod"}
+ />
+ <label htmlFor="ingredient3" className="ml-2">
+ 模组
+ </label>
+ </div>
+ <div className="flex align-items-center">
+ <RadioButton
+ inputId="ingredient4"
+ name="pizza"
+ value="map"
+ onChange={(e: RadioButtonChangeEvent) => {
+ setResourceFormData((prev) => ({
+ ...prev, // 复制顶层所有属性
+ resource: {
+ ...prev.resource, // 复制resource对象的所有属性
+ classify: e.target.value,
+ },
+ }));
+ setIngredient(e.value);
+ }}
+ checked={ingredient === "map"}
+ />
+ <label htmlFor="ingredient4" className="ml-2">
+ 地图
+ </label>
+ </div>
+ </div>
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label htmlFor="gameplayList">资源标签</label>
+ </div>
+ <MultiSelect
+ value={selectedGameplay}
+ onChange={(e: MultiSelectChangeEvent) => {
+ const selectedOptions = e.value as GameplayOption[];
+ // 提取选中项的 name 属性组成字符串数组
+ const selectedNames = selectedOptions.map((item) => item.name);
+
+ setResourceFormData((prev) => ({
+ ...prev,
+ gameplayList: selectedNames,
+ }));
+ setSelectedGameplay(selectedOptions);
+ }}
+ options={gameplayOptions}
+ display="chip"
+ optionLabel="name"
+ placeholder="请选择资源标签"
+ className="w-full md:w-20rem"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label>封面图片</label>
+ </div>
+ <FileUpload
+ mode="advanced"
+ name="resource-image"
+ customUpload
+ uploadHandler={async (e) => {
+ const formData = new FormData();
+ formData.append("file", e.files[0]);
+
+ try {
+ const res = await axios.post(
+ `${process.env.PUBLIC_URL}/file`,
+ formData
+ );
+
+ const fileUrl = res.data.url;
+ console.log(fileUrl);
+ setResourcePictureUrl(fileUrl);
+ toast.current?.show({
+ severity: "success",
+ summary: "上传成功",
+ });
+ } catch (error) {
+ console.log(error);
+ toast.current?.show({
+ severity: "error",
+ summary: "上传失败",
+ });
+ }
+ }}
+ auto
+ chooseLabel="上传资源封面"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label>上传资源文件</label>
+ </div>
+ <FileUpload
+ mode="advanced"
+ name="resource-file"
+ customUpload
+ uploadHandler={async (e) => {
+ const formData = new FormData();
+ formData.append("file", e.files[0]);
+
+ try {
+ const res = await axios.post(
+ `${process.env.PUBLIC_URL}/file`,
+ formData
+ );
+ const fileUrl = res.data;
+ console.log("上传的 torrent 文件 URL:", fileUrl);
+ setTorrentUrl(fileUrl);
+ toast.current?.show({
+ severity: "success",
+ summary: "上传成功",
+ detail: "资源文件已上传",
+ });
+ } catch (error) {
+ console.error("上传资源文件失败:", error);
+ toast.current?.show({
+ severity: "error",
+ summary: "上传失败",
+ detail: "资源文件上传失败",
+ });
+ }
+ }}
+ auto
+ chooseLabel="上传资源文件(.torrent)"
+ />
+ </div>
+ </div>
+ </Dialog>
+
+ {/*删除悬赏弹窗*/}
+ <Dialog
+ header="删除悬赏"
+ visible={deleteVisible}
+ onHide={() => setDeleteVisible(false)}
+ className="resource-delete-dialog"
+ modal
+ footer={
+ <div className="dialog-footer">
+ <Button
+ label="确认"
+ icon="pi pi-check"
+ onClick={handleDeleteSubmit}
+ autoFocus
+ />
+ <Button
+ label="取消"
+ icon="pi pi-times"
+ onClick={() => setDeleteVisible(false)}
+ className="p-button-text"
+ />
+ </div>
+ }
+ >
+ <div className="dialog-form">
+ <span style={{ marginBottom: "10px" }}>确认是否删除该悬赏?</span>
+ </div>
+ </Dialog>
+
+ {/*编辑资源弹窗*/}
+ <Dialog
+ header="编辑资源"
+ visible={editVisible}
+ onHide={() => setEditVisible(false)}
+ className="resource-edit-dialog"
+ modal
+ footer={
+ <div className="dialog-footer">
+ <Button
+ label="确认"
+ icon="pi pi-check"
+ onClick={handleEditSubmit}
+ autoFocus
+ />
+ <Button
+ label="取消"
+ icon="pi pi-times"
+ onClick={() => setEditVisible(false)}
+ className="p-button-text"
+ />
+ </div>
+ }
+ >
+ <div className="dialog-form">
+ <div className="form-field">
+ <div className="form-field-header">
+ <label htmlFor="name">更改标题</label>
+ </div>
+ <InputText
+ id="name"
+ value={editRewardFormData.rewardName}
+ onChange={(e) =>
+ setEditRewardFormData((prev) => ({
+ ...prev,
+ rewardName: e.target.value,
+ }))
+ }
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <label htmlFor="price">更改定价</label>
+ </div>
+ <InputText
+ id="price"
+ value={editRewardFormData.price}
+ onChange={(e) =>
+ setEditRewardFormData((prev) => ({
+ ...prev,
+ price: e.target.value,
+ }))
+ }
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <label htmlFor="description">更改需求</label>
+ </div>
+ <InputTextarea
+ id="description"
+ value={editRewardFormData.rewardDescription}
+ onChange={(e) =>
+ setEditRewardFormData((prev) => ({
+ ...prev,
+ rewardDescription: e.target.value,
+ }))
+ }
+ rows={5}
+ className="w-full"
+ />
+ </div>
+ <div className="form-field">
+ <div className="form-field-header">
+ <span className="form-field-sign">*</span>
+ <label>封面图片</label>
+ </div>
+ <FileUpload
+ mode="advanced"
+ name="reward-image"
+ customUpload
+ uploadHandler={async (e) => {
+ const formData = new FormData();
+ formData.append("file", e.files[0]);
+
+ try {
+ const res = await axios.post(
+ `${process.env.PUBLIC_URL}/file`,
+ formData
+ );
+
+ const fileUrl = res.data.url;
+ console.log(fileUrl);
+ setRewardPictureUrl(fileUrl);
+ toast.current?.show({
+ severity: "success",
+ summary: "上传成功",
+ });
+ } catch (error) {
+ console.log(error);
+ toast.current?.show({
+ severity: "error",
+ summary: "上传失败",
+ });
+ }
+ }}
+ auto
+ accept="image/*"
+ chooseLabel="选择悬赏封面"
+ />
+ </div>
+ </div>
+ </Dialog>
+ </div>
+ );
+}
diff --git a/src/app/user/purchased-resources/page.tsx b/src/app/user/purchased-resources/page.tsx
index c48b626..11284ff 100644
--- a/src/app/user/purchased-resources/page.tsx
+++ b/src/app/user/purchased-resources/page.tsx
@@ -1,12 +1,169 @@
'use client';
-import React from 'react';
+import React, { useEffect, useState, useRef } from "react";
+import { Card } from 'primereact/card';
+import { Image } from 'primereact/image';
-const EmptyPage: React.FC = () => {
+import { Dialog } from 'primereact/dialog';
+// 页面跳转
+import { useRouter } from 'next/navigation';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+
+import { Button } from 'primereact/button';
+// 接口传输
+import axios from 'axios';
+// 标签
+import { Tag } from 'primereact/tag';
+
+
+import { useLocalStorage } from '../../hook/useLocalStorage';
+
+import "./purchased-resource.scss"
+interface User {
+ Id: number;
+}
+// 热门资源数据
+interface Torrent {
+ torrentRecordId: number;
+ torrentUrl: string;
+ infoHash: string;
+ uploadTime: string;
+ uploaderUserId: number;
+}
+
+interface ResourceVersion {
+ resourceVersionId: number;
+ resourceVersionName: string;
+ compatibleVersions: string[];
+ torrentList: Torrent[];
+ seeds: number;
+}
+
+interface Resource {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ resourceSummary: string;
+ lastUpdateTime: string;
+ hot: number;
+ gameplayList: string[];
+ resourceVersionList: ResourceVersion[];
+}
+interface ResourceList {
+ records: Resource[];
+ total: number;
+}
+
+export default function PurchasedResource() {
+ const user = useLocalStorage<User>('user');
+ const userId: number = user?.Id ?? -1;
+ // 热门资源列表
+ const [resources, setResources] = useState<Resource[]>([]);
+ const [totalResource, setTotalResource] = useState(0);
+ const [visible, setVisible] = useState<boolean>(false);
+ const [selectedResource, setSelectedResource] = useState<Resource | null>(null);
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ const router = useRouter();
+
+ // 分页
+ const [first, setFirst] = useState(0);
+ const [rows, setRows] = useState(5);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+ // 获取帖子列表
+ useEffect(() => {
+ handleSearch();
+ }, [first, rows]);
+
+ const handleSearch = async () => {
+
+ try {
+ const pageNumber = first / rows + 1;
+ const response = await axios.get<ResourceList>(process.env.PUBLIC_URL + `/user/purchase`, {
+ params: {
+ userId,
+ pageNumber,
+ rows
+ }
+ });
+ console.log(response.data.records);
+ setResources(response.data.records);
+ setTotalResource(response.data.total);
+ } catch (err) {
+ console.error('搜索资源失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '搜索资源失败' });
+ }
+ };
+
+
return (
- <div className="p-d-flex p-jc-center p-ai-center" style={{ height: '100vh' }}>
- {"一个空页面"}
+ <div className="PurchasedResource">
+ <div className="header">
+ <h1>我的资源库</h1>
+ </div>
+ {/* 全部社区 */}
+ <div className="all-resources-list">
+ {resources.map((resource) => (
+ <Card key={resource.resourceId} className="all-resources-card">
+ <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + "Resource/" + resource.resourcePicture} className="resource-avatar" width="250" height="140" onClick={() => router.push(`/resource/resource-detail/${resource.resourceId}`)} />
+ <div className="resource-header">
+ <div className="resource-content">
+ <h3>{resource.resourceName}</h3>
+ <div className="tags">
+ {resource.gameplayList.map((tag, index) => (
+ <Tag key={index} value={tag} />
+ ))}
+ </div>
+ </div>
+ <div className="resources-states">
+ <Button
+ label="下载"
+ className="classificationButton"
+ onClick={() => {
+ setSelectedResource(resource);
+ setVisible(true);
+ }}
+ />
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ {totalResource > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalResource} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
+ <Dialog
+ header="选择版本并下载"
+ visible={visible}
+ style={{ width: '50vw' }}
+ onHide={() => {
+ setVisible(false);
+ setSelectedResource(null);
+ }}
+ >
+ {selectedResource?.resourceVersionList.map((version) => (
+ <div key={version.resourceVersionId} style={{ marginBottom: '1.5rem' }}>
+ <h4>{version.resourceVersionName}</h4>
+ <p>兼容版本: {version.compatibleVersions.join(', ')}</p>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
+ {version.torrentList.map((torrent) => (
+ <Button
+ key={torrent.torrentRecordId}
+ label={`种子:${torrent.torrentUrl}`}
+ onClick={() => {
+ window.open(torrent.torrentUrl, '_blank');
+ }}
+ className="p-button-sm"
+ />
+ ))}
+ </div>
+ </div>
+ ))}
+ </Dialog>
</div>
);
};
-export default EmptyPage;
diff --git a/src/app/user/purchased-resources/purchased-resource.scss b/src/app/user/purchased-resources/purchased-resource.scss
new file mode 100644
index 0000000..5e466b6
--- /dev/null
+++ b/src/app/user/purchased-resources/purchased-resource.scss
@@ -0,0 +1,5 @@
+.PurchasedResource {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
\ No newline at end of file