修改促销、优化页面布局
Change-Id: Iae813b5b6557efa7059fe6d94bc32e96c984e4ea
diff --git a/src/pages/Forum/posts-detail/PostDetailPage.jsx b/src/pages/Forum/posts-detail/PostDetailPage.jsx
index 2e4874a..bf0f062 100644
--- a/src/pages/Forum/posts-detail/PostDetailPage.jsx
+++ b/src/pages/Forum/posts-detail/PostDetailPage.jsx
@@ -5,6 +5,7 @@
import './PostDetailPage.css';
import { UserContext } from '../../../context/UserContext'; // 用户上下文
import Header from '../../../components/Header';
+import AuthButton from '../../../components/AuthButton';
const formatImageUrl = (url) => {
if (!url) return '';
@@ -259,7 +260,10 @@
onChange={(e) => setNewComment(e.target.value)}
/>
<div className="comment-options">
- <button onClick={handleAddComment}>发布回复</button>
+ <AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}>
+ 发布回复
+ </AuthButton>
+
<button
onClick={() => {
setReplyToCommentId(null);
@@ -284,7 +288,10 @@
onChange={(e) => setNewComment(e.target.value)}
/>
<div className="comment-options">
- <button onClick={handleAddComment}>发布评论</button>
+ <AuthButton roles={['cookie', 'chocolate', 'ice-cream']} onClick={handleAddComment}>
+ 发布评论
+ </AuthButton>
+
</div>
</div>
)}
diff --git a/src/pages/Forum/posts-main/ForumPage.css b/src/pages/Forum/posts-main/ForumPage.css
index 5fd64a2..4eb280f 100644
--- a/src/pages/Forum/posts-main/ForumPage.css
+++ b/src/pages/Forum/posts-main/ForumPage.css
@@ -1,8 +1,8 @@
.forum-page {
- color: #fff;
+ color: #453228;
/* background-color: #5F4437; */
/* background: linear-gradient(180deg, #5F4437, #9c737b); */
- background: #333;
+ background: #f8f3ef;
/* background-color: #5F4437; */
min-height: 100vh;
font-family: Arial, sans-serif;
@@ -37,20 +37,13 @@
align-items: center;
}
- .avatar {
- width: 36px;
- height: 36px;
- border-radius: 50%;
- margin-right: 10px;
- }
-
.nickname {
font-weight: bold;
}
- .cover-image {
- max-height: 80px;
- max-width: 120px;
+ .cover {
+ max-height: 320px;
+ max-width: 320px;
object-fit: cover;
border-radius: 6px;
}
diff --git a/src/pages/Forum/posts-main/components/CreatePostButton.css b/src/pages/Forum/posts-main/components/CreatePostButton.css
index 225ddde..c5cc906 100644
--- a/src/pages/Forum/posts-main/components/CreatePostButton.css
+++ b/src/pages/Forum/posts-main/components/CreatePostButton.css
@@ -1,13 +1,18 @@
.create-post {
display: flex;
justify-content: center;
- margin: 20px 0;
+ margin-top: 3%;
+ margin-bottom: -1%;
}
.create-btn {
- background-color: #BA929A;
- color: white;
- padding: 10px 20px;
+ /* background-color: #BA929A;
+ color: white; */
+ /* background-color: #e9ded2; */
+ /* 使用浅色背景,符合整体风格 */
+ background: none;
+ color: #5F4437;
+ padding: 7px 15px;
border-radius: 8px;
border: none;
cursor: pointer;
@@ -18,7 +23,24 @@
}
.create-btn:hover {
- background-color: #a17b83;
+ background-color: #e9ded2;
+}
+
+.view-btn {
+ background: none;
+ color: #0f5e51;
+ padding: 7px 15px;
+ border-radius: 8px;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ transition: background-color 0.3s ease;
+}
+
+.view-btn:hover {
+ background-color: #e9ded2;
}
/* Modal 样式 */
diff --git a/src/pages/Forum/posts-main/components/PostList.css b/src/pages/Forum/posts-main/components/PostList.css
index ee4d951..f6d6c2a 100644
--- a/src/pages/Forum/posts-main/components/PostList.css
+++ b/src/pages/Forum/posts-main/components/PostList.css
@@ -5,13 +5,68 @@
padding: 30px;
}
-.post-actions {
- justify-content: flex-end; /*靠右对齐*/
+.dianzan-shoucang {
+ /*靠左对齐*/
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-left: -30px;
}
- .post-card {
- background-color: #e9ded2;
- border: 1px solid #000000;
- padding: 16px;
- border-radius: 8px;
- }
-
\ No newline at end of file
+.post-card {
+ background-color: #e9ded2;
+ padding: 16px;
+ border-radius: 8px;
+ /* 移除固定高度 */
+ /* height: 230px; */
+ /* 添加阴影效果增强视觉层次感 */
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ /* 添加过渡效果使交互更流畅 */
+ transition: transform 0.2s ease;
+}
+
+/* 鼠标悬停效果 */
+.post-card:hover {
+ transform: translateY(-2px);
+}
+
+/* 新增:内容包装器,使用flex布局 */
+.post-content-wrapper {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+/* 新增:左侧内容区域 */
+.post-content-left {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+/* 新增:右侧内容区域 */
+.post-content-right {
+ flex: 0 0 30%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+/* 调整封面图片样式 */
+.cover {
+ /* max-width: 100%;
+ max-height: 200px; */
+ width: 50%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 8px;
+}
+
+.custom-link {
+ color: #2196F3; /* 浅蓝色 */
+ text-decoration: none; /* 去除下划线 */
+}
+
+.custom-link:hover {
+ text-decoration: underline; /* 鼠标悬停时显示下划线 */
+}
diff --git a/src/pages/Forum/posts-main/components/PostList.jsx b/src/pages/Forum/posts-main/components/PostList.jsx
index d5ff736..cf0a424 100644
--- a/src/pages/Forum/posts-main/components/PostList.jsx
+++ b/src/pages/Forum/posts-main/components/PostList.jsx
@@ -159,45 +159,55 @@
return (
<div key={post.postNo} className="post-card" style={{ backgroundColor: '#e9ded2' }}>
+ {/* 用户信息 */}
<div className="user-info">
<Link href={`/information/${post.user_id}`}>
<img
- className="avatar"
+ style={{ width: '80px', height: '80px', borderRadius: '50%', cursor: 'pointer', border: '4px solid #d2b48c' }}
src={post.avatarUrl}
alt="头像"
- style={{ cursor: 'pointer' }}
/>
</Link>
- <span className="nickname" style={{ color: '#755e50' }}>{post.username}</span>
+ <span className="nickname" style={{ color: '#755e50', marginLeft: '10px', fontSize: '20px' }}>{post.username}</span>
</div>
- {coverImage && <img className="cover-image" src={coverImage} alt="封面" />}
-
- <h3 style={{ color: '#000000' }}>{post.title || '无标题'}</h3>
- <div className="post-meta">
- <span>发布时间:{timeText}</span>
- <div className="post-actions">
- <button className="icon-btn" onClick={() => toggleLike(post.postNo, post.liked,user.userId
-)}>
- <GoodTwo theme="outline" size="24" fill={post.liked ? '#f00' : '#fff'} />
- <span>{post.likeCount}</span>
- </button>
- <button className="icon-btn" onClick={() => toggleCollect(post.postNo, post.collected, user.userId
-)}>
- <Star theme="outline" size="24" fill={post.collected ? '#ffd700' : '#fff'} />
- <span>{post.collectCount}</span>
- </button>
-
- {canDelete && (
- <button className="icon-btn" onClick={() => handleDeletePost(post.postNo, user.userId
-)}>
- <Delete theme="outline" size="24" fill="#333" />
+ {/* 内容区 - 使用flex布局 */}
+ <div className="post-content-wrapper">
+ {/* 左侧内容:标题和元信息 */}
+ <div className="post-content-left">
+ <h3 style={{ color: '#000000' }}>{post.title || '无标题'}</h3>
+ <div className="post-meta">
+ <span>发布时间:{timeText}</span>
+ </div>
+
+ {/* 点赞和收藏按钮 - 移至内容下方 */}
+ <div className="dianzan-shoucang">
+ <button className="icon-btn" onClick={() => toggleLike(post.postNo, post.liked, user.userId)}>
+ <GoodTwo theme="outline" size="24" fill={post.liked ? '#f00' : '#fff'} />
+ <span>{post.likeCount}</span>
</button>
- )}
+ <button className="icon-btn" onClick={() => toggleCollect(post.postNo, post.collected, user.userId)}>
+ <Star theme="outline" size="24" fill={post.collected ? '#ffd700' : '#fff'} />
+ <span>{post.collectCount}</span>
+ </button>
+
+ {canDelete && (
+ <button className="icon-btn" onClick={() => handleDeletePost(post.postNo, post.user_id)}>
+ <Delete theme="outline" size="24" fill="#333" />
+ </button>
+ )}
+ </div>
+
+ {/* 查看详情按钮 */}
+ <div>
+ <Link href={`/forum/post/${post.postNo}`} className="custom-link">查看详情</Link>
+ </div>
</div>
- </div>
- <div className="detail-button-wrapper">
- <Link href={`/forum/post/${post.postNo}`} className="detail-button">查看详情</Link>
+
+ {/* 右侧内容:封面图片 */}
+ <div className="post-content-right">
+ {coverImage && <img className="cover" src={coverImage} alt="封面" />}
+ </div>
</div>
</div>
);
@@ -213,5 +223,4 @@
);
};
-export default PostList;
-
+export default PostList;
diff --git a/src/pages/Forum/promotion-part/CategoryPromotionDialog.jsx b/src/pages/Forum/promotion-part/CategoryPromotionDialog.jsx
new file mode 100644
index 0000000..101b653
--- /dev/null
+++ b/src/pages/Forum/promotion-part/CategoryPromotionDialog.jsx
@@ -0,0 +1,79 @@
+import React from 'react';
+
+const CategoryPromotionDialog = ({
+ showCategoryDialog,
+ categoryFormData,
+ handleCategoryInputChange,
+ closeCategoryDialog,
+ handleCreateCategoryPromotion
+}) => {
+ return (
+ showCategoryDialog && (
+ <div className="dialog-overlay">
+ <div className="dialog">
+ <h3>创建特定分类促销</h3>
+ <div className="form-item">
+ <label>促销名称:</label>
+ <input
+ type="text"
+ name="name"
+ value={categoryFormData.name}
+ onChange={handleCategoryInputChange}
+ placeholder="请输入促销名称"
+ />
+ </div>
+ <div className="form-item">
+ <label>开始时间:</label>
+ <input
+ type="datetime-local"
+ name="startTime"
+ value={categoryFormData.startTime}
+ onChange={handleCategoryInputChange}
+ />
+ </div>
+ <div className="form-item">
+ <label>结束时间:</label>
+ <input
+ type="datetime-local"
+ name="endTime"
+ value={categoryFormData.endTime}
+ onChange={handleCategoryInputChange}
+ />
+ </div>
+ <div className="form-item">
+ <label>折扣百分比:</label>
+ <input
+ type="number"
+ name="discountPercentage"
+ value={categoryFormData.discountPercentage}
+ onChange={handleCategoryInputChange}
+ placeholder="正数表示上传加成,负数表示下载折扣"
+ step="0.1"
+ />
+ </div>
+ <div className="form-item">
+ <label>促销类别:</label>
+ <select
+ name="category"
+ value={categoryFormData.category}
+ onChange={handleCategoryInputChange}
+ >
+ <option value="movie">电影</option>
+ <option value="tv">剧集</option>
+ <option value="music">音乐</option>
+ <option value="game">游戏</option>
+ <option value="software">软件</option>
+ <option value="book">书籍</option>
+ </select>
+ </div>
+ <div className="dialog-buttons">
+ <button onClick={handleCreateCategoryPromotion}>确定</button>
+ <button onClick={closeCategoryDialog}>取消</button>
+ </div>
+ </div>
+ </div>
+ )
+ );
+};
+
+export default CategoryPromotionDialog;
\ No newline at end of file
diff --git a/src/pages/Forum/promotion-part/ColdTorrentsDialog.jsx b/src/pages/Forum/promotion-part/ColdTorrentsDialog.jsx
new file mode 100644
index 0000000..79d2b4b
--- /dev/null
+++ b/src/pages/Forum/promotion-part/ColdTorrentsDialog.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+
+const ColdTorrentsDialog = ({
+ showColdDialog,
+ coldTorrents,
+ closeColdDialog,
+ fetchTorrentDetail
+}) => {
+ return (
+ showColdDialog && (
+ <div className="cold-dialog-overlay">
+ <div className="cold-dialog">
+ <h3 className="cold-dialog-title">冷门资源列表</h3>
+ <button
+ className="close-btn"
+ onClick={closeColdDialog}
+ >
+ ×
+ </button>
+
+ {coldTorrents.length === 0 ? (
+ <div className="empty-state">暂无冷门资源</div>
+ ) : (
+ <div className="cold-table-container">
+ <table className="cold-torrent-table">
+ <thead>
+ <tr>
+ <th>序号</th>
+ <th>资源名称</th>
+ <th>资源ID</th>
+ <th>分类</th>
+ <th>描述</th>
+ <th>下载用户数</th>
+ <th>浏览次数</th>
+ <th>创建时间</th>
+ </tr>
+ </thead>
+ <tbody>
+ {coldTorrents.map((torrent, index) => (
+ <tr key={torrent.id}>
+ <td>{index + 1}</td>
+ <td>
+ <button
+ className="torrent-link"
+ onClick={() => fetchTorrentDetail(torrent.id)}
+ aria-label={`查看种子${torrent.title || torrent.id}的详情`}
+ >
+ {torrent.title}
+ </button>
+ </td>
+ <td>{torrent.id}</td>
+ <td>{torrent.category || '未分类'}</td>
+ <td>{torrent.description || '无描述'}</td>
+ <td>{torrent.leechers || 0}</td>
+ <td>{torrent.views || 0}</td>
+ <td>{new Date(torrent.createdTime).toLocaleDateString()}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </div>
+ </div>
+ )
+ );
+};
+
+export default ColdTorrentsDialog;
\ No newline at end of file
diff --git a/src/pages/Forum/promotion-part/CreatePromotionDialog.jsx b/src/pages/Forum/promotion-part/CreatePromotionDialog.jsx
new file mode 100644
index 0000000..6dcf236
--- /dev/null
+++ b/src/pages/Forum/promotion-part/CreatePromotionDialog.jsx
@@ -0,0 +1,115 @@
+import React from 'react';
+
+const CreatePromotionDialog = ({
+ showCreateDialog,
+ formData,
+ handleInputChange,
+ closeCreateDialog,
+ handleCreatePromotion,
+ fetchPromoColdTorrents,
+ showPromoColdTable,
+ coldTorrents,
+ handlePromoTorrentSelection
+}) => {
+ return (
+ showCreateDialog && (
+ <div className="dialog-overlay">
+ <div className="dialog">
+ <h3>创建冷门资源促销</h3>
+ <div className="form-item">
+ <label>促销名称:</label>
+ <input
+ type="text"
+ name="name"
+ value={formData.name}
+ onChange={handleInputChange}
+ placeholder="请输入促销名称"
+ />
+ </div>
+ <div className="form-item">
+ <label>开始时间:</label>
+ <input
+ type="datetime-local"
+ name="startTime"
+ value={formData.startTime}
+ onChange={handleInputChange}
+ />
+ </div>
+ <div className="form-item">
+ <label>结束时间:</label>
+ <input
+ type="datetime-local"
+ name="endTime"
+ value={formData.endTime}
+ onChange={handleInputChange}
+ />
+ </div>
+ <div className="form-item">
+ <label>折扣百分比:</label>
+ <input
+ type="number"
+ name="discountPercentage"
+ value={formData.discountPercentage}
+ onChange={handleInputChange}
+ placeholder="正数表示上传加成,负数表示下载折扣"
+ step="0.1"
+ />
+ </div>
+ <div className="form-item">
+ <label>适用种子:</label>
+ <button
+ className="cold-btn small"
+ onClick={fetchPromoColdTorrents}
+ >
+ 选择冷门资源 <span>(点击加载列表)</span>
+ </button>
+
+ {showPromoColdTable && (
+ <div className="torrent-table-container">
+ <table className="torrent-selection-table">
+ <thead>
+ <tr>
+ <th>选择</th>
+ <th>序号</th>
+ <th>资源名称</th>
+ <th>资源ID</th>
+ <th>分类</th>
+ <th>下载用户数</th>
+ <th>浏览次数</th>
+ </tr>
+ </thead>
+ <tbody>
+ {coldTorrents.map((torrent, index) => (
+ <tr key={torrent.id}>
+ <td>
+ <input
+ type="checkbox"
+ checked={formData.applicableTorrentIds.includes(torrent.id)}
+ onChange={(e) => handlePromoTorrentSelection(torrent.id, e.target.checked)}
+ />
+ </td>
+ <td>{index + 1}</td>
+ <td>{torrent.title}</td>
+ <td>{torrent.id}</td>
+ <td>{torrent.category || '未分类'}</td>
+ <td>{torrent.leechers || 0}</td>
+ <td>{torrent.views || 0}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </div>
+
+ <div className="dialog-buttons">
+ <button onClick={handleCreatePromotion}>确定</button>
+ <button onClick={closeCreateDialog}>取消</button>
+ </div>
+ </div>
+ </div>
+ )
+ );
+};
+
+export default CreatePromotionDialog;
\ No newline at end of file
diff --git a/src/pages/Forum/promotion-part/Promotion.css b/src/pages/Forum/promotion-part/Promotion.css
index 46e7a0d..656114a 100644
--- a/src/pages/Forum/promotion-part/Promotion.css
+++ b/src/pages/Forum/promotion-part/Promotion.css
@@ -1,83 +1,3 @@
-.promotion-container {
- padding: 20px;
- margin: 20px 0;
- border-radius: 8px;
-}
-
-/* 并排两列 */
-.carousel-container {
- display: flex;
- gap: 20px;
-}
-
-.carousel-section {
- flex: 1;
-}
-
-.carousel-section h2 {
- font-size: 20px;
- margin-bottom: 15px;
-}
-
-/* 轮播框架 */
-.carousel {
- position: relative;
- /* background: #a54747; */
- /* background: linear-gradient(135deg, #4A3B34, #a54747); */
- /* background: linear-gradient(135deg, #e38f77, #aa3e3e); */
- /* 背景渐变 */
- background: linear-gradient(135deg, #e1cab2, #b68791);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
- border-radius: 6px;
- padding: 15px;
- color: #fff;
- min-height: 200px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-/* 左右箭头 */
-.carousel .arrow {
- background: rgba(0,0,0,0.2);
- border: none;
- color: #fff;
- font-size: 24px;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- cursor: pointer;
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
-}
-
-.carousel .arrow.left {
- left: 10px;
-}
-
-.carousel .arrow.right {
- right: 10px;
-}
-
-.carousel .arrow:hover {
- background: rgba(0,0,0,0.4);
-}
-
-/* 每帧内容 */
-.carousel .slide {
- width: calc(100% - 80px);
- /* 留出箭头空间 */
- text-align: left;
-}
-
-/* 冷门资源专用 slide */
-.cold-slide {
- display: flex;
- gap: 10px;
- align-items: center;
-}
-
/* 资源海报 */
.resource-poster {
width: 80px;
@@ -125,7 +45,7 @@
background: #fff;
padding: 20px;
border-radius: 6px;
- width: 400px;
+ width: 80%;
max-width: 90%;
box-shadow: 0 0 10px rgba(0,0,0,0.25);
}
@@ -154,3 +74,266 @@
cursor: pointer;
}
+
+/* 冷门资源模态框样式 */
+.cold-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 228, 230, 0.3); /* 清透粉半透明背景 */
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.cold-dialog {
+ background-color: #F8F8F0; /* 米白色背景 */
+ padding: 20px;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(255, 192, 203, 0.3); /* 粉调阴影 */
+ max-width: 800px;
+ width: 100%;
+}
+
+.cold-dialog-title {
+ color: #FF6B81; /* 亮粉标题 */
+ text-align: center;
+ margin-bottom: 15px;
+}
+
+.close-btn {
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ font-size: 20px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #FF4E50; /* 深粉关闭按钮 */
+}
+
+.cold-table-container {
+ overflow-x: auto; /* 长表格横向滚动 */
+}
+
+.cold-torrent-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 15px;
+ background-color: white; /* 表格白色背景 */
+}
+
+.cold-torrent-table th,
+.cold-torrent-table td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #FFE4E6; /* 清透粉分隔线 */
+}
+
+.cold-torrent-table th {
+ background-color: #FFF0F5; /* 淡粉表头背景 */
+ color: #FF69B4; /* 粉紫表头文字 */
+ font-weight: 500;
+}
+
+.cold-torrent-table tr:hover {
+ background-color: #FFF5EB; /* 米白悬停效果 */
+}
+
+.empty-state {
+ text-align: center;
+ padding: 20px;
+ color: #666;
+}
+
+/* 适配小屏幕 */
+@media (max-width: 600px) {
+ .cold-dialog {
+ margin: 20px;
+ max-width: calc(100% - 40px);
+ }
+}
+
+.cold-btn.small {
+ font-size: 0.9em;
+ padding: 5px 10px;
+ margin-top: 5px;
+}
+
+.torrent-table-container {
+ margin-top: 10px;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.torrent-selection-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.torrent-selection-table th,
+.torrent-selection-table td {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ text-align: left;
+}
+
+.torrent-selection-table th {
+ background-color: #f5f5f5;
+}
+
+.detail-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 100;
+}
+
+.detail-dialog {
+ background-color: white;
+ padding: 20px;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 600px;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.detail-content {
+ margin-top: 20px;
+}
+
+.detail-item {
+ margin-bottom: 15px;
+ display: flex;
+}
+
+.detail-label {
+ font-weight: bold;
+ min-width: 120px;
+}
+
+.detail-value {
+ flex: 1;
+}
+
+.torrent-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.torrent-link {
+ background: none;
+ border: none;
+ color: #0066cc;
+ text-decoration: underline;
+ cursor: pointer;
+ padding: 0;
+ font-size: inherit;
+}
+
+.torrent-link:hover,
+.torrent-link:focus {
+ text-decoration: none;
+ outline: none;
+ color: #004499;
+}
+
+.torrent-detail-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 200; /* 确保在促销详情对话框之上 */
+}
+
+.torrent-detail-dialog {
+ background-color: white;
+ padding: 20px;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 700px;
+ max-height: 85vh;
+ overflow-y: auto;
+}
+
+.torrent-detail-content {
+ margin-top: 20px;
+}
+
+.torrent-detail-item {
+ margin-bottom: 15px;
+ display: flex;
+}
+
+.torrent-detail-label {
+ font-weight: bold;
+ min-width: 120px;
+}
+
+.torrent-detail-value {
+ flex: 1;
+}
+
+.description {
+ white-space: pre-wrap;
+}
+
+.torrent-cover-container {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 20px;
+}
+
+.torrent-cover {
+ max-width: 100%;
+ max-height: 300px;
+ object-fit: contain;
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+}
+
+.status-badge {
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 0.9em;
+ color: white;
+}
+
+.status-badge.hot {
+ background-color: #e53935;
+}
+
+.status-badge.cold {
+ background-color: #1e88e5;
+}
+
+.status-badge.normal {
+ background-color: #757575;
+}
+
+.download-link {
+ color: #0066cc;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+}
+
+.download-link:hover {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/src/pages/Forum/promotion-part/Promotion.jsx b/src/pages/Forum/promotion-part/Promotion.jsx
index 953a37f..37730f3 100644
--- a/src/pages/Forum/promotion-part/Promotion.jsx
+++ b/src/pages/Forum/promotion-part/Promotion.jsx
@@ -1,832 +1,13 @@
-// import React, { useEffect, useState, useRef } from 'react';
-// import './Promotion.css';
-// import { useUser } from '../../../context/UserContext';
-
-// const Promotion = () => {
-// const { user } = useUser();
-// const [promotions, setPromotions] = useState([]);
-// const [torrents, setTorrents] = useState([]);
-// const [loading, setLoading] = useState(true);
-// const [promoIndex, setPromoIndex] = useState(0);
-// const promoTimerRef = useRef(null);
-
-// // 新增:控制创建对话框显示
-// const [showCreateDialog, setShowCreateDialog] = useState(false);
-
-// // 创建促销活动表单状态
-// const [formData, setFormData] = useState({
-// name: '',
-// startTime: '',
-// endTime: '',
-// discountPercentage: '',
-// uploadCoeff: '',
-// downloadCoeff: '',
-// description: ''
-// });
-
-// useEffect(() => {
-// fetchData();
-// fetchTorrentList();
-// }, []);
-
-// useEffect(() => {
-// if (promotions.length === 0) return;
-// clearInterval(promoTimerRef.current);
-// promoTimerRef.current = setInterval(() => {
-// setPromoIndex(prev => (prev + 1) % promotions.length);
-// }, 5000);
-// return () => clearInterval(promoTimerRef.current);
-// }, [promotions]);
-
-// const fetchData = async () => {
-// try {
-// const response = await fetch('/seeds/promotions');
-// const json = await response.json();
-// const promoData = Array.isArray(json?.data) ? json.data : [];
-// setPromotions(promoData);
-// } catch (error) {
-// console.error('获取促销活动失败:', error);
-// } finally {
-// setLoading(false);
-// }
-// };
-
-// const fetchTorrentList = async () => {
-// try {
-// const response = await fetch('/seeds/list');
-// const json = await response.json();
-// const torrentList = Array.isArray(json?.data) ? json.data : [];
-// setTorrents(torrentList);
-// } catch (error) {
-// console.error('获取种子列表失败:', error);
-// }
-// };
-
-// // 打开创建促销活动弹窗
-// const openCreateDialog = () => {
-// // 重置表单数据
-// setFormData({
-// name: '',
-// startTime: '',
-// endTime: '',
-// discountPercentage: '',
-// uploadCoeff: '',
-// downloadCoeff: '',
-// description: ''
-// });
-// setShowCreateDialog(true);
-// };
-
-// // 关闭弹窗
-// const closeCreateDialog = () => {
-// setShowCreateDialog(false);
-// };
-
-// // 处理表单输入变化
-// const handleInputChange = (e) => {
-// const { name, value } = e.target;
-// setFormData(prev => ({
-// ...prev,
-// [name]: value
-// }));
-// };
-
-// // 提交创建促销活动
-// const handleCreatePromotion = async () => {
-// if (torrents.length === 0) {
-// alert('没有可用的种子,请先上传种子');
-// return;
-// }
-// if (!formData.name.trim()) {
-// alert('促销名称不能为空');
-// return;
-// }
-// if (!formData.startTime || !formData.endTime) {
-// alert('促销开始时间和结束时间不能为空');
-// return;
-// }
-// if (new Date(formData.startTime) >= new Date(formData.endTime)) {
-// alert('促销结束时间必须晚于开始时间');
-// return;
-// }
-// if (!formData.discountPercentage || isNaN(formData.discountPercentage)) {
-// alert('折扣百分比必须是数字');
-// return;
-// }
-
-// const applicableTorrentIds = torrents.map(t => t.id);
-
-// const newPromo = {
-// name: formData.name,
-// startTime: new Date(formData.startTime).toISOString(),
-// endTime: new Date(formData.endTime).toISOString(),
-// discountPercentage: Number(formData.discountPercentage),
-// uploadCoeff: formData.uploadCoeff ? Number(formData.uploadCoeff) : undefined,
-// downloadCoeff: formData.downloadCoeff ? Number(formData.downloadCoeff) : undefined,
-// applicableTorrentIds: JSON.stringify(applicableTorrentIds), // ✅ 关键修改
-// description: formData.description
-// };
-
-
-// try {
-// const res = await fetch('/seeds/promotions', {
-// method: 'POST',
-// headers: { 'Content-Type': 'application/json' },
-// body: JSON.stringify(newPromo)
-// });
-// const json = await res.json();
-// if (json.code === 200) {
-// alert('促销活动创建成功');
-// fetchData();
-// setShowCreateDialog(false);
-// } else {
-// alert('创建失败: ' + (json.msg || '未知错误'));
-// }
-// } catch (err) {
-// console.error('创建促销失败:', err);
-// alert('创建促销失败');
-// }
-// };
-
-// const handleDeletePromotion = async (promotionId) => {
-// if (!window.confirm('确认删除该促销活动吗?')) return;
-
-// try {
-// const res = await fetch(`/seeds/promotions/${promotionId}`, { method: 'DELETE' });
-// const json = await res.json();
-// if (json.success) {
-// alert('删除成功');
-// fetchData();
-// } else {
-// alert('删除失败: ' + json.message);
-// }
-// } catch (err) {
-// console.error('删除失败:', err);
-// }
-// };
-
-// const isAdmin = user?.role === 'admin';
-// const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
-// const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
-// const currentPromo = promotions[promoIndex];
-
-// if (loading) {
-// return <div className="promotion-container">加载中...</div>;
-// }
-
-// return (
-// <div className="promotion-container carousel-container">
-// <section className="carousel-section">
-// <h2>当前促销活动</h2>
-
-// {isAdmin && (
-// <button className="create-btn" onClick={openCreateDialog}>
-// 创建促销活动
-// </button>
-// )}
-
-// {promotions.length === 0 || !currentPromo ? (
-// <div className="empty-state">暂无促销活动</div>
-// ) : (
-// <div
-// className="carousel"
-// onMouseEnter={() => clearInterval(promoTimerRef.current)}
-// onMouseLeave={() => {
-// promoTimerRef.current = setInterval(() => {
-// setPromoIndex(prev => (prev + 1) % promotions.length);
-// }, 3000);
-// }}
-// >
-// <button className="arrow left" onClick={prevPromo}><</button>
-// <div className="slide">
-// <div><strong>促销名称:</strong>{currentPromo?.name ?? '未知'}</div>
-// <div><strong>促销时间:</strong>
-// {currentPromo?.pStartTime && currentPromo?.pEndTime
-// ? `${new Date(currentPromo.pStartTime).toLocaleString()} ~ ${new Date(currentPromo.pEndTime).toLocaleString()}`
-// : '未知'}
-// </div>
-// <div><strong>上传奖励系数:</strong>{currentPromo?.uploadCoeff ?? '无'}</div>
-// <div><strong>下载折扣系数:</strong>{currentPromo?.downloadCoeff ?? '无'}</div>
-// {currentPromo?.description && (
-// <div><strong>描述:</strong>{currentPromo.description}</div>
-// )}
-// {isAdmin && (
-// <button className="delete-btn" onClick={() => handleDeletePromotion(currentPromo.id)}>
-// 删除该活动
-// </button>
-// )}
-// </div>
-// <button className="arrow right" onClick={nextPromo}>></button>
-// </div>
-// )}
-// </section>
-
-// {/* 创建促销活动弹窗 */}
-// {showCreateDialog && (
-// <div className="dialog-overlay">
-// <div className="dialog">
-// <h3>创建促销活动</h3>
-// <div className="form-item">
-// <label>促销名称:</label>
-// <input
-// type="text"
-// name="name"
-// value={formData.name}
-// onChange={handleInputChange}
-// placeholder="请输入促销名称"
-// />
-// </div>
-// <div className="form-item">
-// <label>开始时间:</label>
-// <input
-// type="datetime-local"
-// name="startTime"
-// value={formData.startTime}
-// onChange={handleInputChange}
-// />
-// </div>
-// <div className="form-item">
-// <label>结束时间:</label>
-// <input
-// type="datetime-local"
-// name="endTime"
-// value={formData.endTime}
-// onChange={handleInputChange}
-// />
-// </div>
-// <div className="form-item">
-// <label>折扣百分比(数字):</label>
-// <input
-// type="number"
-// name="discountPercentage"
-// value={formData.discountPercentage}
-// onChange={handleInputChange}
-// placeholder="例如:20 表示 20% 折扣"
-// min="0"
-// max="100"
-// />
-// </div>
-// <div className="form-item">
-// <label>上传奖励系数(可选):</label>
-// <input
-// type="number"
-// name="uploadCoeff"
-// value={formData.uploadCoeff}
-// onChange={handleInputChange}
-// placeholder="例如:1.5"
-// step="0.1"
-// />
-// </div>
-// <div className="form-item">
-// <label>下载折扣系数(可选):</label>
-// <input
-// type="number"
-// name="downloadCoeff"
-// value={formData.downloadCoeff}
-// onChange={handleInputChange}
-// placeholder="例如:0.8"
-// step="0.1"
-// />
-// </div>
-// <div className="form-item">
-// <label>描述(可选):</label>
-// <textarea
-// name="description"
-// value={formData.description}
-// onChange={handleInputChange}
-// placeholder="促销活动描述"
-// rows={3}
-// />
-// </div>
-// <div className="dialog-buttons">
-// <button onClick={handleCreatePromotion}>确定</button>
-// <button onClick={closeCreateDialog}>取消</button>
-// </div>
-// </div>
-// </div>
-// )}
-// </div>
-// );
-// };
-
-// export default Promotion;
-
-
-// // import React, { useEffect, useState, useRef } from 'react';
-// // import './Promotion.css';
-// // import { useUser } from '../../../context/UserContext';
-
-// // const Promotion = () => {
-// // const { user } = useUser();
-// // const [promotions, setPromotions] = useState([]);
-// // const [torrents, setTorrents] = useState([]);
-// // const [loading, setLoading] = useState(true);
-// // const [promoIndex, setPromoIndex] = useState(0);
-// // const promoTimerRef = useRef(null);
-
-// // // 新增:控制模态框显示与表单状态
-// // const [showCreateModal, setShowCreateModal] = useState(false);
-// // const [formData, setFormData] = useState({
-// // name: '',
-// // description: '',
-// // discountPercentage: 0,
-// // startTime: '',
-// // endTime: '',
-// // applicableTorrentIds: [],
-// // });
-
-// // useEffect(() => {
-// // fetchData();
-// // fetchTorrentList();
-// // }, []);
-
-// // useEffect(() => {
-// // if (promotions.length === 0) return;
-// // clearInterval(promoTimerRef.current);
-// // promoTimerRef.current = setInterval(() => {
-// // setPromoIndex(prev => (prev + 1) % promotions.length);
-// // }, 5000);
-// // return () => clearInterval(promoTimerRef.current);
-// // }, [promotions]);
-
-// // const fetchData = async () => {
-// // try {
-// // const response = await fetch('/seeds/promotions');
-// // const json = await response.json();
-// // const promoData = Array.isArray(json?.data) ? json.data : [];
-// // setPromotions(promoData);
-// // } catch (error) {
-// // console.error('获取促销活动失败:', error);
-// // } finally {
-// // setLoading(false);
-// // }
-// // };
-
-// // const fetchTorrentList = async () => {
-// // try {
-// // const response = await fetch('/seeds/list');
-// // const json = await response.json();
-// // const torrentList = Array.isArray(json?.data) ? json.data : [];
-// // setTorrents(torrentList);
-// // } catch (error) {
-// // console.error('获取种子列表失败:', error);
-// // }
-// // };
-
-// // // 打开模态框时,重置表单数据,默认设置时间并填入所有种子ID
-// // const openCreateModal = () => {
-// // if (torrents.length === 0) {
-// // alert('没有可用的种子,请先上传种子');
-// // return;
-// // }
-// // setFormData({
-// // name: '',
-// // description: '',
-// // discountPercentage: 20,
-// // startTime: new Date().toISOString().slice(0, 16), // 用于datetime-local输入框,格式 YYYY-MM-DDTHH:mm
-// // endTime: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 16),
-// // applicableTorrentIds: torrents.map(t => t.id),
-// // });
-// // setShowCreateModal(true);
-// // };
-
-// // // 表单输入处理
-// // const handleInputChange = (e) => {
-// // const { name, value } = e.target;
-// // setFormData(prev => ({
-// // ...prev,
-// // [name]: name === 'discountPercentage' ? Number(value) : value,
-// // }));
-// // };
-
-// // // 点击确定提交创建
-// // const handleCreateConfirm = async () => {
-// // if (!formData.name) {
-// // alert('促销名称不能为空');
-// // return;
-// // }
-// // if (!formData.startTime || !formData.endTime) {
-// // alert('请选择开始时间和结束时间');
-// // return;
-// // }
-// // if (formData.discountPercentage <= 0 || formData.discountPercentage >= 100) {
-// // alert('折扣百分比应在1-99之间');
-// // return;
-// // }
-// // if (!formData.applicableTorrentIds.length) {
-// // alert('请选择适用的种子');
-// // return;
-// // }
-
-// // // 准备发送数据,适配后端字段名
-// // const newPromo = {
-// // name: formData.name,
-// // description: formData.description,
-// // discountPercentage: formData.discountPercentage,
-// // startTime: new Date(formData.startTime).toISOString(),
-// // endTime: new Date(formData.endTime).toISOString(),
-// // applicableTorrentIds: formData.applicableTorrentIds,
-// // };
-
-// // try {
-// // const res = await fetch('/seeds/promotions', {
-// // method: 'POST',
-// // headers: { 'Content-Type': 'application/json' },
-// // body: JSON.stringify(newPromo),
-// // });
-// // const json = await res.json();
-// // if (json.code === 200) {
-// // alert('促销活动创建成功');
-// // setShowCreateModal(false);
-// // fetchData();
-// // } else {
-// // alert('创建失败: ' + (json.msg || '未知错误'));
-// // }
-// // } catch (err) {
-// // console.error('创建促销失败:', err);
-// // alert('创建促销失败');
-// // }
-// // };
-
-// // const handleCancel = () => {
-// // setShowCreateModal(false);
-// // };
-
-// // const handleDeletePromotion = async (promotionId) => {
-// // if (!window.confirm('确认删除该促销活动吗?')) return;
-
-// // try {
-// // const res = await fetch(`/seeds/promotions/${promotionId}`, { method: 'DELETE' });
-// // const json = await res.json();
-// // if (json.success) {
-// // alert('删除成功');
-// // fetchData();
-// // } else {
-// // alert('删除失败: ' + json.message);
-// // }
-// // } catch (err) {
-// // console.error('删除失败:', err);
-// // }
-// // };
-
-// // const isAdmin = user?.role === 'admin';
-// // const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
-// // const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
-// // const currentPromo = promotions[promoIndex];
-
-// // if (loading) {
-// // return <div className="promotion-container">加载中...</div>;
-// // }
-
-// // return (
-// // <div className="promotion-container carousel-container">
-// // <section className="carousel-section">
-// // <h2>当前促销活动</h2>
-
-// // {isAdmin && (
-// // <button className="create-btn" onClick={openCreateModal}>
-// // 创建促销活动
-// // </button>
-// // )}
-
-// // {promotions.length === 0 || !currentPromo ? (
-// // <div className="empty-state">暂无促销活动</div>
-// // ) : (
-// // <div
-// // className="carousel"
-// // onMouseEnter={() => clearInterval(promoTimerRef.current)}
-// // onMouseLeave={() => {
-// // promoTimerRef.current = setInterval(() => {
-// // setPromoIndex(prev => (prev + 1) % promotions.length);
-// // }, 3000);
-// // }}
-// // >
-// // <button className="arrow left" onClick={prevPromo}><</button>
-// // <div className="slide">
-// // <div><strong>促销名称:</strong>{currentPromo?.name ?? '未知'}</div>
-// // <div><strong>促销时间:</strong>
-// // {currentPromo?.startTime && currentPromo?.endTime
-// // ? `${new Date(currentPromo.startTime).toLocaleString()} ~ ${new Date(currentPromo.endTime).toLocaleString()}`
-// // : '未知'}
-// // </div>
-// // <div><strong>折扣百分比:</strong>{currentPromo?.discountPercentage ?? '无'}</div>
-// // {currentPromo?.description && (
-// // <div><strong>描述:</strong>{currentPromo.description}</div>
-// // )}
-// // {isAdmin && (
-// // <button className="delete-btn" onClick={() => handleDeletePromotion(currentPromo.id)}>
-// // 删除该活动
-// // </button>
-// // )}
-// // </div>
-// // <button className="arrow right" onClick={nextPromo}>></button>
-// // </div>
-// // )}
-// // </section>
-
-// // {/* 创建促销模态框 */}
-// // {showCreateModal && (
-// // <div className="modal-overlay">
-// // <div className="modal-content">
-// // <h3>创建促销活动</h3>
-// // <label>
-// // 促销名称:
-// // <input
-// // type="text"
-// // name="name"
-// // value={formData.name}
-// // onChange={handleInputChange}
-// // />
-// // </label>
-// // <label>
-// // 描述:
-// // <textarea
-// // name="description"
-// // value={formData.description}
-// // onChange={handleInputChange}
-// // rows={3}
-// // />
-// // </label>
-// // <label>
-// // 折扣百分比:
-// // <input
-// // type="number"
-// // name="discountPercentage"
-// // value={formData.discountPercentage}
-// // min={1}
-// // max={99}
-// // onChange={handleInputChange}
-// // />
-// // </label>
-// // <label>
-// // 开始时间:
-// // <input
-// // type="datetime-local"
-// // name="startTime"
-// // value={formData.startTime}
-// // onChange={handleInputChange}
-// // />
-// // </label>
-// // <label>
-// // 结束时间:
-// // <input
-// // type="datetime-local"
-// // name="endTime"
-// // value={formData.endTime}
-// // onChange={handleInputChange}
-// // />
-// // </label>
-// // <label>
-// // 适用种子ID(逗号分隔,可留空默认所有):
-// // <input
-// // type="text"
-// // name="applicableTorrentIds"
-// // value={formData.applicableTorrentIds.join(',')}
-// // onChange={(e) => {
-// // const ids = e.target.value
-// // .split(',')
-// // .map(id => id.trim())
-// // .filter(id => id !== '')
-// // .map(id => Number(id))
-// // .filter(id => !isNaN(id));
-// // setFormData(prev => ({ ...prev, applicableTorrentIds: ids }));
-// // }}
-// // />
-// // </label>
-
-// // <div className="modal-buttons">
-// // <button onClick={handleCreateConfirm}>确定</button>
-// // <button onClick={handleCancel}>取消</button>
-// // </div>
-// // </div>
-// // </div>
-// // )}
-
-// // {/* 模态框简单样式 */}
-// // <style>{`
-// // .modal-overlay {
-// // position: fixed;
-// // top: 0; left: 0; right: 0; bottom: 0;
-// // background: rgba(0,0,0,0.4);
-// // display: flex;
-// // justify-content: center;
-// // align-items: center;
-// // z-index: 999;
-// // }
-// // .modal-content {
-// // background: white;
-// // padding: 20px;
-// // border-radius: 6px;
-// // width: 320px;
-// // max-width: 90%;
-// // }
-// // .modal-content label {
-// // display: block;
-// // margin-bottom: 10px;
-// // font-size: 14px;
-// // }
-// // .modal-content input[type="text"],
-// // .modal-content input[type="number"],
-// // .modal-content input[type="datetime-local"],
-// // .modal-content textarea {
-// // width: 100%;
-// // box-sizing: border-box;
-// // padding: 5px;
-// // font-size: 14px;
-// // margin-top: 4px;
-// // }
-// // .modal-buttons {
-// // margin-top: 15px;
-// // text-align: right;
-// // }
-// // .modal-buttons button {
-// // margin-left: 10px;
-// // padding: 6px 12px;
-// // font-size: 14px;
-// // }
-// // `}</style>
-// // </div>
-// // );
-// // };
-
-// // export default Promotion;
-
-
-// // import React, { useEffect, useState, useRef } from 'react';
-// // import './Promotion.css';
-// // import { useUser } from '../../../context/UserContext';
-
-// // const Promotion = () => {
-// // const { user } = useUser();
-// // const [promotions, setPromotions] = useState([]);
-// // const [torrents, setTorrents] = useState([]); // 新增,存放种子列表
-// // const [loading, setLoading] = useState(true);
-// // const [promoIndex, setPromoIndex] = useState(0);
-// // const promoTimerRef = useRef(null);
-
-// // useEffect(() => {
-// // fetchData();
-// // fetchTorrentList(); // 新增,获取种子列表
-// // }, []);
-
-// // useEffect(() => {
-// // if (promotions.length === 0) return;
-// // clearInterval(promoTimerRef.current);
-// // promoTimerRef.current = setInterval(() => {
-// // setPromoIndex(prev => (prev + 1) % promotions.length);
-// // }, 5000);
-// // return () => clearInterval(promoTimerRef.current);
-// // }, [promotions]);
-
-// // // 获取促销数据
-// // const fetchData = async () => {
-// // try {
-// // const response = await fetch('/seeds/promotions');
-// // const json = await response.json();
-// // const promoData = Array.isArray(json?.data) ? json.data : [];
-// // setPromotions(promoData);
-// // } catch (error) {
-// // console.error('获取促销活动失败:', error);
-// // } finally {
-// // setLoading(false);
-// // }
-// // };
-
-// // // 获取种子列表,赋值给torrents
-// // const fetchTorrentList = async () => {
-// // try {
-// // const response = await fetch('/seeds/list');
-// // const json = await response.json();
-// // const torrentList = Array.isArray(json?.data) ? json.data : [];
-// // setTorrents(torrentList);
-// // } catch (error) {
-// // console.error('获取种子列表失败:', error);
-// // }
-// // };
-
-// // // 创建促销时,自动使用当前种子的id列表,而不是写死
-// // const handleCreatePromotion = async () => {
-// // if (torrents.length === 0) {
-// // alert('没有可用的种子,请先上传种子');
-// // return;
-// // }
-
-// // const applicableTorrentIds = torrents.map(t => t.id); // 获取所有种子id数组
-
-// // const newPromo = {
-// // name: '测试促销活动',
-// // startTime: new Date().toISOString(),
-// // endTime: new Date(Date.now() + 7 * 86400000).toISOString(),
-// // discountPercentage: 20,
-// // applicableTorrentIds: applicableTorrentIds, // 动态传入种子ID数组
-// // description: '这是一个测试促销活动'
-// // };
-
-// // try {
-// // const res = await fetch('/seeds/promotions', {
-// // method: 'POST',
-// // headers: {
-// // 'Content-Type': 'application/json'
-// // },
-// // body: JSON.stringify(newPromo)
-// // });
-// // const json = await res.json();
-// // if (json.code === 200) {
-// // alert('促销活动创建成功');
-// // fetchData();
-// // } else {
-// // alert('创建失败: ' + (json.msg || '未知错误'));
-// // }
-// // } catch (err) {
-// // console.error('创建促销失败:', err);
-// // alert('创建促销失败');
-// // }
-// // };
-
-// // const handleDeletePromotion = async (promotionId) => {
-// // if (!window.confirm('确认删除该促销活动吗?')) return;
-
-// // try {
-// // const res = await fetch(`/seeds/promotions/${promotionId}`, {
-// // method: 'DELETE'
-// // });
-// // const json = await res.json();
-// // if (json.success) {
-// // alert('删除成功');
-// // fetchData();
-// // } else {
-// // alert('删除失败: ' + json.message);
-// // }
-// // } catch (err) {
-// // console.error('删除失败:', err);
-// // }
-// // };
-
-// // const isAdmin = user?.role === 'admin';
-// // const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
-// // const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
-// // const currentPromo = promotions[promoIndex];
-
-// // if (loading) {
-// // return <div className="promotion-container">加载中...</div>;
-// // }
-
-// // return (
-// // <div className="promotion-container carousel-container">
-// // <section className="carousel-section">
-// // <h2>当前促销活动</h2>
-
-// // {isAdmin && (
-// // <button className="create-btn" onClick={handleCreatePromotion}>
-// // 创建促销活动
-// // </button>
-// // )}
-
-// // {promotions.length === 0 || !currentPromo ? (
-// // <div className="empty-state">暂无促销活动</div>
-// // ) : (
-// // <div
-// // className="carousel"
-// // onMouseEnter={() => clearInterval(promoTimerRef.current)}
-// // onMouseLeave={() => {
-// // promoTimerRef.current = setInterval(() => {
-// // setPromoIndex(prev => (prev + 1) % promotions.length);
-// // }, 3000);
-// // }}
-// // >
-// // <button className="arrow left" onClick={prevPromo}><</button>
-// // <div className="slide">
-// // <div><strong>促销名称:</strong>{currentPromo?.name ?? '未知'}</div>
-// // <div><strong>促销时间:</strong>
-// // {currentPromo?.pStartTime && currentPromo?.pEndTime
-// // ? `${new Date(currentPromo.pStartTime).toLocaleString()} ~ ${new Date(currentPromo.pEndTime).toLocaleString()}`
-// // : '未知'}
-// // </div>
-// // <div><strong>上传奖励系数:</strong>{currentPromo?.uploadCoeff ?? '无'}</div>
-// // <div><strong>下载折扣系数:</strong>{currentPromo?.downloadCoeff ?? '无'}</div>
-// // {currentPromo?.description && (
-// // <div><strong>描述:</strong>{currentPromo.description}</div>
-// // )}
-// // {isAdmin && (
-// // <button className="delete-btn" onClick={() => handleDeletePromotion(currentPromo.id)}>
-// // 删除该活动
-// // </button>
-// // )}
-// // </div>
-// // <button className="arrow right" onClick={nextPromo}>></button>
-// // </div>
-// // )}
-// // </section>
-// // </div>
-// // );
-// // };
-
-// // export default Promotion;
-
-
import React, { useEffect, useState, useRef } from 'react';
import './Promotion.css';
import { useUser } from '../../../context/UserContext';
+import axios from 'axios';
+import PromotionCarousel from './PromotionCarousel';
+import PromotionDetailDialog from './PromotionDetailDialog';
+import TorrentDetailDialog from './TorrentDetailDialog';
+import CreatePromotionDialog from './CreatePromotionDialog';
+import CategoryPromotionDialog from './CategoryPromotionDialog';
+import ColdTorrentsDialog from './ColdTorrentsDialog';
const Promotion = () => {
const { user } = useUser();
@@ -835,20 +16,36 @@
const [loading, setLoading] = useState(true);
const [promoIndex, setPromoIndex] = useState(0);
const promoTimerRef = useRef(null);
-
- // 新增:控制创建对话框显示
const [showCreateDialog, setShowCreateDialog] = useState(false);
-
- // 创建促销活动表单状态
+ const [showCategoryDialog, setShowCategoryDialog] = useState(false);
const [formData, setFormData] = useState({
name: '',
startTime: '',
endTime: '',
discountPercentage: '',
- uploadCoeff: '',
- downloadCoeff: '',
- description: ''
+ applicableTorrentIds: []
});
+ const [categoryFormData, setCategoryFormData] = useState({
+ name: '',
+ startTime: '',
+ endTime: '',
+ discountPercentage: '',
+ category: 'movie'
+ });
+
+ // 冷门资源列表状态
+ const [coldTorrents, setColdTorrents] = useState([]);
+ const [showColdDialog, setShowColdDialog] = useState(false);
+ // 新增状态:控制促销对话框中冷门资源表格的显示
+ const [showPromoColdTable, setShowPromoColdTable] = useState(false);
+
+ // 新增状态:促销详情数据和详情对话框显示控制
+ const [promotionDetail, setPromotionDetail] = useState(null);
+ const [showDetailDialog, setShowDetailDialog] = useState(false);
+
+ // 新增状态:种子详情数据和种子详情对话框显示控制
+ const [torrentDetail, setTorrentDetail] = useState(null);
+ const [showTorrentDialog, setShowTorrentDialog] = useState(false);
useEffect(() => {
fetchData();
@@ -868,8 +65,11 @@
try {
const response = await fetch('/seeds/promotions');
const json = await response.json();
- const promoData = Array.isArray(json?.data) ? json.data : [];
- setPromotions(promoData);
+ if (json.code === 0 || json.code === 200) {
+ setPromotions(json.data || []);
+ } else {
+ console.error('获取促销活动失败:', json.msg);
+ }
} catch (error) {
console.error('获取促销活动失败:', error);
} finally {
@@ -877,38 +77,191 @@
}
};
+ const formatImageUrl = (url) => {
+ if (!url) return '';
+ const filename = url.split('/').pop();
+ return `http://localhost:5011/uploads/torrents/${filename}`;
+ };
+
+ // 修正后的获取种子详情函数
+ const fetchTorrentDetail = async (torrentId) => {
+ try {
+ // 修正参数名称为torrentId
+ const res = await axios.post(`/seeds/info/${torrentId}`);
+ if (res.data.code === 0) {
+ const seedData = res.data.data;
+
+ // 处理封面图片
+ let cover = seedData.imageUrl;
+ if (!cover && seedData.imgUrl) {
+ const imgs = seedData.imgUrl
+ .split(',')
+ .map((i) => i.trim())
+ .filter(Boolean);
+ cover = imgs.length > 0 ? formatImageUrl(imgs[0]) : null;
+ }
+
+ setTorrentDetail({...seedData, coverImage: cover});
+ setShowTorrentDialog(true);
+ } else {
+ alert(`获取种子详情失败: ${res.data.msg || '未知错误'}`);
+ }
+ } catch (err) {
+ console.error('获取种子详情失败:', err);
+ alert('获取种子详情失败');
+ }
+};
+
+
+const fetchPromotionDetail = async (promotionId) => {
+ try {
+ const response = await fetch(`/seeds/promotions/${promotionId}`);
+ const json = await response.json();
+ if (json.code === 0 || json.code === 200) {
+ // 正确解析applicableTorrentIds
+ const data = {
+ ...json.data,
+ applicableTorrentIds: parseTorrentIds(json.data.applicableTorrentIds)
+ };
+ setPromotionDetail(data);
+ setShowDetailDialog(true);
+ } else {
+ alert(`获取促销详情失败: ${json.msg || '未知错误'}`);
+ }
+ } catch (error) {
+ console.error('获取促销详情失败:', error);
+ alert('获取促销详情失败');
+ }
+};
+
+// 解析种子ID字符串为数组
+const parseTorrentIds = (idString) => {
+ try {
+ // 处理类似 "[69, 49, 6]" 的字符串
+ return JSON.parse(idString);
+ } catch (e) {
+ console.error('解析种子ID失败:', e);
+ return [];
+ }
+};
+
+ // 关闭详情对话框
+ const closeDetailDialog = () => {
+ setShowDetailDialog(false);
+ setPromotionDetail(null);
+ };
+
+ // 关闭种子详情对话框
+ const closeTorrentDialog = () => {
+ setShowTorrentDialog(false);
+ setTorrentDetail(null);
+ };
+
const fetchTorrentList = async () => {
try {
const response = await fetch('/seeds/list');
const json = await response.json();
- const torrentList = Array.isArray(json?.data) ? json.data : [];
- setTorrents(torrentList);
+ if (json.code === 0 || json.code === 200) {
+ // 为每个种子添加selected状态
+ const torrentsWithSelection = (json.data || []).map(torrent => ({
+ ...torrent,
+ selected: false
+ }));
+ setTorrents(torrentsWithSelection);
+ } else {
+ console.error('获取种子列表失败:', json.msg);
+ }
} catch (error) {
console.error('获取种子列表失败:', error);
}
};
- // 打开创建促销活动弹窗
+ const fetchColdTorrents = async () => {
+ try {
+ const response = await fetch('/seeds/cold');
+ const json = await response.json();
+ if (json.code === 0 || json.code === 200) {
+ setColdTorrents(json.data || []); // 存储冷门资源数据
+ setShowColdDialog(true); // 打开模态框
+ } else {
+ alert(`获取冷门资源失败: ${json.msg || '未知错误'}`);
+ }
+ } catch (error) {
+ console.error('获取冷门资源失败:', error);
+ alert('获取冷门资源失败');
+ }
+ };
+
+ // 从服务器获取冷门资源并显示在促销对话框中
+ const fetchPromoColdTorrents = async () => {
+ try {
+ const response = await fetch('/seeds/cold');
+ const json = await response.json();
+ if (json.code === 0 || json.code === 200) {
+ setColdTorrents(json.data || []);
+ setShowPromoColdTable(true);
+ } else {
+ alert(`获取冷门资源失败: ${json.msg || '未知错误'}`);
+ }
+ } catch (error) {
+ console.error('获取冷门资源失败:', error);
+ alert('获取冷门资源失败');
+ }
+ };
+
+ // 处理促销对话框中种子的选择
+ const handlePromoTorrentSelection = (torrentId, isChecked) => {
+ setFormData(prev => {
+ const ids = [...prev.applicableTorrentIds];
+ if (isChecked) {
+ ids.push(torrentId);
+ } else {
+ const index = ids.indexOf(torrentId);
+ if (index !== -1) ids.splice(index, 1);
+ }
+ return {
+ ...prev,
+ applicableTorrentIds: ids
+ };
+ });
+ };
+
+ // 关闭冷门资源模态框
+ const closeColdDialog = () => {
+ setShowColdDialog(false);
+ setColdTorrents([]);
+ };
+
const openCreateDialog = () => {
- // 重置表单数据
setFormData({
name: '',
startTime: '',
endTime: '',
discountPercentage: '',
- uploadCoeff: '',
- downloadCoeff: '',
- description: ''
+ applicableTorrentIds: []
});
setShowCreateDialog(true);
};
- // 关闭弹窗
const closeCreateDialog = () => {
setShowCreateDialog(false);
};
- // 处理表单输入变化
+ const openCategoryDialog = () => {
+ setCategoryFormData({
+ name: '',
+ startTime: '',
+ endTime: '',
+ discountPercentage: '',
+ category: 'movie'
+ });
+ setShowCategoryDialog(true);
+ };
+
+ const closeCategoryDialog = () => {
+ setShowCategoryDialog(false);
+ };
+
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
@@ -917,12 +270,46 @@
}));
};
- // 提交创建促销活动
+ const handleCategoryInputChange = (e) => {
+ const { name, value } = e.target;
+ setCategoryFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const formatSize = (bytes) => {
+ if (bytes === 0) return '0 B';
+
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+
+ return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ const handleTorrentSelection = (torrentId, isChecked) => {
+ setTorrents(prev => prev.map(torrent =>
+ torrent.id === torrentId
+ ? {...torrent, selected: isChecked}
+ : torrent
+ ));
+
+ setFormData(prev => {
+ const ids = [...prev.applicableTorrentIds];
+ if (isChecked) {
+ ids.push(torrentId);
+ } else {
+ const index = ids.indexOf(torrentId);
+ if (index !== -1) ids.splice(index, 1);
+ }
+ return {
+ ...prev,
+ applicableTorrentIds: ids
+ };
+ });
+ };
+
const handleCreatePromotion = async () => {
- if (torrents.length === 0) {
- alert('没有可用的种子,请先上传种子');
- return;
- }
if (!formData.name.trim()) {
alert('促销名称不能为空');
return;
@@ -939,21 +326,19 @@
alert('折扣百分比必须是数字');
return;
}
-
- const applicableTorrentIds = torrents.map(t => t.id);
+ if (formData.applicableTorrentIds.length === 0) {
+ alert('请至少选择一个适用的种子');
+ return;
+ }
const newPromo = {
name: formData.name,
startTime: new Date(formData.startTime).toISOString(),
endTime: new Date(formData.endTime).toISOString(),
discountPercentage: Number(formData.discountPercentage),
- uploadCoeff: formData.uploadCoeff ? Number(formData.uploadCoeff) : undefined,
- downloadCoeff: formData.downloadCoeff ? Number(formData.downloadCoeff) : undefined,
- applicableTorrentIds: applicableTorrentIds,
- description: formData.description
+ applicableTorrentIds: formData.applicableTorrentIds // 使用formData中的种子ID
};
-
try {
const res = await fetch('/seeds/promotions', {
method: 'POST',
@@ -961,12 +346,12 @@
body: JSON.stringify(newPromo)
});
const json = await res.json();
- if (json.code === 200 || json.code === 0) {
+ if (json.code === 0 || json.code === 200) {
alert('促销活动创建成功');
fetchData();
setShowCreateDialog(false);
} else {
- alert('创建失败: ' + (json.msg || '未知错误'));
+ alert(`创建失败: ${json.msg || '未知错误'}`);
}
} catch (err) {
console.error('创建促销失败:', err);
@@ -974,6 +359,51 @@
}
};
+ const handleCreateCategoryPromotion = async () => {
+ if (!categoryFormData.name.trim()) {
+ alert('促销名称不能为空');
+ return;
+ }
+ if (!categoryFormData.startTime || !categoryFormData.endTime) {
+ alert('促销开始时间和结束时间不能为空');
+ return;
+ }
+ if (new Date(categoryFormData.startTime) >= new Date(categoryFormData.endTime)) {
+ alert('促销结束时间必须晚于开始时间');
+ return;
+ }
+ if (!categoryFormData.discountPercentage || isNaN(categoryFormData.discountPercentage)) {
+ alert('折扣百分比必须是数字');
+ return;
+ }
+
+ const newPromo = {
+ name: categoryFormData.name,
+ startTime: new Date(categoryFormData.startTime).toISOString(),
+ endTime: new Date(categoryFormData.endTime).toISOString(),
+ discountPercentage: Number(categoryFormData.discountPercentage)
+ };
+
+ try {
+ const res = await fetch(`/seeds/promotions/category?category=${categoryFormData.category}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newPromo)
+ });
+ const json = await res.json();
+ if (json.code === 0 || json.code === 200) {
+ alert('分类促销活动创建成功');
+ fetchData();
+ setShowCategoryDialog(false);
+ } else {
+ alert(`创建失败: ${json.msg || '未知错误'}`);
+ }
+ } catch (err) {
+ console.error('创建分类促销失败:', err);
+ alert('创建分类促销失败');
+ }
+ };
+
const handleDeletePromotion = async (promotionId) => {
if (!window.confirm('确认删除该促销活动吗?')) return;
@@ -984,7 +414,7 @@
alert('删除成功');
fetchData();
} else {
- alert('删除失败: ' + (json.msg || '未知错误'));
+ alert(`删除失败: ${json.msg || '未知错误'}`);
}
} catch (err) {
console.error('删除失败:', err);
@@ -993,146 +423,93 @@
const isAdmin = user?.role === 'admin';
const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
- const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
+ const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
const currentPromo = promotions[promoIndex];
+ const formatDateTime = (dateTime) => {
+ if (!dateTime) return '未知';
+ return new Date(dateTime).toLocaleString();
+ };
+
+ const getUploadBonusDisplay = (promo) => {
+ if (!promo || !promo.discountPercentage) return '无';
+ const bonus = promo.discountPercentage;
+ return bonus > 0 ? `+${bonus}%` : `-${Math.abs(bonus)}%`;
+ };
+
+ const getDownloadDiscountDisplay = (promo) => {
+ if (!promo || !promo.downloadDiscount) return '无';
+ const discount = (1 - promo.downloadDiscount) * 100;
+ return discount > 0 ? `-${discount.toFixed(1)}%` : '无';
+ };
+
if (loading) {
return <div className="promotion-container">加载中...</div>;
- }
+ };
return (
<div className="promotion-container carousel-container">
- <section className="carousel-section">
- <h2>当前促销活动</h2>
+ <PromotionCarousel
+ promotions={promotions}
+ currentPromo={currentPromo}
+ prevPromo={prevPromo}
+ nextPromo={nextPromo}
+ isAdmin={isAdmin}
+ openCreateDialog={openCreateDialog}
+ openCategoryDialog={openCategoryDialog}
+ fetchColdTorrents={fetchColdTorrents}
+ handleDeletePromotion={handleDeletePromotion}
+ fetchPromotionDetail={fetchPromotionDetail}
+ />
- {isAdmin && (
- <button className="create-btn" onClick={openCreateDialog}>
- 创建促销活动
- </button>
- )}
+ {/* 新增:冷门资源模态框 */}
+ <ColdTorrentsDialog
+ showColdDialog={showColdDialog}
+ coldTorrents={coldTorrents}
+ closeColdDialog={closeColdDialog}
+ fetchTorrentDetail={fetchTorrentDetail}
+ />
- {promotions.length === 0 || !currentPromo ? (
- <div className="empty-state">暂无促销活动</div>
- ) : (
- <div
- className="carousel"
- onMouseEnter={() => clearInterval(promoTimerRef.current)}
- onMouseLeave={() => {
- promoTimerRef.current = setInterval(() => {
- setPromoIndex(prev => (prev + 1) % promotions.length);
- }, 3000);
- }}
- >
- <button className="arrow left" onClick={prevPromo}><</button>
- <div className="slide">
- <div><strong>促销名称:</strong>{currentPromo?.name ?? '未知'}</div>
- <div><strong>促销时间:</strong>
- {currentPromo?.pStartTime && currentPromo?.pEndTime
- ? `${new Date(currentPromo.pStartTime).toLocaleString()} ~ ${new Date(currentPromo.pEndTime).toLocaleString()}`
- : '未知'}
- </div>
- <div><strong>上传奖励系数:</strong>{currentPromo?.uploadCoeff ?? '无'}</div>
- <div><strong>下载折扣系数:</strong>{currentPromo?.downloadCoeff ?? '无'}</div>
- {currentPromo?.description && (
- <div><strong>描述:</strong>{currentPromo.description}</div>
- )}
- {isAdmin && (
- <button className="delete-btn" onClick={() => handleDeletePromotion(currentPromo.id)}>
- 删除该活动
- </button>
- )}
- </div>
- <button className="arrow right" onClick={nextPromo}>></button>
- </div>
- )}
- </section>
+ {/* 促销详情对话框 */}
+ <PromotionDetailDialog
+ showDetailDialog={showDetailDialog}
+ promotionDetail={promotionDetail}
+ closeDetailDialog={closeDetailDialog}
+ torrents={torrents}
+ fetchTorrentDetail={fetchTorrentDetail}
+ />
- {/* 创建促销活动弹窗 */}
- {showCreateDialog && (
- <div className="dialog-overlay">
- <div className="dialog">
- <h3>创建促销活动</h3>
- <div className="form-item">
- <label>促销名称:</label>
- <input
- type="text"
- name="name"
- value={formData.name}
- onChange={handleInputChange}
- placeholder="请输入促销名称"
- />
- </div>
- <div className="form-item">
- <label>开始时间:</label>
- <input
- type="datetime-local"
- name="startTime"
- value={formData.startTime}
- onChange={handleInputChange}
- />
- </div>
- <div className="form-item">
- <label>结束时间:</label>
- <input
- type="datetime-local"
- name="endTime"
- value={formData.endTime}
- onChange={handleInputChange}
- />
- </div>
- <div className="form-item">
- <label>折扣百分比(数字):</label>
- <input
- type="number"
- name="discountPercentage"
- value={formData.discountPercentage}
- onChange={handleInputChange}
- placeholder="例如:20 表示 20% 折扣"
- min="0"
- max="100"
- />
- </div>
- <div className="form-item">
- <label>上传奖励系数(可选):</label>
- <input
- type="number"
- name="uploadCoeff"
- value={formData.uploadCoeff}
- onChange={handleInputChange}
- placeholder="例如:1.5"
- step="0.1"
- />
- </div>
- <div className="form-item">
- <label>下载折扣系数(可选):</label>
- <input
- type="number"
- name="downloadCoeff"
- value={formData.downloadCoeff}
- onChange={handleInputChange}
- placeholder="例如:0.8"
- step="0.1"
- />
- </div>
- <div className="form-item">
- <label>描述(可选):</label>
- <textarea
- name="description"
- value={formData.description}
- onChange={handleInputChange}
- placeholder="促销活动描述"
- rows={3}
- />
- </div>
- <div className="dialog-buttons">
- <button onClick={handleCreatePromotion}>确定</button>
- <button onClick={closeCreateDialog}>取消</button>
- </div>
- </div>
- </div>
- )}
+ {/* 种子详情对话框 */}
+ <TorrentDetailDialog
+ showTorrentDialog={showTorrentDialog}
+ torrentDetail={torrentDetail}
+ closeTorrentDialog={closeTorrentDialog}
+ />
+
+ {/* 创建冷门资源促销弹窗 */}
+ <CreatePromotionDialog
+ showCreateDialog={showCreateDialog}
+ formData={formData}
+ handleInputChange={handleInputChange}
+ closeCreateDialog={closeCreateDialog}
+ handleCreatePromotion={handleCreatePromotion}
+ fetchPromoColdTorrents={fetchPromoColdTorrents}
+ showPromoColdTable={showPromoColdTable}
+ coldTorrents={coldTorrents}
+ handlePromoTorrentSelection={handlePromoTorrentSelection}
+ />
+
+ {/* 创建特定分类促销弹窗 */}
+ <CategoryPromotionDialog
+ showCategoryDialog={showCategoryDialog}
+ categoryFormData={categoryFormData}
+ handleCategoryInputChange={handleCategoryInputChange}
+ closeCategoryDialog={closeCategoryDialog}
+ handleCreateCategoryPromotion={handleCreateCategoryPromotion}
+ />
</div>
);
};
-export default Promotion;
+export default Promotion;
+
diff --git a/src/pages/Forum/promotion-part/PromotionCarousel.css b/src/pages/Forum/promotion-part/PromotionCarousel.css
new file mode 100644
index 0000000..330c886
--- /dev/null
+++ b/src/pages/Forum/promotion-part/PromotionCarousel.css
@@ -0,0 +1,150 @@
+/* 优化后的轮播样式 */
+.carousel-container {
+ display: flex;
+ margin-top: 20px;
+ margin-left: 2%;
+ margin-right: 2%;
+}
+
+.carousel-section {
+ flex: 1;
+}
+
+.carousel-section h2 {
+ font-size: 20px;
+ margin-bottom: 15px;
+}
+
+.carousel {
+ position: relative;
+ background: linear-gradient(135deg, #e1cab2, #b68791);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ border-radius: 12px;
+ padding: 24px 48px;
+ color: #fff;
+ min-height: 220px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.promotion-name {
+ font-size: 24px;
+ font-weight: bold;
+ margin-top: 12px;
+ margin-bottom: 14px;
+ color: #135c69;
+ text-align: center;
+}
+
+.carousel .arrow {
+ background: rgba(0, 0, 0, 0.3);
+ border: none;
+ color: #fff;
+ font-size: 24px;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ cursor: pointer;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ transition: background 0.3s;
+ z-index: 10;
+}
+
+.carousel .arrow.left {
+ left: 16px;
+}
+
+.carousel .arrow.right {
+ right: 16px;
+}
+
+.carousel .arrow:hover {
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.carousel .slide {
+ width: 100%;
+ max-width: 720px;
+ text-align: left;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ line-height: 1.6;
+}
+
+.carousel .slide strong {
+ font-weight: 600;
+}
+
+.carousel .action-buttons {
+ display: flex;
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.carousel .action-buttons button {
+ padding: 6px 12px;
+ font-size: 14px;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.carousel .delete-btn {
+ background-color: #d9534f;
+ color: white;
+}
+
+.carousel .delete-btn:hover {
+ background-color: #c9302c;
+}
+
+.carousel .view-btn {
+ background-color: #5bc0de;
+ color: white;
+}
+
+.carousel .view-btn:hover {
+ background-color: #31b0d5;
+}
+
+/* 轮播指示器容器 */
+.carousel-indicators {
+ margin-top: 5%;
+ position: absolute;
+ bottom: 16px; /* 距离轮播底部的间距,可调整 */
+ left: 50%;
+ transform: translateX(-50%); /* 水平居中 */
+ display: flex;
+ align-items: center;
+ gap: 8px; /* 圆点间距 */
+ z-index: 10; /* 确保在内容上层 */
+}
+
+/* 单个圆点 */
+.carousel-indicators .indicator {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%; /* 圆形 */
+ background-color: rgba(255, 255, 255, 0.4); /* 未选中状态颜色 */
+ cursor: pointer;
+ transition: background-color 0.3s ease; /* 渐变过渡 */
+}
+
+/* 选中状态的圆点 */
+.carousel-indicators .indicator.active {
+ background-color: #fff; /* 选中时的高亮颜色 */
+}
+
+
+/* 冷门资源专用 slide */
+.cold-slide {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
diff --git a/src/pages/Forum/promotion-part/PromotionCarousel.jsx b/src/pages/Forum/promotion-part/PromotionCarousel.jsx
new file mode 100644
index 0000000..6408c54
--- /dev/null
+++ b/src/pages/Forum/promotion-part/PromotionCarousel.jsx
@@ -0,0 +1,143 @@
+import React, { useState } from 'react';
+import './PromotionCarousel.css';
+
+const PromotionCarousel = ({
+ promotions,
+ isAdmin,
+ openCreateDialog,
+ openCategoryDialog,
+ fetchColdTorrents,
+ handleDeletePromotion,
+ fetchPromotionDetail
+}) => {
+ // 1. 新增状态:跟踪当前轮播项的索引
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ // 2. 上一个/下一个轮播的逻辑
+ const prevPromo = () => {
+ if (promotions.length === 0) return;
+ setCurrentIndex(prev =>
+ prev === 0 ? promotions.length - 1 : prev - 1
+ );
+ };
+
+ const nextPromo = () => {
+ if (promotions.length === 0) return;
+ setCurrentIndex(prev =>
+ prev === promotions.length - 1 ? 0 : prev + 1
+ );
+ };
+
+ // 3. 当前显示的促销项(从数组中取)
+ const currentPromo = promotions[currentIndex];
+
+ // 原有工具函数保持不变
+ const formatDateTime = (dateTime) => {
+ if (!dateTime) return '未知';
+ // dateTime [2025,1,3,12,10]
+ return new Date(...dateTime).toLocaleString();
+
+ };
+
+ // 修改为一个统一的处理函数,避免混淆
+const getUploadBonusDisplay = (promo) => {
+ if (!promo || typeof promo.discountPercentage !== 'number') return '无';
+ const bonus = promo.discountPercentage;
+ return bonus > 0 ? `${(bonus * 100).toFixed(0)}%` : '无';
+};
+
+const getDownloadDiscountDisplay = (promo) => {
+ if (!promo || typeof promo.discountPercentage !== 'number') return '无';
+ const discount = promo.discountPercentage;
+ return discount < 0 ? `${Math.abs(discount * 100).toFixed(0)}%` : '无';
+};
+
+
+ if (!Array.isArray(promotions) || promotions.length === 0 || !promotions[currentIndex]) {
+ return (
+ <section className="carousel-section">
+ <h2 style={{ marginLeft: '1.6rem' }}>当前促销活动</h2>
+ {isAdmin && (
+ <div style={{ display: 'flex', gap: '0px', marginBottom: '10px' }}>
+ <button className="create-btn" onClick={openCreateDialog}>
+ +创建冷门资源促销
+ </button>
+ <button className="create-btn" onClick={openCategoryDialog}>
+ +创建特定分类促销
+ </button>
+ </div>
+ )}
+ <div className="empty-state">暂无促销活动</div>
+ </section>
+ );
+}
+
+
+ return (
+ <section className="carousel-section">
+ <h2 style={{ marginLeft: '1.6rem' }}>当前促销活动</h2>
+
+ {isAdmin && (
+ <div style={{ display: 'flex', gap: '0px', marginBottom: '10px' }}>
+ <button className="create-btn" onClick={openCreateDialog}>
+ +创建冷门资源促销
+ </button>
+ <button className="create-btn" onClick={openCategoryDialog}>
+ +创建特定分类促销
+ </button>
+ </div>
+ )}
+
+ <div
+ className="carousel"
+ onMouseEnter={() => {}}
+ onMouseLeave={() => {}}
+ >
+
+ <button className="arrow left" onClick={prevPromo}><</button>
+ <div className="slide">
+ <div className='promotion-name'><strong>{currentPromo?.name ?? '未知'}</strong></div>
+ <div style={{ color: '#135c69' }}><strong>促销时间:</strong>
+ {currentPromo?.startTime && currentPromo?.endTime
+ ? `${formatDateTime(currentPromo.startTime)} ~ ${formatDateTime(currentPromo.endTime)}`
+ : '未知'}
+ </div>
+ <div style={{ color: '#135c69' }}><strong>上传奖励:</strong>{getUploadBonusDisplay(currentPromo)}</div>
+ <div style={{ color: '#135c69' }}><strong>下载折扣:</strong>{getDownloadDiscountDisplay(currentPromo)}</div>
+ {currentPromo?.description && (
+ <div><strong>描述:</strong>{currentPromo.description}</div>
+ )}
+
+ <div className="action-buttons" style={{ display: 'flex', gap: '10px' }}>
+ {isAdmin && (
+ <button className="delete-btn" onClick={() => handleDeletePromotion(currentPromo.id)}>
+ 删除活动
+ </button>
+ )}
+ <button
+ className="view-btn"
+ onClick={() => fetchPromotionDetail(currentPromo.id)}
+ aria-label={`查看${currentPromo.name}的详情`}
+ >
+ 查看详情
+ </button>
+ </div>
+ </div>
+ <button className="arrow right" onClick={nextPromo}>></button>
+
+ {/* 4. 新增:底部指示器 */}
+ <div className="carousel-indicators">
+ {promotions.map((_, index) => (
+ <div
+ key={index}
+ className={`indicator ${currentIndex === index ? 'active' : ''}`}
+ onClick={() => setCurrentIndex(index)} // 点击切换轮播项
+ />
+ ))}
+ </div>
+ </div>
+ </section>
+ );
+};
+
+export default PromotionCarousel;
\ No newline at end of file
diff --git a/src/pages/Forum/promotion-part/PromotionDetailDialog.jsx b/src/pages/Forum/promotion-part/PromotionDetailDialog.jsx
new file mode 100644
index 0000000..5310c79
--- /dev/null
+++ b/src/pages/Forum/promotion-part/PromotionDetailDialog.jsx
@@ -0,0 +1,166 @@
+import { type } from '@testing-library/user-event/dist/type';
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const PromotionDetailDialog = ({
+ showDetailDialog,
+ promotionDetail,
+ closeDetailDialog,
+ torrents,
+ fetchTorrentDetail
+}) => {
+ const navigate = useNavigate();
+
+ // 处理API返回的日期数组格式
+ const parseApiDate = (dateArray) => {
+ if (!Array.isArray(dateArray) || dateArray.length < 3) return null;
+
+ // 注意:JavaScript的Date月份是从0开始的,所以需要减1
+ return new Date(
+ dateArray[0], // 年
+ dateArray[1] - 1, // 月 (减1)
+ dateArray[2], // 日
+ dateArray[3] || 0, // 时 (默认0)
+ dateArray[4] || 0, // 分 (默认0)
+ dateArray[5] || 0 // 秒 (默认0)
+ );
+ };
+
+ const formatDateTime = (dateArray) => {
+ if (!dateArray) return '未知';
+
+ try {
+ const date = parseApiDate(dateArray);
+ return date ? date.toLocaleString() : '格式错误';
+ } catch (e) {
+ return '格式错误';
+ }
+ };
+
+ const getUploadBonusDisplay = (promo) => {
+ if (!promo || !promo.discountPercentage) return '无';
+ const bonus = promo.discountPercentage;
+ return bonus > 0 ? `+${bonus}%` : `-${Math.abs(bonus)}%`;
+ };
+
+ const getDownloadDiscountDisplay = (promo) => {
+ if (!promo || !promo.downloadDiscount) return '无';
+ const discount = (1 - promo.downloadDiscount) * 100;
+ return discount > 0 ? `-${discount.toFixed(1)}%` : '无';
+ };
+
+ const redirectToTorrentDetail = (torrentId) => {
+ if (fetchTorrentDetail) {
+ navigate(`/seed/${torrentId}`);
+ }
+ };
+
+ return (
+ showDetailDialog && promotionDetail && (
+ <div className="detail-dialog-overlay">
+ <div className="detail-dialog">
+ <h3 className="detail-dialog-title">{promotionDetail.name}</h3>
+ <button
+ className="close-btn"
+ onClick={closeDetailDialog}
+ >
+ ×
+ </button>
+
+ <div className="detail-content">
+ {/* <div className="detail-item"> */}
+ {/* <span className="detail-label">促销ID:</span> */}
+ {/* <span className="detail-value">{promotionDetail.id}</span>
+ </div> */}
+ {/* <div className="detail-item">
+ <span className="detail-label">促销名称:</span>
+ <span className="detail-value">{promotionDetail.name}</span>
+ </div> */}
+ <div className="detail-item">
+ <span className="detail-label">开始时间:</span>
+ <span className="detail-value">{formatDateTime(promotionDetail.startTime)}</span>
+ </div>
+ <div className="detail-item">
+ <span className="detail-label">结束时间:</span>
+ <span className="detail-value">{formatDateTime(promotionDetail.endTime)}</span>
+ </div>
+ <div className="detail-item">
+ <span className="detail-label">上传奖励:</span>
+ <span className="detail-value">{getUploadBonusDisplay(promotionDetail)}</span>
+ </div>
+ <div className="detail-item">
+ <span className="detail-label">下载折扣:</span>
+ <span className="detail-value">{getDownloadDiscountDisplay(promotionDetail)}</span>
+ </div>
+ {/* <div className="detail-item">
+ <span className="detail-label">创建时间:</span>
+ <span className="detail-value">{formatDateTime(promotionDetail.createTime)}</span>
+ </div> */}
+ {/* <div className="detail-item">
+ <span className="detail-label">创建者:</span>
+ <span className="detail-value">{promotionDetail.creator || '未知'}</span>
+ </div> */}
+
+ {promotionDetail.description && (
+ <div className="detail-item">
+ <span className="detail-label">描述:</span>
+ <span className="detail-value">{promotionDetail.description}</span>
+ </div>
+ )}
+
+ {/* 适用种子列表 */}
+ <div className="detail-item">
+ <span className="detail-label">适用种子:</span>
+ <div className="detail-value">
+ {promotionDetail.applicableTorrentIds ? (
+ <div className="torrent-list">
+ {/* 解析字符串形式的数组 */}
+ {parseTorrentIds(promotionDetail.applicableTorrentIds).map(torrentId => {
+ const matchedTorrent = torrents.find(t => t.id === torrentId) || {};
+
+ return (
+ <button
+ key={torrentId}
+ className="torrent-link"
+ onClick={() => redirectToTorrentDetail(torrentId)}
+ aria-label={`查看种子${matchedTorrent.name || torrentId}的详情`}
+ >
+ {matchedTorrent.name || `种子${torrentId}`}
+ {/* {matchedTorrent.seeders > 10 && <span className="status-indicator hot">热门</span>}
+ {matchedTorrent.seeders === 0 && <span className="status-indicator cold">冷门</span>} */}
+ </button>
+ );
+ })}
+ </div>
+ ) : (
+ <span className="empty-list">暂无适用种子</span>
+ )}
+ </div>
+ </div>
+ </div>
+
+ <div className="dialog-buttons">
+ <button onClick={closeDetailDialog}>关闭</button>
+ </div>
+ </div>
+ </div>
+ )
+ );
+};
+
+// 解析种子ID字符串为数组
+const parseTorrentIds = (idString) => {
+ if (typeof idString === 'number') {
+ return [idString];
+ }
+
+ if (Array.isArray(idString)) {
+ return idString;
+ }
+
+ let items = typeof idString === 'string' && !idString.startsWith('[') ? idString.split(',') : JSON.parse(idString || '[]');
+
+ return items;
+};
+
+export default PromotionDetailDialog;
\ No newline at end of file
diff --git a/src/pages/Forum/promotion-part/TorrentDetailDialog.jsx b/src/pages/Forum/promotion-part/TorrentDetailDialog.jsx
new file mode 100644
index 0000000..f038b90
--- /dev/null
+++ b/src/pages/Forum/promotion-part/TorrentDetailDialog.jsx
@@ -0,0 +1,130 @@
+import React from 'react';
+
+const TorrentDetailDialog = ({
+ showTorrentDialog,
+ torrentDetail,
+ closeTorrentDialog
+}) => {
+ const formatSize = (bytes) => {
+ if (bytes === 0) return '0 B';
+
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+
+ return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ const formatDateTime = (dateTime) => {
+ if (!dateTime) return '未知';
+ return new Date(dateTime).toLocaleString();
+ };
+
+ return (
+ showTorrentDialog && torrentDetail && (
+ <div className="torrent-detail-overlay">
+ <div className="torrent-detail-dialog">
+ <h3 className="torrent-detail-title">种子详情 - {torrentDetail.title}</h3>
+ <button
+ className="close-btn"
+ onClick={closeTorrentDialog}
+ >
+ ×
+ </button>
+
+ {/* 封面图片 */}
+ {torrentDetail.coverImage && (
+ <div className="torrent-cover-container">
+ <img
+ src={torrentDetail.coverImage}
+ alt={`${torrentDetail.name}封面`}
+ className="torrent-cover"
+ />
+ </div>
+ )}
+
+ <div className="torrent-detail-content">
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">种子ID:</span>
+ <span className="torrent-detail-value">{torrentDetail.id}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">种子名称:</span>
+ <span className="torrent-detail-value">{torrentDetail.title}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">分类:</span>
+ <span className="torrent-detail-value">{torrentDetail.category || '未分类'}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">大小:</span>
+ <span className="torrent-detail-value">{formatSize(torrentDetail.size)}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">上传者:</span>
+ <span className="torrent-detail-value">{torrentDetail.username || '匿名'}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">上传时间:</span>
+ <span className="torrent-detail-value">{formatDateTime(torrentDetail.createTime)}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">做种人数:</span>
+ <span className="torrent-detail-value">{torrentDetail.seeders || 0}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">下载人数:</span>
+ <span className="torrent-detail-value">{torrentDetail.leechers || 0}</span>
+ </div>
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">完成次数:</span>
+ <span className="torrent-detail-value">{torrentDetail.completed || 0}</span>
+ </div>
+
+ {/* 种子状态 */}
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">状态:</span>
+ <span className="torrent-detail-value">
+ {torrentDetail.seeders > 10 ? (
+ <span className="status-badge hot">热门</span>
+ ) : torrentDetail.seeders === 0 ? (
+ <span className="status-badge cold">冷门</span>
+ ) : (
+ <span className="status-badge normal">普通</span>
+ )}
+ </span>
+ </div>
+
+ {/* 种子描述 */}
+ {torrentDetail.description && (
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">描述:</span>
+ <div className="torrent-detail-value description">{torrentDetail.description}</div>
+ </div>
+ )}
+
+ {/* 下载链接 */}
+ {torrentDetail.downloadUrl && (
+ <div className="torrent-detail-item">
+ <span className="torrent-detail-label">下载:</span>
+ <a
+ href={torrentDetail.downloadUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="download-link"
+ >
+ <i className="fa fa-download"></i> 下载种子
+ </a>
+ </div>
+ )}
+ </div>
+
+ <div className="dialog-buttons">
+ <button onClick={closeTorrentDialog}>关闭</button>
+ </div>
+ </div>
+ </div>
+ )
+ );
+};
+
+export default TorrentDetailDialog;
\ No newline at end of file