feat(hot-resource): add hot resource page with line chart for popularity trends
Change-Id: I13aa76ff02f7f43225bf1736630eec3286bb75a6
diff --git a/package-lock.json b/package-lock.json
index 6e3de0c..f229b32 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@icon-park/react": "^1.4.2",
"@react-icons/all-files": "^4.1.0",
"axios": "^1.9.0",
+ "chart.js": "^4.4.9",
"lodash": "^4.17.21",
"next": "15.2.4",
"primeicons": "^7.0.0",
@@ -613,6 +614,11 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.7",
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz",
@@ -2044,6 +2050,17 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.4.9.tgz",
+ "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
diff --git a/package.json b/package.json
index c61cf2d..faa6978 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@icon-park/react": "^1.4.2",
"@react-icons/all-files": "^4.1.0",
"axios": "^1.9.0",
+ "chart.js": "^4.4.9",
"lodash": "^4.17.21",
"next": "15.2.4",
"primeicons": "^7.0.0",
diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx
index 764e358..81c8a6b 100644
--- a/src/app/community/page.tsx
+++ b/src/app/community/page.tsx
@@ -118,7 +118,7 @@
</div>
{/* 全部分类 */}
- <h1>全部分类</h1>
+ <h1>分类</h1>
<div className="all-communities-classifications">
<Link href="/community/resource-community-list/材质包">
<Image
diff --git a/src/app/globals.scss b/src/app/globals.scss
index 097d350..a2f8cf4 100644
--- a/src/app/globals.scss
+++ b/src/app/globals.scss
@@ -50,20 +50,21 @@
}
+.no-underline {
+ text-decoration: none;
+}
+
.tools {
display: flex;
align-items: center;
gap: 2rem;
- .no-underline {
- text-decoration: none;
- }
-
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
+ transition: color 0.5s ease;
i {
font-size: 1.25rem;
@@ -74,6 +75,16 @@
font-size: 0.75rem;
color: #333;
}
+
+ &:active {
+ i {
+ color: #14b8a6; // 点击时图标变色
+ }
+
+ span {
+ color: #14b8a6; // 点击时文字变色
+ }
+ }
}
.p-avatar {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 8d2a66a..e29b4f6 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -28,9 +28,12 @@
<div className="logo-name">
<Link href="/" className="no-underline">
<img src="/logo.png" alt="Logo" className="logo" />
+
+ </Link>
+ <Link href="/" className="no-underline">
+ <span className="name">MCPT</span>
</Link>
- <span className="name">MCPT</span>
</div>
<div className="tools">
<Link href="/resource/hot-resource" className="no-underline">
diff --git a/src/app/main.scss b/src/app/main.scss
index 90bf67d..f10a723 100644
--- a/src/app/main.scss
+++ b/src/app/main.scss
@@ -34,6 +34,7 @@
img {
border-radius: 8px;
object-fit: cover;
+ cursor: pointer;
}
}
@@ -47,8 +48,8 @@
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
- background: rgba(255, 255, 255, 0);
- color: #fff;
+ background: rgba(213, 244, 235, 0.5);
+ color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
@@ -63,7 +64,7 @@
/* 右箭头靠右 */
.p-carousel-next {
- right: 0.5rem;
+ right: 1rem;
}
}
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index a994e7c..fb42dac 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -61,9 +61,6 @@
resourceId: number;
resourceName: string;
resourcePicture: string;
- likes: number;
- dowloads: number;
- seeds: number;
}
interface HotResourceList {
records: HotResource[];
@@ -143,9 +140,7 @@
// 获取热门资源幻灯片
const fetchHotResources = async () => {
try {
- const response = await axios.get<HotResourceList>(process.env.PUBLIC_URL +`/resource/hot`, {
- params: { pageNumber: 1, rows: 3, searchValue: '', type: '' }
- });
+ const response = await axios.get<HotResourceList>(process.env.PUBLIC_URL +`/resource/hot/slide`);
console.log('获取热门社区幻灯片:', response.data.records);
setHotResources(response.data.records);
} catch (err) {
diff --git a/src/app/resource/hot-resource/hot-resource.scss b/src/app/resource/hot-resource/hot-resource.scss
new file mode 100644
index 0000000..587eb15
--- /dev/null
+++ b/src/app/resource/hot-resource/hot-resource.scss
@@ -0,0 +1,192 @@
+.HotResource {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+.main-header {
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+ margin: 0 auto;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+.chart-wrapper {
+ flex: 1.2; // 右侧占一半
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ padding: 0.5rem;
+ justify-content: center;
+ background: #fff;
+ border-radius: 10px;
+
+ canvas {
+ width: 100% !important;
+ height: 100% !important;
+ }
+}
+
+.carousel-wrapper {
+ flex: 0.8;
+
+ .custom-carousel {
+ position: relative;
+
+ .carousel-item {
+ position: relative;
+ max-width: 100px;
+
+ img {
+ border-radius: 10px 10px 10px 10px;
+ border-top-right-radius: 10px;
+ border-bottom-left-radius: 10px;
+ object-fit: cover;
+ cursor: pointer;
+ }
+
+ h3 {
+ width: 480px;
+ text-align: center;
+ bottom: 0;
+ left: 0;
+ color: #fff;
+ background: #14b8a6;
+ padding: 0.5rem 1rem;
+ font-size: 1.75rem;
+ margin: 0;
+ border-radius: 0 0 10px 10px;
+ position: absolute;
+ }
+ }
+
+ .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(213, 244, 235, 0.5);
+ color: #ffffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ /* 确保按钮可点击 */
+ }
+
+ /* 左箭头靠左 */
+ .p-carousel-prev {
+ left: 0.5rem;
+ }
+
+ /* 右箭头靠右 */
+ .p-carousel-next {
+ right: 0.6rem;
+ }
+ }
+}
+
+
+// 全部社区样式
+.all-resources {
+ width: 100%;
+ padding: 1rem;
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &-card {
+ height: 140px;
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+ transition: transform 0.3s ease;
+ box-shadow: none !important; // 取消阴影
+ cursor: pointer;
+
+ //填充卡片
+ &.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;
+ }
+
+ .resource-header {
+ display: flex;
+ flex: 1;
+ max-width: 850px;
+ padding-left: 20px;
+ padding-right: 20px;
+ margin-bottom: 20px;
+ }
+
+ .resource-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+
+ h3 {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #2c3e50;
+ }
+
+ .tags {
+ display: flex;
+ gap: 0.5rem;
+ }
+
+ .resource-introduction {
+ color: #666;
+ font-size: 1rem;
+ margin-bottom: 0;
+ }
+ }
+
+ .resources-states {
+ min-width: 120px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 0.5rem;
+
+ .state-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #666;
+ font-size: 1rem;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/resource/hot-resource/page.tsx b/src/app/resource/hot-resource/page.tsx
index c48b626..022ebe3 100644
--- a/src/app/resource/hot-resource/page.tsx
+++ b/src/app/resource/hot-resource/page.tsx
@@ -1,12 +1,271 @@
'use client';
-import React from 'react';
-const EmptyPage: React.FC = () => {
+import React, { useEffect, useState, useRef } from "react";
+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 { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 评分图标
+import { Fire } from '@icon-park/react';
+// 接口传输
+import axios from 'axios';
+// 标签
+import { Tag } from 'primereact/tag';
+
+import { Chart } from 'primereact/chart';
+
+import { TabView, TabPanel } from 'primereact/tabview';
+// 样式
+import './hot-resource.scss';
+
+// 热门资源数据
+
+interface HotResourceSlide {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+}
+
+interface HotResourceSlideList {
+ records: HotResourceSlide[];
+}
+interface HotResource {
+ resourceId: number;
+ resourceName: string;
+ resourcePicture: string;
+ resourceSummary: string;
+ lastUpdateTime: string;
+ hot: number;
+ gamePlayList: { gameplayName: string }[];
+}
+interface HotEntry {
+ hot: number;
+}
+interface HotInfo {
+ resourceId: number;
+ resourceName: string;
+ hotList: HotEntry[];
+}
+interface HotInfoResponse {
+ hotInfoList: HotInfo[];
+}
+interface HotResourceList {
+ total: number;
+ records: HotResource[];
+}
+// 主页
+export default function HotResource() {
+ // 热门资源列表
+ const [hotResources, setHotResources] = useState<HotResource[]>([]);
+ const [totalHotResource, setTotalHotResource] = useState(0);
+ const [hotResourceSlide, setHotResourceSlide] = useState<HotResourceSlide[]>([]);
+ // 图表数据
+ const [chartData, setChartData] = useState({});
+ const [chartOptions, setChartOptions] = useState({});
+
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ const router = useRouter();
+ const [activeTabIndex, setActiveTabIndex] = useState(0);
+ const resourceTabs = [
+ { title: '模组' },
+ { title: '地图' },
+ { title: '整合包' },
+ { title: '材质包' }
+ ];
+ // 当前选中的 classify
+ const [classify, setClassify] = useState<string>(resourceTabs[0].title);
+
+ // 分页
+ const [first, setFirst] = useState(0);
+ const [rows, setRows] = useState(6);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+ // 获取帖子列表
+ useEffect(() => {
+ fetchHotResources(classify);
+ }, [first, rows, classify]);
+
+ useEffect(() => {
+ fetchHotInfo();
+ // 组件加载时获取热门资源幻灯片
+ fetchHotResourcesSlide();
+ }, []);
+
+ const generateColor = (index: number): string => {
+ const colors = [
+ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0',
+ '#9966FF', '#FF9F40', '#C9CBCF', '#009688'
+ ];
+ return colors[index % colors.length];
+ };
+
+ const fetchHotInfo = async () => {
+ try {
+ const response = await axios.get<HotInfoResponse>(process.env.PUBLIC_URL + `/resource/hot-info`);
+ const hotInfoList = response.data.hotInfoList;
+
+ // 获取最近七天的日期标签(格式:MM-DD)
+ const labels = Array.from({ length: 7 }, (_, i) => {
+ const date = new Date();
+ date.setDate(date.getDate() - (6 - i)); // 向前推6~0天,按时间顺序排列
+ return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+ });
+
+ const datasets = hotInfoList.map((info, idx) => ({
+ label: info.resourceName,
+ data: info.hotList.map(h => h.hot),
+ fill: false,
+ tension: 0.4,
+ borderColor: generateColor(idx),
+ backgroundColor: generateColor(idx)
+ }));
+
+ setChartData({
+ labels,
+ datasets
+ });
+
+ setChartOptions({
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'right'
+ },
+ title: {
+ display: true,
+ text: '资源热度变化趋势图'
+ }
+ },
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: '日期'
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: '热度'
+ },
+ beginAtZero: true,
+ min: 4,
+ max: 10,
+ ticks: {
+ stepSize: 2, // ← 每 1 个单位显示一个刻度
+ font: {
+ size: 12
+ }
+ }
+ }
+ }
+ });
+ } catch (err) {
+ console.error('获取资源热度信息失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取热度信息失败' });
+ }
+ };
+
+ // 获取热门资源幻灯片
+ const fetchHotResourcesSlide = async () => {
+ try {
+ const response = await axios.get<HotResourceSlideList>(process.env.PUBLIC_URL + `/resource/hot/slide`);
+ console.log('获取热门社区幻灯片:', response.data.records);
+ setHotResourceSlide(response.data.records);
+ } catch (err) {
+ console.error('获取Mod失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取Mod失败' });
+ }
+ };
+ // 获取热门资源
+ const fetchHotResources = async (classify: string) => {
+ try {
+ const pageNumber = first / rows + 1;
+ console.log("当前页" + pageNumber + "size" + rows + "分类" + classify);
+ const response = await axios.get<HotResourceList>(process.env.PUBLIC_URL + `/resource/hot`, {
+ params: { pageNumber, rows, classify }
+ });
+ console.log('获取热门社区:', response.data.records);
+
+ setHotResources(response.data.records);
+ setTotalHotResource(response.data.total);
+ } catch (err) {
+ console.error('获取Mod失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取Mod失败' });
+ }
+ };
return (
- <div className="p-d-flex p-jc-center p-ai-center" style={{ height: '100vh' }}>
- {"一个空页面"}
+ <div className="HotResource">
+ {/* 轮播图部分 */}
+ <div className="main-header">
+ <div className="carousel-wrapper">
+ <Carousel
+ value={hotResourceSlide}
+ 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="480" height="350" />
+ <h3>{hotResource.resourceName}</h3>
+ </div>
+ )}
+ />
+ </div>
+ <div className="chart-wrapper">
+ <Chart type="line" data={chartData} options={chartOptions} />
+ </div>
+ </div>
+ {/* 全部社区 */}
+ <TabView scrollable className="all-resources" activeIndex={activeTabIndex} onTabChange={(e) => {
+ setActiveTabIndex(e.index);
+ const newClassify = resourceTabs[e.index].title;
+ setClassify(newClassify);
+ }}>
+ {resourceTabs.map((tab) => {
+ return (
+ <TabPanel key={tab.title} header={tab.title}>
+ <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" />
+ <div className="resource-header">
+ <div className="resource-content">
+ <h3>{hotResource.resourceName}</h3>
+ <div className="tags">
+ {hotResource.gamePlayList.map((tag, index) => (
+ <Tag key={index} value={tag.gameplayName} />
+ ))}
+ </div>
+ </div>
+ <div className="resources-states">
+ <div className="state-item">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>热度: {hotResource.hot}</span>
+ </div>
+ <div className="state-item">
+ <span>最新更新时间: {hotResource.lastUpdateTime}</span>
+ </div>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </TabPanel>
+ );
+ })}
+ {totalHotResource > 6 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalHotResource} rowsPerPageOptions={[6, 12]} onPageChange={onPageChange} />)}
+ </TabView>
</div>
);
-};
-
-export default EmptyPage;
+}