add mainView, reward, community pages
Change-Id: I70da6ed3e91ebf4124c2074b6508192a19ed9909
diff --git "a/src/app/community/community-detail/\133communityId\135/page.tsx" "b/src/app/community/community-detail/\133communityId\135/page.tsx"
index 40200f7..850c54e 100644
--- "a/src/app/community/community-detail/\133communityId\135/page.tsx"
+++ "b/src/app/community/community-detail/\133communityId\135/page.tsx"
@@ -88,7 +88,7 @@
useEffect(() => {
const fetchThreadInfo = async () => {
try {
- const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/community/info?communityId=${communityId}`);
+ const { data } = await axios.get(process.env.PUBLIC_URL + `/community/info?communityId=${communityId}`);
setCommunityInfo(data);
setTotalThreads(data.threadNumber);
} catch (err) {
@@ -106,12 +106,12 @@
const fetchThreads = async () => {
try {
- const page = first / rows + 1;
- console.log("当前页" + page + "size" + rows + "搜索内容" + searchValue);
+ const pageNumber = first / rows + 1;
+ console.log("当前页" + pageNumber + "size" + rows + "搜索内容" + searchValue);
const option = selectedOption.name // 添加排序参数
const response = await axios.get<ThreadList>(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/community/threads`, {
- params: { communityId, page, rows, option, searchValue }
+ process.env.PUBLIC_URL + `/community/threads`, {
+ params: { communityId, pageNumber, rows, option, searchValue }
}
);
console.log('获取帖子列表:', response.data.records);
@@ -147,7 +147,7 @@
communityId: communityId // 从URL参数获取的社区ID
};
// 发送POST请求
- const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/thread', postData);
+ const response = await axios.post(process.env.PUBLIC_URL + '/thread', postData);
if (response.status === 200) {
toast.current?.show({ severity: 'success', summary: 'Success', detail: '帖子发布成功' });
@@ -187,9 +187,8 @@
</div>
</div>
<div className="input">
- <Button label="返回列表" link onClick={() => router.push(`/community/resource-community-list/${communityInfo?.type}`)} />
<div className="action-section">
- <div className="communities-searchBar">
+ <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>
@@ -274,7 +273,7 @@
<FileUpload
mode="basic"
name="thread-image"
- url="/file" // 与后端交互的URL
+ url={process.env.PUBLIC_URL + "/file"} // 与后端交互的URL
accept="image/*"
maxFileSize={10000000000}
chooseLabel="选择图片"
diff --git "a/src/app/community/community-detail/\133communityId\135/resource-community.scss" "b/src/app/community/community-detail/\133communityId\135/resource-community.scss"
index a40ebaa..543a846 100644
--- "a/src/app/community/community-detail/\133communityId\135/resource-community.scss"
+++ "b/src/app/community/community-detail/\133communityId\135/resource-community.scss"
@@ -76,10 +76,10 @@
display: flex;
align-items: center;
- .communities-searchBar {
+ .searchBar {
max-width: 100%;
position: relative;
-
+ border-radius: 0px 10px 10px 0px;
.pi-search {
position: absolute;
left: 1rem;
diff --git a/src/app/community/community.scss b/src/app/community/community.scss
index 42bfda1..5762301 100644
--- a/src/app/community/community.scss
+++ b/src/app/community/community.scss
@@ -9,7 +9,7 @@
// 热门社区样式
.hot-communities {
margin: 3rem 0;
-
+ cursor: pointer;
h1 {
margin-bottom: 1.5rem;
text-align: center; // 页面居中
@@ -121,7 +121,7 @@
border-radius: 0.5rem;
transition: transform 0.3s ease;
box-shadow: none !important; // 取消阴影
-
+ cursor: pointer;
//填充卡片
&.p-card.p-component {
padding: 0;
diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx
index a47cb47..764e358 100644
--- a/src/app/community/page.tsx
+++ b/src/app/community/page.tsx
@@ -55,7 +55,7 @@
useEffect(() => {
const fetchHotCommunity = async () => {
try {
- const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/community/hot`);
+ const { data } = await axios.get(process.env.PUBLIC_URL + `/community/hot`);
setHotCommunities(data.communityList);
} catch (err) {
console.error(err);
@@ -69,7 +69,7 @@
useEffect(() => {
const fetchCommunity = async () => {
try {
- const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/community/common`);
+ const { data } = await axios.get(process.env.PUBLIC_URL + `/community/common`);
setCommunities(data.communityList);
console.log(data.communityList);
} catch (err) {
@@ -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={process.env.NEXT_PUBLIC_NGINX_URL + item.communityPicture} alt={item.communityName} height="200" className="w-full h-48 object-cover" />
</div>
}
>
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 a34fc07..16f6bd6 100644
--- "a/src/app/community/resource-community-list/\133category\135/page.tsx"
+++ "b/src/app/community/resource-community-list/\133category\135/page.tsx"
@@ -73,12 +73,12 @@
const fetchCommunities = async () => {
try {
- const page = first / rows + 1;
+ const pageNumber = first / rows + 1;
const type = category === 'all' ? '全部' : category;
- console.log("当前页" + page + "size" + rows + "type" + type + "searchValue" + searchValue);
+ console.log("当前页" + pageNumber + "size" + rows + "type" + type + "searchValue" + searchValue);
const response = await axios.get<CommunityList>(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/community`, {
- params: { type, page, rows, searchValue }
+ process.env.PUBLIC_URL + `/community`, {
+ params: { type, pageNumber, rows, searchValue }
}
);
console.log('获取社区列表:', response.data.records);
@@ -101,7 +101,7 @@
</div>
{/* 社区搜索栏 */}
- <div className="communities-searchBar">
+ <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>
diff --git "a/src/app/community/resource-community-list/\133category\135/resource-community-list.scss" "b/src/app/community/resource-community-list/\133category\135/resource-community-list.scss"
index c8a0018..9b863bf 100644
--- "a/src/app/community/resource-community-list/\133category\135/resource-community-list.scss"
+++ "b/src/app/community/resource-community-list/\133category\135/resource-community-list.scss"
@@ -17,6 +17,7 @@
padding: 1rem;
&-card {
+ cursor: pointer;
height: 140px;
padding: 1.5rem;
margin-bottom: 1rem;
diff --git "a/src/app/community/thread-detail/\133threadId\135/page.tsx" "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
index 2189402..89ae1e8 100644
--- "a/src/app/community/thread-detail/\133threadId\135/page.tsx"
+++ "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
@@ -7,7 +7,6 @@
import { InputText } from "primereact/inputtext";
// 页面跳转
import { useParams } from 'next/navigation'
-import { useRouter } from 'next/navigation';
// 接口传输
import axios from 'axios';
// 回复评论
@@ -69,7 +68,6 @@
// 获取URL参数,页面跳转
const params = useParams<{ threadId: string }>()
const threadId = decodeURIComponent(params.threadId); // 防止中文路径乱码
- const router = useRouter();
// 消息提醒
const toast = useRef<Toast>(null);
// 帖子信息
@@ -103,7 +101,7 @@
const fetchThreadInfo = async () => {
try {
- const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/thread?threadId=${threadId}`);
+ const { data } = await axios.get(process.env.PUBLIC_URL + `/thread?threadId=${threadId}`);
setThreadInfo(data);
setTotalComments(data.commentNumber);
} catch (err) {
@@ -116,7 +114,7 @@
useEffect(() => {
if (!threadInfo) return;
// 发帖人
- axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/user/info?userId=${threadInfo.userId}`)
+ axios.get(process.env.PUBLIC_URL + `/user/info?userId=${threadInfo.userId}`)
.then(res => setUserInfo(res.data))
.catch(console.error);
}, [threadInfo]);
@@ -127,7 +125,7 @@
if (!threadInfo.isLike) {
try {
const response = await axios.post(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/thread/like`, {
+ process.env.PUBLIC_URL + `/thread/like`, {
params: { threadId, userId: 22301145 }
}
);
@@ -143,7 +141,7 @@
} else {
try {
const response = await axios.delete(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/thread/like`, {
+ process.env.PUBLIC_URL + `/thread/like`, {
params: { threadId, userId: 22301145 }
}
);
@@ -180,11 +178,11 @@
// 获取评论列表
const fetchComments = async () => {
try {
- const page = first / rows + 1;
- console.log("当前页" + page + "size" + rows);
+ const pageNumber = first / rows + 1;
+ console.log("当前页" + pageNumber + "size" + rows);
const response = await axios.get<CommentList>(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/comments`, {
- params: { threadId, page, rows }
+ process.env.PUBLIC_URL + `/comments`, {
+ params: { threadId, pageNumber, rows }
}
);
console.log('获取评论列表:', response.data.records);
@@ -193,7 +191,7 @@
response.data.records.forEach(comment => {
if (comment.userId != null && !commentUserInfos.has(comment.userId)) {
axios.get<UserInfo>(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/user/info`,
+ process.env.PUBLIC_URL + `/user/info`,
{ params: { userId: comment.userId } }
).then(res => {
setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data));
@@ -220,7 +218,7 @@
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
};
- const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/comment', newComment);
+ const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
if (response.status === 200) {
toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' });
@@ -250,7 +248,7 @@
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
};
- const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/comment', newComment);
+ const response = await axios.post(process.env.PUBLIC_URL + '/comment', newComment);
if (response.status === 200) {
toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
@@ -272,7 +270,7 @@
try {
// 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
const response = await axios.delete(
- `http://127.0.0.1:4523/m1/6387307-6083949-default/comment?commentId=${commentId}`
+ process.env.PUBLIC_URL + `/comment?commentId=${commentId}`
);
if (response.status === 200) {
@@ -334,7 +332,6 @@
<div className="comments-section">
<div className="comments-header">
<h2>评论 ({totalComments})</h2>
- <Button label="返回社区" link onClick={() => router.push(`/community/community-detail/${threadInfo.communityId}`)} />
</div>
<div className="comments-input">
<Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
diff --git "a/src/app/community/thread-detail/\133threadId\135/thread.scss" "b/src/app/community/thread-detail/\133threadId\135/thread.scss"
index b2524f0..37cbe4d 100644
--- "a/src/app/community/thread-detail/\133threadId\135/thread.scss"
+++ "b/src/app/community/thread-detail/\133threadId\135/thread.scss"
@@ -77,125 +77,4 @@
}
}
}
-
- // 评论区域
- .comments-section {
- .comments-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- h2 {
- font-size: 1.5rem;
- color: #2d3748;
- margin-bottom: 1.5rem;
- }
- }
-
- // 评论输入区
- .comments-input {
- display: flex;
- align-items: center;
- gap: 1rem;
- padding: 1rem;
- border-radius: 0.5rem;
-
- .p-inputtext {
- flex: 1; // 输入框占据剩余空间
- height: 3rem;
- }
-
- .p-button {
- height: 3rem;
- }
- }
-
- // 评论列表
- .comments-list {
- display: flex;
- flex-direction: column;
- margin-top: 1rem;
- gap: 0.5rem;
-
- .comment-item {
- padding: 1.5rem;
- border-radius: 0.5rem;
-
- .comment-user {
- display: flex;
- align-items: center;
- gap: 1rem;
- margin-bottom: 1rem;
-
- .comment-meta {
- display: flex;
- align-items: center;
- width: 100%;
- justify-content: space-between;
- gap: 0.5rem;
-
- .comment-time {
- justify-content: space-between;
- gap: 0.75rem
- }
-
- .username {
- margin-left: 0.5rem;
- font-weight: 600;
- color: #2d3748;
- }
-
- .floor {
- color: #718096;
- margin-right: 0.75rem;
- font-size: 0.875rem;
- }
-
- .time {
- color: #a0aec0;
- font-size: 0.875rem;
- }
- }
- }
-
- .comment-content {
- padding-left: 3.5rem;
-
- .reply-to {
- display: inline-block;
- color: #93C4C1;
- font-size: 0.875rem;
- margin-bottom: 0.5rem;
- }
-
- p {
- color: #4a5568;
- margin: 0;
- line-height: 1.5;
- }
- }
- }
- }
- }
-}
-
-.p-sidebar-header,
-.p-sidebar-custom-header {
- padding: 10px !important;
-}
-
-.p-overlaypanel-content {
- padding: 0 !important;
-}
-
-.reply {
-
- .reply-input {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-self: center;
- gap: 3rem;
- padding: 1rem;
- }
}
\ No newline at end of file
diff --git a/src/app/globals.scss b/src/app/globals.scss
index 77fe0da..097d350 100644
--- a/src/app/globals.scss
+++ b/src/app/globals.scss
@@ -54,9 +54,11 @@
display: flex;
align-items: center;
gap: 2rem;
+
.no-underline {
text-decoration: none;
}
+
.tool-item {
display: flex;
flex-direction: column;
@@ -82,7 +84,7 @@
.logo-name {
display: flex;
align-items: center;
- gap: 0.5rem;
+ gap: 1rem;
.logo {
width: 40px;
@@ -90,7 +92,7 @@
}
.name {
- font-size: 1.25rem;
+ font-size: 2rem;
font-weight: bold;
}
}
@@ -145,7 +147,7 @@
// 搜索栏样式
-.communities-searchBar {
+.searchBar {
max-width: 600px;
margin: 2rem auto;
position: relative;
@@ -167,4 +169,125 @@
font-size: 1.1rem;
border: 1px solid #ddd;
}
+}
+
+// 评论区域
+.comments-section {
+ .comments-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ h2 {
+ font-size: 1.5rem;
+ color: #2d3748;
+ margin-bottom: 1.5rem;
+ }
+ }
+
+ // 评论输入区
+ .comments-input {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ border-radius: 0.5rem;
+
+ .p-inputtext {
+ flex: 1; // 输入框占据剩余空间
+ height: 3rem;
+ }
+
+ .p-button {
+ height: 3rem;
+ }
+ }
+
+ // 评论列表
+ .comments-list {
+ display: flex;
+ flex-direction: column;
+ margin-top: 1rem;
+ gap: 0.5rem;
+
+ .comment-item {
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+
+ .comment-user {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+
+ .comment-meta {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ justify-content: space-between;
+ gap: 0.5rem;
+
+ .comment-time {
+ justify-content: space-between;
+ gap: 0.75rem
+ }
+
+ .username {
+ margin-left: 0.5rem;
+ font-weight: 600;
+ color: #2d3748;
+ }
+
+ .floor {
+ color: #718096;
+ margin-right: 0.75rem;
+ font-size: 0.875rem;
+ }
+
+ .time {
+ color: #a0aec0;
+ font-size: 0.875rem;
+ }
+ }
+ }
+
+ .comment-content {
+ padding-left: 3.5rem;
+
+ .reply-to {
+ display: inline-block;
+ color: #93C4C1;
+ font-size: 0.875rem;
+ margin-bottom: 0.5rem;
+ }
+
+ p {
+ color: #4a5568;
+ margin: 0;
+ line-height: 1.5;
+ }
+ }
+ }
+ }
+}
+
+.p-sidebar-header,
+.p-sidebar-custom-header {
+ padding: 10px !important;
+}
+
+.p-overlaypanel-content {
+ padding: 0 !important;
+}
+
+.reply {
+
+ .reply-input {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-self: center;
+ gap: 3rem;
+ padding: 1rem;
+ }
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 68e7de7..8d2a66a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -9,7 +9,7 @@
import 'primeicons/primeicons.css';
import 'primereact/resources/themes/lara-light-teal/theme.css'; // 主题
// 全局样式
-import './globals.scss';
+import './globals.scss';
//设置标题
export const metadata: Metadata = {
@@ -33,7 +33,13 @@
<span className="name">MCPT</span>
</div>
<div className="tools">
- <Link href="/classification" className="no-underline">
+ <Link href="/resource/hot-resource" className="no-underline">
+ <div className="tool-item">
+ <i className="pi pi-trophy" />
+ <span>热门</span>
+ </div>
+ </Link>
+ <Link href="/search" className="no-underline">
<div className="tool-item">
<i className="pi pi-search" />
<span>搜索</span>
@@ -47,7 +53,7 @@
</div>
</Link>
- <Link href="/classification" className="no-underline">
+ <Link href="/resource/classification" className="no-underline">
<div className="tool-item">
<i className="pi pi-tags" />
<span>分类</span>
diff --git a/src/app/main.scss b/src/app/main.scss
new file mode 100644
index 0000000..90bf67d
--- /dev/null
+++ b/src/app/main.scss
@@ -0,0 +1,214 @@
+.Home {
+ padding: 2rem;
+ position: relative;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+.main-header {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+ margin: 0 auto;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 20px;
+}
+
+.carousel-wrapper {
+ flex: 1;
+
+ .custom-carousel {
+ position: relative;
+
+ .carousel-item {
+ max-width: 100px;
+
+ img {
+ border-radius: 8px;
+ object-fit: cover;
+ }
+ }
+
+ .p-carousel-prev,
+ .p-carousel-next {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 10;
+ /* 保证在图片之上 */
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ /* 确保按钮可点击 */
+ }
+
+ /* 左箭头靠左 */
+ .p-carousel-prev {
+ left: 0.5rem;
+ }
+
+ /* 右箭头靠右 */
+ .p-carousel-next {
+ right: 0.5rem;
+ }
+ }
+}
+
+.rewards-panel {
+ display: flex;
+ flex-direction: column;
+ height: 350px;
+ width: 400px;
+ background: #fff;
+ border-radius: 10px;
+ padding: 1rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+ h2 {
+ color: #526665;
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid #eee;
+ text-align: center;
+ }
+
+ .rewards-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ .reward-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem;
+ background: #f8f9fa;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: #f0f4f7;
+ transform: translateY(-2px);
+ }
+
+ .reward-title {
+ font-size: 0.7rem;
+ color: #333;
+ }
+
+ .reward-price {
+ font-weight: bold;
+ color: #526665;
+ }
+ }
+ }
+
+ .view-all-button {
+ width: 60%;
+ margin-left: 4.5rem;
+ margin-top: 2rem;
+ }
+}
+
+.site-stats {
+ max-width: 1200px;
+ margin: 2rem auto;
+ padding: 1rem;
+ background-color: #fff;
+ border-radius: 8px;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+ .stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+
+ .stat-value {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .stat-label {
+ font-size: 0.875rem;
+ color: #666;
+ }
+ }
+}
+
+.resource-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1rem;
+
+ .resource-card {
+ transition: transform 0.2s ease;
+ cursor: pointer;
+ box-shadow: none !important;
+
+ .p-image {
+ img {
+ border-radius: 0.5rem 0.5rem 0 0;
+ object-fit: cover;
+ }
+ }
+
+ .p-card-body {
+ padding: 0;
+ }
+
+ .p-card-content {
+ padding: 0;
+ }
+
+ &:hover {
+ transform: translateY(-4px);
+ }
+
+ .card-content {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ margin-left: 16px;
+ margin-right: 16px;
+ margin-bottom: 16px;
+
+ h3 {
+ margin: 1rem 0;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #2d3748;
+ }
+
+ .view-count {
+ position: absolute;
+ bottom: 0rem;
+ right: 0rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #718096;
+ font-size: 0.9rem;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/page.module.css b/src/app/page.module.css
deleted file mode 100644
index a11c8f3..0000000
--- a/src/app/page.module.css
+++ /dev/null
@@ -1,168 +0,0 @@
-.page {
- --gray-rgb: 0, 0, 0;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
-
- --button-primary-hover: #383838;
- --button-secondary-hover: #f2f2f2;
-
- display: grid;
- grid-template-rows: 20px 1fr 20px;
- align-items: center;
- justify-items: center;
- min-height: 100svh;
- padding: 80px;
- gap: 64px;
- font-family: var(--font-geist-sans);
-}
-
-@media (prefers-color-scheme: dark) {
- .page {
- --gray-rgb: 255, 255, 255;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
-
- --button-primary-hover: #ccc;
- --button-secondary-hover: #1a1a1a;
- }
-}
-
-.main {
- display: flex;
- flex-direction: column;
- gap: 32px;
- grid-row-start: 2;
-}
-
-.main ol {
- font-family: var(--font-geist-mono);
- padding-left: 0;
- margin: 0;
- font-size: 14px;
- line-height: 24px;
- letter-spacing: -0.01em;
- list-style-position: inside;
-}
-
-.main li:not(:last-of-type) {
- margin-bottom: 8px;
-}
-
-.main code {
- font-family: inherit;
- background: var(--gray-alpha-100);
- padding: 2px 4px;
- border-radius: 4px;
- font-weight: 600;
-}
-
-.ctas {
- display: flex;
- gap: 16px;
-}
-
-.ctas a {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- border: 1px solid transparent;
- transition:
- background 0.2s,
- color 0.2s,
- border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
-}
-
-a.primary {
- background: var(--foreground);
- color: var(--background);
- gap: 8px;
-}
-
-a.secondary {
- border-color: var(--gray-alpha-200);
- min-width: 158px;
-}
-
-.footer {
- grid-row-start: 3;
- display: flex;
- gap: 24px;
-}
-
-.footer a {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.footer img {
- flex-shrink: 0;
-}
-
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- a.primary:hover {
- background: var(--button-primary-hover);
- border-color: transparent;
- }
-
- a.secondary:hover {
- background: var(--button-secondary-hover);
- border-color: transparent;
- }
-
- .footer a:hover {
- text-decoration: underline;
- text-underline-offset: 4px;
- }
-}
-
-@media (max-width: 600px) {
- .page {
- padding: 32px;
- padding-bottom: 80px;
- }
-
- .main {
- align-items: center;
- }
-
- .main ol {
- text-align: center;
- }
-
- .ctas {
- flex-direction: column;
- }
-
- .ctas a {
- font-size: 14px;
- height: 40px;
- padding: 0 16px;
- }
-
- a.secondary {
- min-width: auto;
- }
-
- .footer {
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .logo {
- filter: invert();
- }
-}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 84af2cb..a994e7c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,95 +1,396 @@
-import Image from "next/image";
-import styles from "./page.module.css";
+'use client';
+import React, { useEffect, useState, useRef } from "react";
+import { Button } from 'primereact/button';
+import { Card } from 'primereact/card';
+import { Image } from 'primereact/image';
+import { Carousel } from 'primereact/carousel';
+// 页面跳转
+import { useRouter } from 'next/navigation';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 评分图标
+import { Fire } from '@icon-park/react';
+// 接口传输
+import axios from 'axios';
+
+// 样式
+import './main.scss';
+// 模组列表数据
+interface Mod {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ likes: number;
+}
+interface ModList {
+ records: Mod[];
+}
+// 地图列表数据
+interface Map {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ likes: number;
+}
+interface MapList {
+ records: Mod[];
+}
+// 帖子列表数据
+interface Modpack {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ likes: number;
+}
+interface ModpackList {
+ records: Modpack[];
+}
+// 帖子列表数据
+interface Texture {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ likes: number;
+}
+interface TextureList {
+ records: Texture[];
+}
+// 热门资源幻灯片数据
+interface HotResource {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ likes: number;
+ dowloads: number;
+ seeds: number;
+}
+interface HotResourceList {
+ records: HotResource[];
+}
+// 站点统计数据
+interface Stats {
+ threadCount: number;
+ downloadCount: number;
+ authorCount: number;
+ resourceCount: number;
+}
+// 悬赏列表
+interface Reward {
+ rewardId: number;
+ userId: number;
+ rewardPicture: string;
+ rewardName: string;
+ createdAt: string;
+ rewardDescription: string;
+ price: number;
+}
+interface RewardList {
+ total: number; // 总记录数
+ records: Reward[];
+}
+// 主页
export default function Home() {
- return (
- <div className={styles.page}>
- <main className={styles.main}>
- <Image
- className={styles.logo}
- src="/next.svg"
- alt="Next.js logo"
- width={180}
- height={38}
- priority
- />
- <ol>
- <li>
- Get started by editing <code>src/app/page.tsx</code>.
- </li>
- <li>Save and see your changes instantly.</li>
- </ol>
+ // 模组列表
+ const [mods, setMods] = useState<Mod[]>([]);
+ // 悬赏列表
+ const [reward, setReweard] = useState<Reward[]>([]);
+ // 地图列表
+ const [maps, setMaps] = useState<Map[]>([]);
+ // 整合包列表
+ const [modpacks, setModpacks] = useState<Modpack[]>([]);
+ // 材质包列表
+ const [textures, setTextures] = useState<Texture[]>([]);
+ // 热门资源列表
+ const [hotResources, setHotResources] = useState<HotResource[]>([]);
+ // 站点统计信息
+ const [stats, setStats] = useState<Stats>({
+ threadCount: 0,
+ downloadCount: 0,
+ authorCount: 0,
+ resourceCount: 0
+ });
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ const router = useRouter();
- <div className={styles.ctas}>
- <a
- className={styles.primary}
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- className={styles.logo}
- src="/vercel.svg"
- alt="Vercel logomark"
- width={20}
- height={20}
- />
- Deploy now
- </a>
- <a
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- className={styles.secondary}
- >
- Read our docs
- </a>
+
+ // 获取帖子列表
+ useEffect(() => {
+ fetchHotResources();
+ fetchRewards();
+ fetchstats();
+ fetchRecommendMods();
+ fetchRecommendMaps();
+ fetchRecommendModpacks();
+ fetchRecommendTextures();
+ }, []);
+
+ // 获取悬赏列表
+ const fetchRewards = async () => {
+ try {
+ const response = await axios.get<RewardList>(process.env.PUBLIC_URL +`/reward`, {
+ params: { pageNumber: 1, rows: 5, searchValue: '', option: '' }
+ });
+ console.log('获取悬赏列表:', response.data.records);
+ setReweard(response.data.records);
+ } catch (err) {
+ console.error('获取悬赏失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取悬赏失败' });
+ }
+ };
+
+ // 获取热门资源幻灯片
+ const fetchHotResources = async () => {
+ try {
+ const response = await axios.get<HotResourceList>(process.env.PUBLIC_URL +`/resource/hot`, {
+ params: { pageNumber: 1, rows: 3, searchValue: '', type: '' }
+ });
+ console.log('获取热门社区幻灯片:', response.data.records);
+ setHotResources(response.data.records);
+ } catch (err) {
+ console.error('获取Mod失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取Mod失败' });
+ }
+ };
+
+ // 获取全站数据
+ const fetchstats = async () => {
+ try {
+ const response = await axios.get<Stats>(process.env.PUBLIC_URL +`/total/info`);
+ console.log('获取全站数据:', response.data);
+ setStats(response.data);
+ } catch (err) {
+ console.error('获取数据失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取数据失败' });
+ }
+ };
+
+
+ // 获取推荐模组、地图、整合包、材质包
+ const fetchRecommendMods = async () => {
+ try {
+ const response = await axios.get<ModList>(process.env.PUBLIC_URL +`/resource/recommend`, {
+ params: { userId: 22301145, pageNumber: 1, rows: 3, type: '模组' }
+ });
+ console.log('获取模组列表:', response.data.records);
+ setMods(response.data.records);
+ } catch (err) {
+ console.error('获取Mod失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取Mod失败' });
+ }
+ };
+ const fetchRecommendMaps = async () => {
+ try {
+ const response = await axios.get<MapList>(process.env.PUBLIC_URL +`/resource/recommend`, {
+ params: { userId: 22301145, pageNumber: 1, rows: 3, type: '地图' }
+ });
+ console.log('获取模组列表:', response.data.records);
+ setMaps(response.data.records);
+ } catch (err) {
+ console.error('获取地图失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取地图失败' });
+ }
+ };
+ const fetchRecommendModpacks = async () => {
+ try {
+ const response = await axios.get<ModpackList>(process.env.PUBLIC_URL +`/resource/recommend`, {
+ params: { userId: 22301145, pageNumber: 1, rows: 3, type: '整合包' }
+ });
+ console.log('获取模组列表:', response.data.records);
+ setModpacks(response.data.records);
+ } catch (err) {
+ console.error('获取整合包失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取整合包失败' });
+ }
+ };
+ const fetchRecommendTextures = async () => {
+ try {
+ const response = await axios.get<TextureList>(process.env.PUBLIC_URL +`/resource/recommend`, {
+ params: { userId: 22301145, pageNumber: 1, rows: 3, type: '材质包' }
+ });
+ console.log('获取模组列表:', response.data.records);
+ setTextures(response.data.records);
+ } catch (err) {
+ console.error('获取材质包失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取材质包失败' });
+ }
+ };
+ return (
+ <div className="Home">
+ {/* 轮播图部分 */}
+ <div className="main-header">
+ <div className="carousel-wrapper">
+ <Carousel
+ value={hotResources}
+ numVisible={1}
+ numScroll={1}
+ showIndicators={false}
+ showNavigators={true}
+ 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" />
+ </div>
+ )}
+ />
</div>
- </main>
- <footer className={styles.footer}>
- <a
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- aria-hidden
- src="/file.svg"
- alt="File icon"
- width={16}
- height={16}
+ <div className="rewards-panel">
+ <h2>悬赏排行</h2>
+ <div className="rewards-list">
+ {reward.map((item) => (
+ <div key={item.rewardId} className="reward-item" onClick={() => router.push(`/reward/reward-detail/${item.rewardId}`)}>
+ <span className="reward-title">{item.rewardName}</span>
+ <span className="reward-price">${item.price}</span>
+ </div>
+ ))}
+ </div>
+ <Button
+ label="查看全部悬赏"
+ className="view-all-button"
+ onClick={() => router.push('/reward')}
/>
- Learn
- </a>
- <a
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- aria-hidden
- src="/window.svg"
- alt="Window icon"
- width={16}
- height={16}
+ </div>
+ </div>
+ {/* 站点统计信息 */}
+ <div className="site-stats">
+ <div className="stat-item">
+ <span className="stat-value">{stats.threadCount}</span>
+ <span className="stat-label">总帖子数</span>
+ </div>
+ <div className="stat-item">
+ <span className="stat-value">{stats.downloadCount}</span>
+ <span className="stat-label">下载总数</span>
+ </div>
+ <div className="stat-item">
+ <span className="stat-value">{stats.authorCount}</span>
+ <span className="stat-label">人数统计</span>
+ </div>
+ <div className="stat-item">
+ <span className="stat-value">{stats.resourceCount}</span>
+ <span className="stat-label">内容总数</span>
+ </div>
+ </div>
+ {/* 推荐模组列表 */}
+ <div className="recommended-mods">
+ <div className="section-header">
+ <h1>推荐模组资源</h1>
+ <Button
+ label="显示更多"
+ link
+ onClick={() => router.push('/resource/recommend/模组')}
/>
- Examples
- </a>
- <a
- href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- aria-hidden
- src="/globe.svg"
- alt="Globe icon"
- width={16}
- height={16}
+ </div>
+ <div className="resource-grid">
+ {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}
+ alt={mod.resourceName}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{mod.resourceName}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{mod.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+ {/* 推荐地图列表 */}
+ <div className="recommended-maps">
+ <div className="section-header">
+ <h1>推荐地图资源</h1>
+ <Button
+ label="显示更多"
+ link
+ onClick={() => router.push('/resource/recommend/地图')}
/>
- Go to nextjs.org →
- </a>
- </footer>
+ </div>
+ <div className="resource-grid">
+ {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}
+ alt={map.resourceName}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{map.resourceName}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{map.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+ {/* 推荐材质包列表 */}
+ <div className="recommended-textures">
+ <div className="section-header">
+ <h1>推荐材质包资源</h1>
+ <Button
+ label="显示更多"
+ link
+ onClick={() => router.push('/resource/recommend/材质包')}
+ />
+ </div>
+ <div className="resource-grid">
+ {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}
+ alt={texture.resourceName}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{texture.resourceName}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{texture.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+ {/* 推荐整合包列表 */}
+ <div className="recommended-Modpacks">
+ <div className="section-header">
+ <h1>推荐整合包资源</h1>
+ <Button
+ label="显示更多"
+ link
+ onClick={() => router.push('/resource/recommend/整合包')}
+ />
+ </div>
+ <div className="resource-grid">
+ {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}
+ alt={modpack.resourceName}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{modpack.resourceName}</h3>
+ <div className="view-count">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>{modpack.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
</div>
);
}
diff --git a/src/app/reward/page.tsx b/src/app/reward/page.tsx
new file mode 100644
index 0000000..59abf97
--- /dev/null
+++ b/src/app/reward/page.tsx
@@ -0,0 +1,263 @@
+'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 { useRouter } from 'next/navigation';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 发布帖子
+import { Dialog } from 'primereact/dialog';
+import { FileUpload } from 'primereact/fileupload';
+import { InputTextarea } from 'primereact/inputtextarea';
+// 接口传输
+import axios from 'axios';
+// 防抖函数
+import { debounce } from 'lodash';
+import { TabView, TabPanel } from 'primereact/tabview';
+// 样式
+import './reward.scss';
+
+// 悬赏列表数据
+interface Reward {
+ rewardId: number;
+ userId: number;
+ rewardPicture: string;
+ rewardName: string;
+ createAt: string;
+ rewardDescription: string;
+ price: number;
+}
+interface RewardList {
+ total: number; // 总记录数
+ records: Reward[];
+}
+
+
+// 社区详情页面
+export default function RewardDetailPage() {
+ // 页面跳转
+ 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(() => {
+ 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: 22301145, // 记得用户登录状态获取
+ 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>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/reward/reward-detail/\133rewardId\135/page.tsx" "b/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
new file mode 100644
index 0000000..fb8f8b1
--- /dev/null
+++ "b/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
@@ -0,0 +1,364 @@
+'use client';
+
+import React, { useEffect, useState, useRef } from 'react';
+import { Image } from 'primereact/image';
+import { Avatar } from 'primereact/avatar';
+import { Button } from 'primereact/button';
+import { InputText } from "primereact/inputtext";
+// 页面跳转
+import { useParams } from 'next/navigation'
+// 接口传输
+import axios from 'axios';
+// 回复评论
+import { OverlayPanel } from 'primereact/overlaypanel';
+import { Sidebar } from 'primereact/sidebar';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 样式
+import './reward-detail.scss';
+
+
+// 评论信息
+interface Comment {
+ commentId: number;
+ userId: number | null;
+ replyId: number;
+ resourceId: number;
+ reawardId: number;
+ content: string;
+ createAt: string;
+}
+// 评论列表
+interface CommentList {
+ total: number; // 评论总数
+ records: Comment[]; // 当前页评论数组
+}
+// 悬赏信息
+interface RewardInfo {
+ rewardId: number;
+ userId: number;
+ rewardPicture: string;
+ rewardName: string;
+ rewardDescription: string;
+ price: number;
+ createAt: string;
+ lastUpdateAt: string;
+ commentNumber: number;
+ communityId: number;
+}
+// 用户信息
+interface UserInfo {
+ userId: number;
+ username: string;
+ avatar: string;
+ signature: string;
+}
+// 新评论接口
+interface NewComment {
+ userId: number;
+ threadId: number;
+ resourceId: number;
+ rewardId: number;
+ replyId: number;
+ content: string;
+ createAt: string;
+}
+
+
+//帖子详情界面
+export default function RewardDetailPage() {
+ // 获取URL参数,页面跳转
+ const params = useParams<{ rewardId: string }>()
+ const rewardId = decodeURIComponent(params.rewardId); // 防止中文路径乱码
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 帖子信息
+ const [rewardInfo, setRewardInfo] = useState<RewardInfo | null>(null);
+ // 发帖人信息
+ const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
+ // 评论人信息
+ const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map());
+ //评论
+ const [comments, setComments] = useState<Comment[]>([]);
+ const [commentValue, setCommentValue] = useState<string>('');
+ const [totalComments, setTotalComments] = useState<number>(0);
+ // 回复
+ const [replyValue, setReplyValue] = useState<string>('');
+ const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视
+ // 评论选择框
+ const ops = useRef<OverlayPanel[]>([]);
+
+ // 分页
+ const [first, setFirst] = useState<number>(0);
+ const [rows, setRows] = useState<number>(5);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+
+ // 获取帖子信息
+ useEffect(() => {
+ fetchRewardInfo();
+ }, [rewardId, rewardInfo]);
+
+ const fetchRewardInfo = async () => {
+ try {
+ const { data } = await axios.get(process.env.PUBLIC_URL +`/reward/info?rewardId=${rewardId}`);
+ setRewardInfo(data);
+ } catch (err) {
+ console.error(err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取悬赏信息失败' });
+ }
+ };
+
+ // 获取发帖人
+ useEffect(() => {
+ if (!rewardInfo) return;
+ // 发帖人
+ axios.get(process.env.PUBLIC_URL +`/user/info?userId=${rewardInfo.userId}`)
+ .then(res => setUserInfo(res.data))
+ .catch(console.error);
+ }, [rewardInfo]);
+
+
+ // 当 rewardId 或分页参数变化时重新拉评论
+ useEffect(() => {
+ if (!rewardId) return;
+
+ fetchComments();
+ }, [rewardId, first, rows]);
+
+
+ //通过评论ID获取评论人信息
+ const getReplyUserName = (replyId: number) => {
+ if (replyId == null || replyId == 0) return '';
+ const replyComment = comments.find(comment => comment.commentId === replyId);
+ if (!replyComment?.userId) return '匿名用户';
+ return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户';
+ };
+
+ // 获取评论列表
+ const fetchComments = async () => {
+ try {
+ const pageNumber = first / rows + 1;
+ console.log("当前页" + pageNumber + "size" + rows);
+ const response = await axios.get<CommentList>(
+ process.env.PUBLIC_URL +`/comments`, {
+ params: { id: rewardId, pageNumber, rows, type: 'reward' }
+ }
+ );
+ console.log('获取评论列表:', response.data.records);
+ setComments(response.data.records);
+ setTotalComments(response.data.total);
+ // 拉取评论对应用户信息
+ response.data.records.forEach(comment => {
+ if (comment.userId != null && !commentUserInfos.has(comment.userId)) {
+ axios.get<UserInfo>(
+ process.env.PUBLIC_URL +`/user/info`,
+ { params: { userId: comment.userId } }
+ ).then(res => {
+ setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data));
+ });
+ }
+ });
+ } catch (err) {
+ console.error('获取评论失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' });
+ }
+ };
+
+ // 回复评论接口
+ const publishReply = async (commentId: number) => {
+ if (!replyValue.trim() || !rewardInfo) return;
+ console.log('发布评论:', commentId);
+ try {
+ const newComment: NewComment = {
+ userId: 22301145,
+ rewardId: rewardInfo.rewardId,
+ threadId: 0,
+ resourceId: 0,
+ replyId: commentId,
+ content: commentValue,
+ createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ };
+
+ const response = await axios.post(process.env.PUBLIC_URL +'/comment', newComment);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' });
+ // 更新评论列表
+ fetchComments();
+ setVisibleReply(false)
+ // 清空输入框
+ setReplyValue('');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' });
+ }
+ };
+
+ // 发布评论接口
+ const publishComment = async () => {
+ if (!commentValue.trim() || !rewardInfo) return;
+
+ try {
+ const newComment: NewComment = {
+ userId: 22301145,
+ rewardId: rewardInfo.rewardId,
+ threadId: 0,
+ resourceId: 0,
+ replyId: 0, // 直接评论,不是回复
+ content: commentValue,
+ createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ };
+
+ const response = await axios.post(process.env.PUBLIC_URL +'/comment', newComment);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
+ // 更新评论列表
+ fetchComments();
+ // 清空输入框
+ setCommentValue('');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' });
+ }
+ };
+
+ // 删除评论接口
+ const deleteComment = async (commentId: number) => {
+ if (!rewardInfo) return;
+
+ try {
+ // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
+ const response = await axios.delete(
+ process.env.PUBLIC_URL +`/comment?commentId=${commentId}`
+ );
+
+ if (response.status === 200) {
+ fetchComments();
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' });
+ } else {
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' });
+ console.error('删除评论失败,状态码:', response.status);
+ }
+ } catch (error) {
+ console.error('删除评论接口报错:', error);
+ }
+ };
+
+ const ReplyHeader = (
+ <div className="flex align-items-center gap-1">
+ <h3>回复评论</h3>
+ </div>
+ );
+ if (!rewardInfo || !userInfo) return <div>Loading...</div>;
+ return (
+ <div className="reward-detail">
+ <Toast ref={toast}></Toast>
+ {/* 帖子头部 */}
+ <div className="reward-header">
+ <h1>{rewardInfo.rewardName}</h1>
+ <span className="post-time">{"最新更新时间:" + rewardInfo.lastUpdateAt}</span>
+ </div>
+
+ {/* 帖子内容 */}
+ <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" />
+ <div className="user-meta">
+ <h3>{userInfo.username}</h3>
+ <span>{userInfo.signature}</span>
+ </div>
+ </div>
+
+ {/* 左侧文字内容 */}
+ <div className="reward-info">
+ <p>{rewardInfo.rewardDescription}</p>
+ </div>
+ </div>
+
+
+ {/* 右侧图片+价格+按钮 */}
+ <div className="reward-media">
+ <Image
+ src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + rewardInfo.rewardPicture}
+ alt={rewardInfo.rewardName}
+ width="500"
+ height="400"
+ />
+ <div className="reward-actions">
+ <span className="reward-price">¥{rewardInfo.price}</span>
+ <Button className="submit-bounty">提交悬赏</Button>
+ </div>
+ </div>
+ </div>
+ {/* 评论列表 */}
+ <div className="comments-section">
+ <div className="comments-header">
+ <h2>评论 ({totalComments})</h2>
+ </div>
+ <div className="comments-input">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} />
+ <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} />
+ </div>
+ <div className="comments-list">
+ {comments.map((comment, index) => (
+ <div key={comment.commentId} className="comment-item">
+ <div className="comment-user">
+ <Avatar
+ image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+ size="normal"
+ shape="circle"
+ />
+ <div className="comment-meta">
+ <span className="username">
+ {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'}
+ </span>
+ <div className="comment-time">
+ <span className="floor">#{index + 1}楼</span>
+ <span className="time">{comment.createAt}</span>
+ </div>
+ </div>
+ <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} />
+ </div>
+ <div className="comment-content">
+ {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>}
+ <p>{comment.content}</p>
+ </div>
+ <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置
+ ref={el => {
+ if (el) ops.current[index] = el;
+ }}>
+ <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} />
+ {comment.userId === 22301145 &&
+ <Button
+ label="删除"
+ text
+ size="small"
+ onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }}
+ />
+ }
+ </OverlayPanel>
+ <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
+ <div className="reply-input">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} />
+ <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} />
+ </div>
+ </Sidebar>
+ </div>
+ ))}
+ {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
+ </div>
+ </div>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/reward/reward-detail/\133rewardId\135/reward-detail.scss" "b/src/app/reward/reward-detail/\133rewardId\135/reward-detail.scss"
new file mode 100644
index 0000000..a38cd0a
--- /dev/null
+++ "b/src/app/reward/reward-detail/\133rewardId\135/reward-detail.scss"
@@ -0,0 +1,93 @@
+.reward-detail {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+
+ // 把标题和用户信息、发布时间都放到同一个 flex 容器里
+ .reward-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+
+ h1 {
+ order: 1;
+ font-size: 2rem;
+ color: #1a202c;
+ margin: 0;
+ }
+
+ .post-time {
+ order: 3;
+ color: #718096;
+ font-size: 0.875rem;
+ }
+ }
+
+ // 贴子正文,文本在左,图片 + 价格 + 按钮在右
+ .reward-content {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ margin-top: 1rem;
+
+
+ .reward-info-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ width: 50%;
+
+ .user-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ // 文本部分
+ .reward-info {
+
+ p {
+ font-size: 1rem;
+ line-height: 1.75;
+ color: #4a5568;
+ margin-bottom: 2rem;
+ }
+ }
+ }
+
+
+
+
+ // 右侧媒体区:图片、价格、提交按钮
+ .reward-media {
+ display: flex;
+ width: 50%;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 2rem;
+
+ img {
+ margin-top: 1rem;
+ border-radius: 0.5rem 0.5rem 0.5rem 0.5rem;
+ }
+
+ .reward-actions {
+ min-width: 120px;
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+ margin-bottom: 2rem;
+
+ .reward-price {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #2c3e50;
+ margin-bottom: 0.5rem;
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/reward/reward.scss b/src/app/reward/reward.scss
new file mode 100644
index 0000000..377e588
--- /dev/null
+++ b/src/app/reward/reward.scss
@@ -0,0 +1,249 @@
+@import '../globals.scss';
+
+.reward {
+ padding: 2rem;
+ position: relative;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+ cursor: pointer;
+ .reward-header {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ .title-section {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ align-items: flex-start;
+
+ h1 {
+ font-size: 3rem;
+ margin: 0;
+ margin-top: 64px;
+ }
+
+ .subtitle {
+ margin: 0;
+ color: #718096;
+ font-size: 1rem;
+ }
+
+ .reward-states {
+ display: flex;
+ gap: 2rem;
+ align-items: center;
+
+ .state-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #666;
+
+ span {
+ font-size: 0.9rem;
+ }
+ }
+ }
+ }
+
+ .input {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ margin-top: 2rem;
+
+ .reward-buttons {
+ display: flex;
+ gap: 1rem;
+
+ .p-button {
+ width: 150px;
+ font-size: 0.9rem;
+ padding: 0.5rem 1rem;
+ border-radius: 8px;
+ }
+ }
+ }
+
+ .searchBar {
+ max-width: 100%;
+ position: relative;
+
+ .pi-search {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1;
+ }
+
+ .search-helper {
+ width: 316px;
+ height: 3rem;
+ border-radius: 10px 10px 10px 10px;
+ font-size: 1.1rem;
+ border: 1px solid #ddd;
+ }
+ }
+
+ .select-dropdown {
+ width: 100px;
+ height: 48px;
+ border-radius: 0px 10px 10px 0px;
+
+ .p-dropdown-items {
+ max-height: 20px;
+ }
+ }
+
+ }
+
+ // 全部社区样式
+ .rewards-list {
+ width: 100%;
+ padding: 1rem;
+
+ &-card {
+ height: 140px;
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+ transition: transform 0.3s ease;
+ box-shadow: none !important; // 取消阴影
+
+ //填充卡片
+ &.p-card.p-component {
+ padding: 0;
+ }
+
+ .p-card-body {
+ padding: 0;
+ }
+
+ &:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .p-card-content {
+ height: 140px;
+ display: flex;
+ justify-content: space-between;
+ padding: 0;
+ }
+
+ img {
+ border-radius: 0.5rem 0 0 0.5rem;
+ object-fit: cover;
+ }
+
+ .reward-header {
+ display: flex;
+ flex: 1;
+ max-width: 800px;
+ padding-left: 20px;
+ padding-right: 20px;
+ margin-bottom: 20px;
+ }
+
+
+ .reward-avatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 8px;
+ object-fit: cover;
+ }
+
+ .reward-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+
+ h3 {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #2c3e50;
+ }
+
+ .reward-introduction {
+ color: #666;
+ font-size: 1rem;
+ margin-bottom: 0;
+ }
+ }
+
+ .reward-states {
+ min-width: 120px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 1rem;
+
+ .price {
+ font-size: 1.25rem;
+ font-weight: bold;
+ color: #526665;
+ }
+ }
+ }
+ }
+}
+
+// ========== 弹窗发布样式 ==========
+
+.publish-dialog {
+ width: 600px !important;
+ max-width: 90vw;
+
+ .p-dialog-header {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: $heading-color;
+ padding-bottom: 0.5rem;
+ }
+
+ .p-dialog-content {
+ padding-top: 0;
+ padding-bottom: 0;
+
+ .publish-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ margin-top: 1rem;
+
+ .form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ label {
+ font-weight: 600;
+ color: $heading-color;
+ }
+
+ input,
+ textarea {
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ color: #2d3748;
+ }
+
+
+ .p-fileupload {
+ .p-button {
+ width: 100%;
+ justify-content: center;
+ border: none;
+ margin-bottom: 1rem;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file