Merge "修复种子列表搜索排序"
diff --git a/src/components/Header.css b/src/components/Header.css
index 66fe1a0..a19f9e9 100644
--- a/src/components/Header.css
+++ b/src/components/Header.css
@@ -4,7 +4,7 @@
justify-content: space-between;
align-items: center;
padding: 10px 20px;
- background-color: #6b4f3b; /* 深棕色背景 */
+ background-color: #6c3e28; /* 深棕色背景 */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
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
diff --git a/src/pages/FriendMoments/FriendMoments.css b/src/pages/FriendMoments/FriendMoments.css
index edab68f..d383c27 100644
--- a/src/pages/FriendMoments/FriendMoments.css
+++ b/src/pages/FriendMoments/FriendMoments.css
@@ -1,12 +1,3 @@
-.user-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- object-fit: cover;
- border: 1.5px solid #ddd;
- box-shadow: 0 1px 4px rgba(0,0,0,0.1);
-}
-
.friend-moments-container {
margin: 0 auto;
background: #f8f3ef;
@@ -90,9 +81,13 @@
.delete-btn {
background: none;
border: none;
- color: #f44;
+ color: rgb(139, 31, 31);
cursor: pointer;
- font-size: 12px;
+ /* font-size: 12px; */
+}
+
+.delete-btn:hover {
+ background: #e9ded2;
}
.modal-overlay {
@@ -188,17 +183,6 @@
margin-bottom: 10px;
}
-.feed-title {
- font-size: 1.2em;
- font-weight: bold;
- margin-bottom: 8px;
-}
-
-.feed-content {
- margin-bottom: 12px;
- line-height: 1.5;
-}
-
.feed-images {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
@@ -235,13 +219,6 @@
color: #555;
}
-.delete-btn {
- background: none;
- border: none;
- color: #f44336;
- cursor: pointer;
-}
-
.comment-box {
margin-top: 12px;
}
@@ -284,6 +261,8 @@
}
.comment-user {
+ margin-bottom: 0.5%;
+ font-size: 1.2em;
font-weight: bold;
color: #333;
}
@@ -310,6 +289,7 @@
font-size: 0.9em;
cursor: pointer;
padding: 0;
+ margin-left: 0px;
}
.nested-replies {
diff --git a/src/pages/FriendMoments/FriendMoments.jsx b/src/pages/FriendMoments/FriendMoments.jsx
index ab1aef2..0d92303 100644
--- a/src/pages/FriendMoments/FriendMoments.jsx
+++ b/src/pages/FriendMoments/FriendMoments.jsx
@@ -1,1341 +1,3 @@
-// // FriendMoments.js
-// import React, { useContext, useState, useEffect } from 'react';
-// import axios from 'axios';
-// import './FriendMoments.css';
-// import Header from '../../components/Header';
-// import { Edit, GoodTwo, Comment } from '@icon-park/react';
-// import { UserContext } from '../../context/UserContext'; // 引入用户上下文
-
-// // 修改后的封面图 URL 拼接函数
-// const formatImageUrl = (url) => {
-// if (!url) return '';
-// const filename = url.split('/').pop(); // 提取文件名部分
-// return `http://localhost:5011/uploads/dynamic/${filename}`;
-// };
-
-// const FriendMoments = () => {
-// const [feeds, setFeeds] = useState([]);
-// const [filteredFeeds, setFilteredFeeds] = useState([]);
-// const [query, setQuery] = useState('');
-// const [loading, setLoading] = useState(true);
-// const [error, setError] = useState(null);
-
-// // 从上下文中获取用户信息
-// const { user } = useContext(UserContext);
-// const userId = user?.userId || null; // 从用户上下文中获取userId
-// const username = user?.username || '未知用户'; // 获取用户名
-
-// // Modal state & form fields
-// const [showModal, setShowModal] = useState(false);
-// const [title, setTitle] = useState('');
-// const [content, setContent] = useState('');
-// const [selectedImages, setSelectedImages] = useState([]);
-// const [previewUrls, setPreviewUrls] = useState([]);
-
-// // 检查用户是否已登录
-// const isLoggedIn = !!userId;
-
-// // 拉取好友动态列表
-// const fetchFeeds = async () => {
-// if (!isLoggedIn) {
-// setLoading(false);
-// setError('请先登录');
-// return;
-// }
-
-// setLoading(true);
-// setError(null);
-// try {
-// // 注意这里修改了API路径,使用getAllDynamics接口
-// const res = await axios.get(`/echo/dynamic/${userId}/getAllDynamics`);
-
-// // 检查API返回的数据结构
-// console.log('API响应数据:', res.data);
-
-// // 从响应中提取dynamic数组
-// const dynamicList = res.data.dynamic || [];
-
-// // 将API返回的数据结构转换为前端期望的格式
-// const formattedFeeds = dynamicList.map(item => ({
-// postNo: item.dynamic_id, // 使用API返回的dynamic_id作为帖子ID
-// title: item.title,
-// postContent: item.content,
-// imageUrl: item.images, // 使用API返回的images字段
-// postTime: item.time, // 使用API返回的time字段
-// postLikeNum: item.likes?.length || 0, // 点赞数
-// liked: item.likes?.some(like => like.user_id === userId), // 当前用户是否已点赞
-// user_id: item.user_id, // 发布者ID
-// username: item.username, // 发布者昵称
-// avatar_url: item.avatar_url, // 发布者头像
-// comments: item.comments || [] // 评论列表
-// }));
-
-// setFeeds(formattedFeeds);
-// setFilteredFeeds(formattedFeeds);
-// } catch (err) {
-// console.error('获取动态列表失败:', err);
-// setError('获取动态列表失败,请稍后重试');
-// } finally {
-// setLoading(false);
-// }
-// };
-
-// useEffect(() => {
-// fetchFeeds();
-// }, [userId]);
-
-// // 搜索处理
-// const handleSearch = () => {
-// const q = query.trim().toLowerCase();
-// if (!q) {
-// setFilteredFeeds(feeds);
-// return;
-// }
-// setFilteredFeeds(
-// feeds.filter(f =>
-// (f.title || '').toLowerCase().includes(q) ||
-// (f.postContent || '').toLowerCase().includes(q)
-// )
-// );
-// };
-
-// const handleReset = () => {
-// setQuery('');
-// setFilteredFeeds(feeds);
-// };
-
-// // 对话框内:处理图片选择
-// const handleImageChange = (e) => {
-// const files = Array.from(e.target.files);
-// if (!files.length) return;
-
-// const previewUrls = files.map(file => URL.createObjectURL(file));
-
-// setSelectedImages(files);
-// setPreviewUrls(previewUrls);
-// };
-
-// // 对话框内:提交新动态
-// const handleSubmit = async () => {
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// if (!content.trim()) {
-// alert('内容不能为空');
-// return;
-// }
-
-// try {
-// // 使用formData格式提交
-// const formData = new FormData();
-// formData.append('title', title.trim() || '');
-// formData.append('content', content.trim());
-
-// // 添加图片文件
-// selectedImages.forEach((file, index) => {
-// formData.append('image_url', file);
-// });
-
-// // 调用创建动态API
-// await axios.post(`/echo/dynamic/${userId}/createDynamic`, formData, {
-// headers: {
-// 'Content-Type': 'multipart/form-data'
-// }
-// });
-
-// // 重置表单
-// setTitle('');
-// setContent('');
-// setSelectedImages([]);
-// setPreviewUrls([]);
-// setShowModal(false);
-// fetchFeeds();
-// alert('发布成功');
-// } catch (err) {
-// console.error('发布失败', err);
-// alert('发布失败,请稍后重试');
-// }
-// };
-
-// // 删除动态 - 注意:API文档中未提供删除接口,这里保留原代码
-// const handleDelete = async (dynamicId) => {
-
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// if (!window.confirm('确定要删除这条动态吗?')) return;
-// try {
-// // 注意:API文档中未提供删除接口,这里使用原代码中的路径
-// await axios.delete(`/echo/dynamic/me/deleteDynamic/${dynamicId}`);
-// fetchFeeds();
-// alert('删除成功');
-// } catch (err) {
-// console.error('删除失败', err);
-// alert('删除失败,请稍后重试');
-// }
-// };
-
-// // 点赞动态
-// const handleLike = async (dynamicId, islike) => {
-// if (islike) {
-// handleUnlike(dynamicId);
-// return
-// }
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// // 验证dynamicId是否有效
-// if (!dynamicId) {
-// console.error('无效的dynamicId:', dynamicId);
-// alert('点赞失败:动态ID无效');
-// return;
-// }
-
-// console.log('当前用户ID:', userId);
-// console.log('即将点赞的动态ID:', dynamicId);
-
-// try {
-// // 确保参数是整数类型
-// const requestData = {
-// userId: parseInt(userId),
-// dynamicId: parseInt(dynamicId)
-// };
-
-// // 验证参数是否为有效数字
-// if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
-// console.error('无效的参数:', requestData);
-// alert('点赞失败:参数格式错误');
-// return;
-// }
-
-// console.log('点赞请求数据:', requestData);
-
-// const res = await axios.post(`/echo/dynamic/like`, requestData, {
-// headers: {
-// 'Content-Type': 'application/json' // 明确指定JSON格式
-// }
-// });
-
-// console.log('点赞API响应:', res.data);
-
-// if (res.status === 200) {
-// // 更新本地状态
-// setFeeds(prevFeeds => {
-// return prevFeeds.map(feed => {
-// if (feed.postNo === dynamicId) {
-// return {
-// ...feed,
-// postLikeNum: (feed.postLikeNum || 0) + 1,
-// liked: true
-// };
-// }
-// return feed;
-// });
-// });
-// } else {
-// alert(res.data.message || '点赞失败');
-// }
-// } catch (err) {
-// console.error('点赞失败', err);
-
-// // 检查错误响应,获取更详细的错误信息
-// if (err.response) {
-// console.error('错误响应数据:', err.response.data);
-// console.error('错误响应状态:', err.response.status);
-// console.error('错误响应头:', err.response.headers);
-// }
-
-// alert('点赞失败,请稍后重试');
-// }
-// };
-
-// // 取消点赞
-// const handleUnlike = async (dynamicId) => {
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// // 验证dynamicId是否有效
-// if (!dynamicId) {
-// console.error('无效的dynamicId:', dynamicId);
-// alert('取消点赞失败:动态ID无效');
-// return;
-// }
-
-// // 检查是否已经取消点赞,防止重复请求
-// const currentFeed = feeds.find(feed => feed.postNo === dynamicId);
-// if (currentFeed && !currentFeed.liked) {
-// console.warn('尝试重复取消点赞,已忽略');
-// return;
-// }
-
-// try {
-// // 确保参数是整数类型
-// const requestData = {
-// userId: parseInt(userId),
-// dynamicId: parseInt(dynamicId)
-// };
-
-// // 验证参数是否为有效数字
-// if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
-// console.error('无效的参数:', requestData);
-// alert('取消点赞失败:参数格式错误');
-// return;
-// }
-
-// console.log('取消点赞请求数据:', requestData);
-
-// const res = await axios.delete(`/echo/dynamic/unlike`, {
-// headers: {
-// 'Content-Type': 'application/json' // 明确指定JSON格式
-// },
-// data: requestData // 将参数放在data属性中
-// });
-
-// console.log('取消点赞API响应:', res.data);
-
-// if (res.status === 200) {
-// // 更新本地状态
-// setFeeds(prevFeeds => {
-// return prevFeeds.map(feed => {
-// if (feed.postNo === dynamicId) {
-// return {
-// ...feed,
-// postLikeNum: Math.max(0, (feed.postLikeNum || 0) - 1),
-// liked: false
-// };
-// }
-// return feed;
-// });
-// });
-// } else {
-// alert(res.data.message || '取消点赞失败');
-// }
-// } catch (err) {
-// console.error('取消点赞失败', err);
-
-// // 检查错误响应,获取更详细的错误信息
-// if (err.response) {
-// console.error('错误响应数据:', err.response.data);
-// console.error('错误响应状态:', err.response.status);
-// console.error('错误响应头:', err.response.headers);
-// }
-
-// alert('取消点赞失败,请稍后重试');
-// }
-// };
-
-// // 评论好友动态
-// const handleComment = async (dynamicId, parentCommentId = null, replyToUsername = '') => {
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// const commentInputId = `comment-input-${dynamicId}-${parentCommentId}`;
-// const commentInput = document.getElementById(commentInputId);
-
-// if (!commentInput || !commentInput.value.trim()) {
-// alert('评论内容不能为空');
-// return;
-// }
-
-// const commentContent = commentInput.value.trim();
-
-// try {
-// // 准备请求数据
-// const requestData = {
-// content: commentContent
-// };
-
-// // 如果是回复,添加parent_comment_id
-// if (parentCommentId) {
-// requestData.parent_comment_id = parentCommentId;
-// }
-
-// const res = await axios.post(`/echo/dynamic/${userId}/feeds/${dynamicId}/comments`, requestData);
-
-// if (res.status === 200 || res.status === 201) {
-// // 创建新评论对象
-// const newComment = {
-// // 使用API返回的评论ID,如果没有则生成临时ID
-// id: res.data.comment_id || `temp-${Date.now()}`,
-// user_id: userId,
-// username: username,
-// content: commentContent,
-// time: new Date().toISOString(),
-// // 如果是回复,添加reply_to_username
-// ...(replyToUsername && { reply_to_username: replyToUsername })
-// };
-
-// // 更新本地状态
-// setFeeds(prevFeeds => {
-// return prevFeeds.map(feed => {
-// if (feed.postNo === dynamicId) {
-// if (parentCommentId) {
-// // 这是一个回复,找到父评论并添加到其replies数组
-// return {
-// ...feed,
-// comments: feed.comments.map(comment => {
-// if (comment.id === parentCommentId || comment.postNo === parentCommentId) {
-// // 确保replies数组存在
-// if (!comment.replies) {
-// return {
-// ...comment,
-// replies: [newComment]
-// };
-// }
-// return {
-// ...comment,
-// replies: [...comment.replies, newComment]
-// };
-// }
-// return comment;
-// })
-// };
-// } else {
-// // 这是一个新评论,添加到评论列表
-// return {
-// ...feed,
-// comments: [...feed.comments, newComment]
-// };
-// }
-// }
-// return feed;
-// });
-// });
-
-// // 清空输入框并隐藏回复框
-// if (commentInput) {
-// commentInput.value = '';
-// }
-// toggleReplyBox(dynamicId, parentCommentId);
-// } else {
-// alert(res.data.error || '评论失败');
-// }
-// } catch (err) {
-// console.error('评论失败', err);
-// alert('评论失败,请稍后重试');
-// }
-// };
-
-// // 切换回复框显示状态
-// const toggleReplyBox = (dynamicId, parentCommentId = null) => {
-// const replyBoxId = `reply-box-${dynamicId}-${parentCommentId}`;
-// const replyBox = document.getElementById(replyBoxId);
-
-// if (replyBox) {
-// replyBox.style.display = replyBox.style.display === 'none' ? 'block' : 'none';
-
-// // 如果显示,聚焦到输入框
-// if (replyBox.style.display === 'block') {
-// const commentInput = document.getElementById(`comment-input-${dynamicId}-${parentCommentId}`);
-// if (commentInput) commentInput.focus();
-// }
-// }
-// };
-
-// return (
-// <div className="friend-moments-container">
-// <Header />
-// <div className="fm-header">
-// <button className="create-btn" onClick={() => setShowModal(true)}>
-// <Edit theme="outline" size="18" style={{ marginRight: '6px' }} />
-// 创建动态
-// </button>
-// </div>
-
-// <div className="feed-list">
-// {loading ? (
-// <div className="loading-message">加载中...</div>
-// ) : error ? (
-// <div className="error-message">{error}</div>
-// ) : !isLoggedIn ? (
-// <div className="login-prompt">
-// <p>请先登录查看好友动态</p>
-// </div>
-// ) : filteredFeeds.length === 0 ? (
-// <div className="empty-message">暂无动态</div>
-// ) : (
-// filteredFeeds.map(feed => (
-// <div className="feed-item" key={feed.postNo || `feed-${Math.random()}`}>
-// {/* 显示发布者信息 */}
-// <div className="feed-author">
-// <img
-// className="user-avatar"
-// src={feed.avatar_url || 'https://example.com/default-avatar.jpg'}
-// alt={feed.username || '用户头像'}
-// />
-// <div>
-// <h4>{feed.username || '未知用户'}</h4>
-// <span className="feed-date">{new Date(feed.postTime || Date.now()).toLocaleString()}</span>
-// </div>
-// </div>
-
-// {feed.title && <h4 className="feed-title">{feed.title}</h4>}
-// <p className="feed-content">{feed.postContent || '无内容'}</p>
-
-// {feed.imageUrl && (
-// <div className="feed-images">
-// {typeof feed.imageUrl === 'string' ? (
-// <img src={formatImageUrl(feed.imageUrl)} alt="动态图片" />
-// ) : (
-// feed.imageUrl.map((url, i) => (
-// <img key={i} src={formatImageUrl(url)} alt={`动态图${i}`} />
-// ))
-// )}
-// </div>
-// )}
-
-// <div className="feed-footer">
-// <div className="like-container">
-// <button className="icon-btn" onClick={() => handleLike(feed.postNo, feed.liked, feed.user_id)}>
-// <GoodTwo theme="outline" size="24" fill={feed.liked ? '#f00' : '#fff'} />
-// <span>{feed.postLikeNum || 0}</span>
-// </button>
-
-// <button
-// className="icon-btn"
-// onClick={() => toggleReplyBox(feed.postNo)}
-// >
-// <Comment theme="outline" size="24" fill="#333" />
-// <span>评论</span>
-// </button>
-// </div>
-
-// {feed.user_id === userId && (
-// <button className="delete-btn" onClick={() => handleDelete(feed.postNo)}>
-// 删除
-// </button>
-// )}
-// </div>
-
-// {/* 动态的评论输入框 */}
-// <div id={`reply-box-${feed.postNo}-null`} className="comment-box" style={{display: 'none'}}>
-// <textarea
-// id={`comment-input-${feed.postNo}-null`}
-// className="comment-input"
-// placeholder="请输入评论内容..."
-// />
-// <button
-// className="submit-comment-btn"
-// onClick={() => handleComment(feed.postNo)}
-// >
-// 发布评论
-// </button>
-// </div>
-
-// {/* 评论列表 */}
-// {Array.isArray(feed.comments) && feed.comments.length > 0 && (
-// <div className="comments-container">
-// <h5>评论 ({feed.comments.length})</h5>
-// <div className="comments-list">
-// {feed.comments.map((comment, index) => (
-// <div className="comment-item" key={comment.id || index}>
-// <div className="comment-header">
-// <span className="comment-user">{comment.username || '用户'}</span>
-// <span className="comment-time">
-// {new Date(comment.time || Date.now()).toLocaleString()}
-// </span>
-// </div>
-// <p className="comment-content">
-// {/* 显示回复格式 */}
-// {comment.reply_to_username ?
-// <span className="reply-prefix">{comment.username} 回复 {comment.reply_to_username}:</span> :
-// <span>{comment.username}:</span>
-// }
-// {comment.content}
-// </p>
-// <button
-// className="reply-btn"
-// onClick={() => toggleReplyBox(feed.postNo, comment.id || index, comment.username)}
-// >
-// 回复
-// </button>
-
-// {/* 该评论的回复框 */}
-// <div id={`reply-box-${feed.postNo}-${comment.id || index}`} className="comment-box nested-reply-box" style={{display: 'none'}}>
-// <textarea
-// id={`comment-input-${feed.postNo}-${comment.id || index}`}
-// className="comment-input"
-// placeholder={`回复 ${comment.username}...`}
-// />
-// <button
-// className="submit-comment-btn"
-// onClick={() => handleComment(feed.postNo, comment.id || index, comment.username)}
-// >
-// 发布回复
-// </button>
-// </div>
-
-// {/* 嵌套回复 */}
-// {Array.isArray(comment.replies) && comment.replies.length > 0 && (
-// <div className="nested-replies">
-// {comment.replies.map((reply, replyIndex) => (
-// <div className="reply-item" key={reply.id || `${comment.id || index}-${replyIndex}`}>
-// <p className="reply-content">
-// {reply.reply_to_username ?
-// <span className="reply-prefix">{reply.username} 回复 {reply.reply_to_username}:</span> :
-// <span>{reply.username}:</span>
-// }
-// {reply.content}
-// </p>
-// <button
-// className="reply-btn"
-// onClick={() => toggleReplyBox(feed.postNo, reply.id || `${comment.id || index}-${replyIndex}`, reply.username)}
-// >
-// 回复
-// </button>
-
-// {/* 该回复的回复框 */}
-// <div id={`reply-box-${feed.postNo}-${reply.id || `${comment.id || index}-${replyIndex}`}`} className="comment-box nested-reply-box" style={{display: 'none'}}>
-// <textarea
-// id={`comment-input-${feed.postNo}-${reply.id || `${comment.id || index}-${replyIndex}`}`}
-// className="comment-input"
-// placeholder={`回复 ${reply.username}...`}
-// />
-// <button
-// className="submit-comment-btn"
-// onClick={() => handleComment(feed.postNo, reply.id || `${comment.id || index}-${replyIndex}`, reply.username)}
-// >
-// 发布回复
-// </button>
-// </div>
-// </div>
-// ))}
-// </div>
-// )}
-// </div>
-// ))}
-// </div>
-// </div>
-// )}
-// </div>
-// ))
-// )}
-// </div>
-
-// {/* Modal 对话框 */}
-// {showModal && (
-// <div className="modal-overlay" onClick={() => setShowModal(false)}>
-// <div className="modal-dialog" onClick={e => e.stopPropagation()}>
-// <h3>发布新动态</h3>
-// <input
-// type="text"
-// placeholder="标题"
-// value={title}
-// onChange={e => setTitle(e.target.value)}
-// />
-// <textarea
-// placeholder="写下你的内容..."
-// value={content}
-// onChange={e => setContent(e.target.value)}
-// />
-// <label className="file-label">
-// 选择图片
-// <input
-// type="file"
-// accept="image/*"
-// multiple
-// onChange={handleImageChange}
-// style={{ display: 'none' }}
-// />
-// </label>
-// <div className="cf-preview">
-// {previewUrls.map((url, i) => (
-// <img key={i} src={url} alt={`预览${i}`} />
-// ))}
-// </div>
-// <div className="modal-actions">
-// <button className="btn cancel" onClick={() => setShowModal(false)}>
-// 取消
-// </button>
-// <button className="btn submit" onClick={handleSubmit}>
-// 发布
-// </button>
-// </div>
-// </div>
-// </div>
-// )}
-// </div>
-// );
-// };
-
-// export default FriendMoments;
-// // FriendMoments.js
-// import React, { useContext, useState, useEffect } from 'react';
-// import axios from 'axios';
-// import './FriendMoments.css';
-// import Header from '../../components/Header';
-// import { Edit, GoodTwo, Comment } from '@icon-park/react';
-// import { UserContext } from '../../context/UserContext'; // 引入用户上下文
-
-// // 修改后的封面图 URL 拼接函数
-// const formatImageUrl = (url) => {
-// if (!url) return '';
-// const filename = url.split('/').pop(); // 提取文件名部分
-// return `http://localhost:5011/uploads/dynamic/${filename}`;
-// };
-
-// const FriendMoments = () => {
-// const [feeds, setFeeds] = useState([]);
-// const [filteredFeeds, setFilteredFeeds] = useState([]);
-// const [query, setQuery] = useState('');
-// const [loading, setLoading] = useState(true);
-// const [error, setError] = useState(null);
-
-// // 从上下文中获取用户信息
-// const { user } = useContext(UserContext);
-// const userId = user?.userId || null; // 从用户上下文中获取userId
-// const username = user?.username || '未知用户'; // 获取用户名
-
-// // Modal state & form fields
-// const [showModal, setShowModal] = useState(false);
-// const [title, setTitle] = useState('');
-// const [content, setContent] = useState('');
-// const [selectedImages, setSelectedImages] = useState([]);
-// const [previewUrls, setPreviewUrls] = useState([]);
-
-// // 检查用户是否已登录
-// const isLoggedIn = !!userId;
-
-// // 拉取好友动态列表
-// const fetchFeeds = async () => {
-// if (!isLoggedIn) {
-// setLoading(false);
-// setError('请先登录');
-// return;
-// }
-
-// setLoading(true);
-// setError(null);
-// try {
-// // 注意这里修改了API路径,使用getAllDynamics接口
-// const res = await axios.get(`/echo/dynamic/${userId}/getAllDynamics`);
-
-// // 检查API返回的数据结构
-// console.log('API响应数据:', res.data);
-
-// // 从响应中提取dynamic数组
-// const dynamicList = res.data.dynamic || [];
-
-// // 将API返回的数据结构转换为前端期望的格式
-// const formattedFeeds = dynamicList.map(item => ({
-// postNo: item.dynamic_id, // 使用API返回的dynamic_id作为帖子ID
-// title: item.title,
-// postContent: item.content,
-// imageUrl: item.images, // 使用API返回的images字段
-// postTime: item.time, // 使用API返回的time字段
-// postLikeNum: item.likes?.length || 0, // 点赞数
-// liked: item.likes?.some(like => like.user_id === userId), // 当前用户是否已点赞
-// user_id: item.user_id, // 发布者ID
-// username: item.username, // 发布者昵称
-// avatar_url: item.avatar_url, // 发布者头像
-// comments: item.comments || [] // 评论列表
-// }));
-
-// setFeeds(formattedFeeds);
-// setFilteredFeeds(formattedFeeds);
-// } catch (err) {
-// console.error('获取动态列表失败:', err);
-// setError('获取动态列表失败,请稍后重试');
-// } finally {
-// setLoading(false);
-// }
-// };
-
-// useEffect(() => {
-// fetchFeeds();
-// }, [userId]);
-
-// // 搜索处理
-// const handleSearch = () => {
-// const q = query.trim().toLowerCase();
-// if (!q) {
-// setFilteredFeeds(feeds);
-// return;
-// }
-// setFilteredFeeds(
-// feeds.filter(f =>
-// (f.title || '').toLowerCase().includes(q) ||
-// (f.postContent || '').toLowerCase().includes(q)
-// )
-// );
-// };
-
-// const handleReset = () => {
-// setQuery('');
-// setFilteredFeeds(feeds);
-// };
-
-// // 对话框内:处理图片选择
-// const handleImageChange = (e) => {
-// const files = Array.from(e.target.files);
-// if (!files.length) return;
-
-// const previewUrls = files.map(file => URL.createObjectURL(file));
-
-// setSelectedImages(files);
-// setPreviewUrls(previewUrls);
-// };
-
-// // 对话框内:提交新动态
-// const handleSubmit = async () => {
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// if (!content.trim()) {
-// alert('内容不能为空');
-// return;
-// }
-
-// try {
-// // 使用formData格式提交
-// const formData = new FormData();
-// formData.append('title', title.trim() || '');
-// formData.append('content', content.trim());
-
-// // 添加图片文件
-// selectedImages.forEach((file, index) => {
-// formData.append('image_url', file);
-// });
-
-// // 调用创建动态API
-// await axios.post(`/echo/dynamic/${userId}/createDynamic`, formData, {
-// headers: {
-// 'Content-Type': 'multipart/form-data'
-// }
-// });
-
-// // 重置表单
-// setTitle('');
-// setContent('');
-// setSelectedImages([]);
-// setPreviewUrls([]);
-// setShowModal(false);
-// fetchFeeds();
-// alert('发布成功');
-// } catch (err) {
-// console.error('发布失败', err);
-// alert('发布失败,请稍后重试');
-// }
-// };
-
-// // 删除动态 - 注意:API文档中未提供删除接口,这里保留原代码
-// const handleDelete = async (dynamicId) => {
-
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// if (!window.confirm('确定要删除这条动态吗?')) return;
-// try {
-// // 注意:API文档中未提供删除接口,这里使用原代码中的路径
-// await axios.delete(`/echo/dynamic/me/deleteDynamic/${dynamicId}`);
-// fetchFeeds();
-// alert('删除成功');
-// } catch (err) {
-// console.error('删除失败', err);
-// alert('删除失败,请稍后重试');
-// }
-// };
-
-// // 点赞动态
-// const handleLike = async (dynamicId, islike) => {
-// if (islike) {
-// handleUnlike(dynamicId);
-// return
-// }
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// // 验证dynamicId是否有效
-// if (!dynamicId) {
-// console.error('无效的dynamicId:', dynamicId);
-// alert('点赞失败:动态ID无效');
-// return;
-// }
-
-// console.log('当前用户ID:', userId);
-// console.log('即将点赞的动态ID:', dynamicId);
-
-// try {
-// // 确保参数是整数类型
-// const requestData = {
-// userId: parseInt(userId),
-// dynamicId: parseInt(dynamicId)
-// };
-
-// // 验证参数是否为有效数字
-// if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
-// console.error('无效的参数:', requestData);
-// alert('点赞失败:参数格式错误');
-// return;
-// }
-
-// console.log('点赞请求数据:', requestData);
-
-// const res = await axios.post(`/echo/dynamic/like`, requestData, {
-// headers: {
-// 'Content-Type': 'application/json' // 明确指定JSON格式
-// }
-// });
-
-// console.log('点赞API响应:', res.data);
-
-// if (res.status === 200) {
-// // 更新本地状态
-// feeds.forEach(feed => {
-// if (feed.postNo === dynamicId) {
-// feed.postLikeNum = (feed.postLikeNum || 0) + 1;
-// feed.liked = true;
-// }
-// });
-// setFeeds([...feeds]); // 更新状态以触发重新渲染
-// } else {
-// alert(res.data.message || '点赞失败');
-// }
-// } catch (err) {
-// console.error('点赞失败', err);
-
-// // 检查错误响应,获取更详细的错误信息
-// if (err.response) {
-// console.error('错误响应数据:', err.response.data);
-// console.error('错误响应状态:', err.response.status);
-// console.error('错误响应头:', err.response.headers);
-// }
-
-// alert('点赞失败,请稍后重试');
-// }
-// };
-
-// // 取消点赞
-// const handleUnlike = async (dynamicId) => {
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// // 验证dynamicId是否有效
-// if (!dynamicId) {
-// console.error('无效的dynamicId:', dynamicId);
-// alert('取消点赞失败:动态ID无效');
-// return;
-// }
-
-// // 检查是否已经取消点赞,防止重复请求
-// const currentFeed = feeds.find(feed => feed.postNo === dynamicId);
-// if (currentFeed && !currentFeed.liked) {
-// console.warn('尝试重复取消点赞,已忽略');
-// return;
-// }
-
-// try {
-// // 确保参数是整数类型
-// const requestData = {
-// userId: parseInt(userId),
-// dynamicId: parseInt(dynamicId)
-// };
-
-// // 验证参数是否为有效数字
-// if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
-// console.error('无效的参数:', requestData);
-// alert('取消点赞失败:参数格式错误');
-// return;
-// }
-
-// console.log('取消点赞请求数据:', requestData);
-
-// const res = await axios.delete(`/echo/dynamic/unlike`, {
-// headers: {
-// 'Content-Type': 'application/json' // 明确指定JSON格式
-// },
-// data: requestData // 将参数放在data属性中
-// });
-
-// console.log('取消点赞API响应:', res.data);
-
-// if (res.status === 200) {
-// // 更新本地状态
-// feeds.forEach(feed => {
-// if (feed.postNo === dynamicId) {
-// feed.postLikeNum = Math.max(0, (feed.postLikeNum || 0) - 1);
-// feed.liked = false;
-// }
-// });
-// setFeeds([...feeds]); // 更新状态以触发重新渲染
-// } else {
-// alert(res.data.message || '取消点赞失败');
-// }
-// } catch (err) {
-// console.error('取消点赞失败', err);
-
-// // 检查错误响应,获取更详细的错误信息
-// if (err.response) {
-// console.error('错误响应数据:', err.response.data);
-// console.error('错误响应状态:', err.response.status);
-// console.error('错误响应头:', err.response.headers);
-// }
-
-// alert('取消点赞失败,请稍后重试');
-// }
-// };
-
-// // 评论好友动态
-// const handleComment = async (dynamicId, parentCommentId = null, replyToUsername = '') => {
-// if (!isLoggedIn) {
-// alert('请先登录');
-// return;
-// }
-
-// const commentInputRef = document.getElementById(`comment-input-${dynamicId}-${parentCommentId}`);
-// if (!commentInputRef || !commentInputRef.value.trim()) {
-// alert('评论内容不能为空');
-// return;
-// }
-
-// const commentContent = commentInputRef.value.trim();
-
-// try {
-// // 准备请求数据
-// const requestData = {
-// content: commentContent
-// };
-
-// // 如果是回复,添加parent_comment_id
-// if (parentCommentId) {
-// requestData.parent_comment_id = parentCommentId;
-// }
-
-// const res = await axios.post(`/echo/dynamic/${userId}/feeds/${dynamicId}/comments`, requestData);
-
-// if (res.status === 200 || res.status === 201) {
-// // 成功获取评论数据
-// const newComment = {
-// user_id: userId,
-// username: username,
-// content: commentContent,
-// time: new Date().toISOString(), // 使用当前时间作为评论时间
-// // 如果是回复,添加parent_comment_id和reply_to_username
-// ...(parentCommentId && { parent_comment_id: parentCommentId }),
-// ...(replyToUsername && { reply_to_username: replyToUsername })
-// };
-
-// // 更新本地状态,添加新评论
-// setFeeds(prevFeeds => {
-// return prevFeeds.map(feed => {
-// if (feed.postNo === dynamicId) {
-// // 确保comments是数组,并且正确合并新评论
-// const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
-
-// if (parentCommentId) {
-// // 查找父评论并添加回复
-// return {
-// ...feed,
-// comments: currentComments.map(comment => {
-// if (comment.id === parentCommentId || comment.postNo === parentCommentId) {
-// // 如果父评论已有replies数组,添加到其中
-// if (Array.isArray(comment.replies)) {
-// return {
-// ...comment,
-// replies: [...comment.replies, newComment]
-// };
-// } else {
-// // 否则创建新的replies数组
-// return {
-// ...comment,
-// replies: [newComment]
-// };
-// }
-// }
-// return comment;
-// })
-// };
-// } else {
-// // 普通评论,添加到评论列表
-// return {
-// ...feed,
-// comments: [...currentComments, newComment]
-// };
-// }
-// }
-// return feed;
-// });
-// });
-
-// // 清空输入框
-// if (commentInputRef) {
-// commentInputRef.value = '';
-// }
-
-// // 隐藏回复框
-// toggleReplyBox(dynamicId, parentCommentId);
-// } else {
-// alert(res.data.error || '评论失败');
-// }
-// } catch (err) {
-// console.error('评论失败', err);
-// alert('评论失败,请稍后重试');
-// }
-// };
-
-// // 切换回复框显示状态
-// const toggleReplyBox = (dynamicId, parentCommentId = null) => {
-// const replyBoxId = `reply-box-${dynamicId}-${parentCommentId}`;
-// const replyBox = document.getElementById(replyBoxId);
-
-// if (replyBox) {
-// replyBox.style.display = replyBox.style.display === 'none' ? 'block' : 'none';
-
-// // 如果显示,聚焦到输入框
-// if (replyBox.style.display === 'block') {
-// const commentInput = document.getElementById(`comment-input-${dynamicId}-${parentCommentId}`);
-// if (commentInput) commentInput.focus();
-// }
-// }
-// };
-
-// return (
-// <div className="friend-moments-container">
-// <Header />
-// <div className="fm-header">
-// <button className="create-btn" onClick={() => setShowModal(true)}>
-// <Edit theme="outline" size="18" style={{ marginRight: '6px' }} />
-// 创建动态
-// </button>
-// </div>
-
-// <div className="feed-list">
-// {loading ? (
-// <div className="loading-message">加载中...</div>
-// ) : error ? (
-// <div className="error-message">{error}</div>
-// ) : !isLoggedIn ? (
-// <div className="login-prompt">
-// <p>请先登录查看好友动态</p>
-// </div>
-// ) : filteredFeeds.length === 0 ? (
-// <div className="empty-message">暂无动态</div>
-// ) : (
-// filteredFeeds.map(feed => (
-// <div className="feed-item" key={feed.postNo || `feed-${Math.random()}`}>
-// {/* 显示发布者信息 */}
-// <div className="feed-author">
-// <img
-// className="user-avatar"
-// src={feed.avatar_url || 'https://example.com/default-avatar.jpg'}
-// alt={feed.username || '用户头像'}
-// />
-// <div>
-// <h4>{feed.username || '未知用户'}</h4>
-// <span className="feed-date">{new Date(feed.postTime || Date.now()).toLocaleString()}</span>
-// </div>
-// </div>
-
-// {feed.title && <h4 className="feed-title">{feed.title}</h4>}
-// <p className="feed-content">{feed.postContent || '无内容'}</p>
-
-// {feed.imageUrl && (
-// <div className="feed-images">
-// {typeof feed.imageUrl === 'string' ? (
-// <img src={formatImageUrl(feed.imageUrl)} alt="动态图片" />
-// ) : (
-// feed.imageUrl.map((url, i) => (
-// <img key={i} src={formatImageUrl(url)} alt={`动态图${i}`} />
-// ))
-// )}
-// </div>
-// )}
-
-// <div className="feed-footer">
-// <div className="like-container">
-// <button className="icon-btn" onClick={() => handleLike(feed.postNo, feed.liked, feed.user_id)}>
-// <GoodTwo theme="outline" size="24" fill={feed.liked ? '#f00' : '#fff'} />
-// <span>{feed.postLikeNum || 0}</span>
-// </button>
-
-// <button
-// className="icon-btn"
-// onClick={() => toggleReplyBox(feed.postNo)}
-// >
-// <Comment theme="outline" size="24" fill="#333" />
-// <span>评论</span>
-// </button>
-// </div>
-
-// {feed.user_id === userId && (
-// <button className="delete-btn" onClick={() => handleDelete(feed.postNo)}>
-// 删除
-// </button>
-// )}
-// </div>
-
-// {/* 动态的评论输入框 */}
-// <div id={`reply-box-${feed.postNo}-null`} className="comment-box" style={{display: 'none'}}>
-// <textarea
-// id={`comment-input-${feed.postNo}-null`}
-// className="comment-input"
-// placeholder="请输入评论内容..."
-// />
-// <button
-// className="submit-comment-btn"
-// onClick={() => handleComment(feed.postNo)}
-// >
-// 发布评论
-// </button>
-// </div>
-
-// {/* 评论列表 */}
-// {Array.isArray(feed.comments) && feed.comments.length > 0 && (
-// <div className="comments-container">
-// <h5>评论 ({feed.comments.length})</h5>
-// <div className="comments-list">
-// {feed.comments.map((comment, index) => (
-// <div className="comment-item" key={index}>
-// <div className="comment-header">
-// <span className="comment-user">{comment.username || '用户'}</span>
-// <span className="comment-time">
-// {new Date(comment.time || Date.now()).toLocaleString()}
-// </span>
-// </div>
-// <p className="comment-content">
-// {/* 显示回复格式 */}
-// {comment.reply_to_username ?
-// <span className="reply-prefix">{comment.username} 回复 {comment.reply_to_username}:</span> :
-// <span>{comment.username}:</span>
-// }
-// {comment.content}
-// </p>
-// <button
-// className="reply-btn"
-// onClick={() => toggleReplyBox(feed.postNo, comment.id || index)}
-// >
-// 回复
-// </button>
-
-// {/* 该评论的回复框 */}
-// <div id={`reply-box-${feed.postNo}-${comment.id || index}`} className="comment-box nested-reply-box" style={{display: 'none'}}>
-// <textarea
-// id={`comment-input-${feed.postNo}-${comment.id || index}`}
-// className="comment-input"
-// placeholder={`回复 ${comment.username}...`}
-// />
-// <button
-// className="submit-comment-btn"
-// onClick={() => handleComment(feed.postNo, comment.id || index, comment.username)}
-// >
-// 发布回复
-// </button>
-// </div>
-
-// {/* 嵌套回复 */}
-// {Array.isArray(comment.replies) && comment.replies.length > 0 && (
-// <div className="nested-replies">
-// {comment.replies.map((reply, replyIndex) => (
-// <div className="reply-item" key={replyIndex}>
-// <p className="reply-content">
-// {reply.reply_to_username ?
-// <span className="reply-prefix">{reply.username} 回复 {reply.reply_to_username}:</span> :
-// <span>{reply.username}:</span>
-// }
-// {reply.content}
-// </p>
-// <button
-// className="reply-btn"
-// onClick={() => toggleReplyBox(feed.postNo, reply.id || `${index}-${replyIndex}`, reply.username)}
-// >
-// 回复
-// </button>
-
-// {/* 该回复的回复框 */}
-// <div id={`reply-box-${feed.postNo}-${reply.id || `${index}-${replyIndex}`}`} className="comment-box nested-reply-box" style={{display: 'none'}}>
-// <textarea
-// id={`comment-input-${feed.postNo}-${reply.id || `${index}-${replyIndex}`}`}
-// className="comment-input"
-// placeholder={`回复 ${reply.username}...`}
-// />
-// <button
-// className="submit-comment-btn"
-// onClick={() => handleComment(feed.postNo, reply.id || `${index}-${replyIndex}`, reply.username)}
-// >
-// 发布回复
-// </button>
-// </div>
-// </div>
-// ))}
-// </div>
-// )}
-// </div>
-// ))}
-// </div>
-// </div>
-// )}
-// </div>
-// ))
-// )}
-// </div>
-
-// {/* Modal 对话框 */}
-// {showModal && (
-// <div className="modal-overlay" onClick={() => setShowModal(false)}>
-// <div className="modal-dialog" onClick={e => e.stopPropagation()}>
-// <h3>发布新动态</h3>
-// <input
-// type="text"
-// placeholder="标题"
-// value={title}
-// onChange={e => setTitle(e.target.value)}
-// />
-// <textarea
-// placeholder="写下你的内容..."
-// value={content}
-// onChange={e => setContent(e.target.value)}
-// />
-// <label className="file-label">
-// 选择图片
-// <input
-// type="file"
-// accept="image/*"
-// multiple
-// onChange={handleImageChange}
-// style={{ display: 'none' }}
-// />
-// </label>
-// <div className="cf-preview">
-// {previewUrls.map((url, i) => (
-// <img key={i} src={url} alt={`预览${i}`} />
-// ))}
-// </div>
-// <div className="modal-actions">
-// <button className="btn cancel" onClick={() => setShowModal(false)}>
-// 取消
-// </button>
-// <button className="btn submit" onClick={handleSubmit}>
-// 发布
-// </button>
-// </div>
-// </div>
-// </div>
-// )}
-// </div>
-// );
-// };
-
-// export default FriendMoments;
-
-
// FriendMoments.js
import React, { useContext, useState, useEffect } from 'react';
import axios from 'axios';
@@ -1790,18 +452,18 @@
{/* 显示发布者信息 */}
<div className="feed-author">
<img
- className="user-avatar"
+ style={{ width: '70px', height: '70px', borderRadius: '50%' }}
src={feed.avatar_url || 'https://example.com/default-avatar.jpg'}
alt={feed.username || '用户头像'}
/>
<div>
- <h4>{feed.username || '未知用户'}</h4>
+ <div style={{ fontWeight: 'bold', fontSize: '20px', marginBottom: '5px' }}>{feed.username || '未知用户'}</div>
<span className="feed-date">{new Date(feed.postTime || Date.now()).toLocaleString()}</span>
</div>
</div>
- {feed.title && <h4 className="feed-title">{feed.title}</h4>}
- <p className="feed-content">{feed.postContent || '无内容'}</p>
+ {feed.title && <h4 style={{ fontWeight: 'bold', fontSize: '18px', margin: '15px 0' }}>{feed.title}</h4>}
+ <div style={{ margin: '20px 0' }}>{feed.postContent || '无内容'}</div>
{feed.imageUrl && (
<div className="feed-images">
@@ -1818,7 +480,7 @@
<div className="feed-footer">
<div className="like-container">
<button className="icon-btn" onClick={() => handleLike(feed.postNo, feed.liked, feed.user_id)}>
- <GoodTwo theme="outline" size="24" fill={feed.liked ? '#f00' : '#fff'} />
+ <GoodTwo theme="outline" size="24" fill={feed.liked ? '#ffa600dd' : '#000000'} />
<span>{feed.postLikeNum || 0}</span>
</button>
@@ -1828,8 +490,8 @@
toggleReplyBox(feed.postNo);
}}
>
- <Comment theme="outline" size="24" fill="#333" />
- <span>评论</span>
+ <Comment theme="outline" size="24" fill="#333" />评论
+ {/* <span style={{ fontSize: '14px', color: '#333' }}>评论</span> */}
</button>
</div>
@@ -1861,7 +523,7 @@
{/* 评论列表 */}
{Array.isArray(feed.comments) && feed.comments.length > 0 && (
<div className="comments-container">
- <h5>评论 ({feed.comments.length})</h5>
+ <h5 style={{ fontWeight: 'bold', fontSize: '18px', marginTop: '10px', marginBottom: '20px' }}>评论 ({feed.comments.length})</h5>
<div className="comments-list">
{feed.comments.map((comment, index) => (
<div className="comment-item" key={index}>
@@ -1873,10 +535,10 @@
</div>
<p className="comment-content">
{/* 显示回复格式 */}
- {comment.reply_to_username ?
+ {/* {comment.reply_to_username ?
<span className="reply-prefix">{comment.username} 回复 {comment.reply_to_username}:</span> :
<span>{comment.username}:</span>
- }
+ } */}
{comment.content}
</p>
<button
diff --git a/src/pages/PromotionsPage/PromotionsPage.css b/src/pages/PromotionsPage/PromotionsPage.css
deleted file mode 100644
index bc5bf8c..0000000
--- a/src/pages/PromotionsPage/PromotionsPage.css
+++ /dev/null
@@ -1,206 +0,0 @@
-/* .promotions-page {
- padding: 20px;
- font-family: Arial, sans-serif;
-}
-
-.promotions-container {
- max-width: 1200px;
- margin: 0 auto;
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 20px;
-}
-
-h1, h2, h3 {
- color: #333;
-}
-
-.admin-actions {
- margin-bottom: 20px;
-}
-
-.create-button, .submit-button, .delete-button {
- background-color: #4CAF50;
- color: white;
- border: none;
- padding: 10px 15px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- transition: background-color 0.3s;
-}
-
-.create-button:hover, .submit-button:hover {
- background-color: #45a049;
-}
-
-.delete-button {
- background-color: #f44336;
- margin-top: 10px;
-}
-
-.delete-button:hover {
- background-color: #d32f2f;
-}
-
-.create-promotion-form {
- background-color: #f9f9f9;
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
-}
-
-.form-group {
- margin-bottom: 15px;
-}
-
-.form-row {
- display: flex;
- gap: 20px;
-}
-
-.form-row .form-group {
- flex: 1;
-}
-
-label {
- display: block;
- margin-bottom: 5px;
- font-weight: bold;
-}
-
-input, select {
- width: 100%;
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- box-sizing: border-box;
-}
-
-.error-message {
- color: #f44336;
- margin-bottom: 15px;
- padding: 10px;
- background-color: #ffebee;
- border-radius: 4px;
-}
-
-.promotions-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
-}
-
-.promotions-list {
- border-right: 1px solid #eee;
- padding-right: 20px;
-}
-
-.promotion-items {
- max-height: 600px;
- overflow-y: auto;
-}
-
-.promotion-item {
- background-color: #f9f9f9;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 15px;
- cursor: pointer;
- transition: background-color 0.3s;
-}
-
-.promotion-item:hover {
- background-color: #f0f0f0;
-}
-
-.promotion-item.active {
- background-color: #e3f2fd;
- border-left: 4px solid #2196F3;
-}
-
-.promotion-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
-}
-
-.promotion-type {
- background-color: #2196F3;
- color: white;
- padding: 3px 8px;
- border-radius: 12px;
- font-size: 12px;
-}
-
-.promotion-dates {
- color: #666;
- font-size: 14px;
- margin-bottom: 10px;
-}
-
-.promotion-coeffs {
- display: flex;
- gap: 15px;
- font-weight: bold;
-}
-
-.promotion-coeffs span {
- background-color: #e3f2fd;
- padding: 3px 8px;
- border-radius: 4px;
-}
-
-.promotion-details {
- padding: 0 20px;
-}
-
-.detail-item {
- margin-bottom: 15px;
-}
-
-.detail-item label {
- font-weight: bold;
- color: #555;
-}
-
-.detail-item span {
- display: block;
- margin-top: 5px;
- padding: 8px;
- background-color: #f5f5f5;
- border-radius: 4px;
-}
-
-.no-promotions, .no-selection, .loading {
- text-align: center;
- padding: 40px;
- color: #888;
-}
-
-.pagination {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 15px;
- margin-top: 20px;
-}
-
-.pagination button {
- padding: 5px 10px;
- background-color: #f0f0f0;
- border: 1px solid #ddd;
- border-radius: 4px;
- cursor: pointer;
-}
-
-.pagination button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.pagination span {
- font-size: 14px;
-} */
\ No newline at end of file
diff --git a/src/pages/PromotionsPage/PromotionsPage.jsx b/src/pages/PromotionsPage/PromotionsPage.jsx
deleted file mode 100644
index ff01ef3..0000000
--- a/src/pages/PromotionsPage/PromotionsPage.jsx
+++ /dev/null
@@ -1,435 +0,0 @@
-// import React, { useState, useEffect } from 'react';
-// import './PromotionsPage.css';
-
-
-// function PromotionsPage() {
-// const [promotions, setPromotions] = useState([]);
-// const [currentPromotion, setCurrentPromotion] = useState(null);
-// const [isAdmin, setIsAdmin] = useState(false);
-// const [isLoading, setIsLoading] = useState(false);
-// const [error, setError] = useState(null);
-// const [formData, setFormData] = useState({
-// name: '',
-// uploadCoeff: 1,
-// downloadCoeff: 1,
-// timeRange: 0,
-// criteria: 1,
-// pStartTime: '',
-// pEndTime: ''
-// });
-// const [isCreating, setIsCreating] = useState(false);
-// const [currentPage, setCurrentPage] = useState(1);
-// const [perPage] = useState(10);
-// const [totalPromotions, setTotalPromotions] = useState(0);
-
-// const getAuthHeaders = () => {
-// const token = localStorage.getItem('token');
-// return {
-// 'Authorization': token ? `Bearer ${token}` : '',
-// 'Content-Type': 'application/json'
-// };
-// };
-
-// const fetchPromotions = async (page = 1) => {
-// setIsLoading(true);
-// try {
-// const response = await fetch(`/promotions/list?page=${page}&per_page=${perPage}`, {
-// headers: getAuthHeaders()
-// });
-
-// if (!response.ok) {
-// throw new Error('获取促销活动失败');
-// }
-
-// const data = await response.json();
-// if (data.code === 0 && data.result) {
-// setPromotions(data.rows || []);
-// setTotalPromotions(data.total || 0);
-// } else {
-// throw new Error(data.msg || '获取促销活动失败');
-// }
-// } catch (err) {
-// console.error('获取促销活动错误:', err);
-// setError(err.message);
-// } finally {
-// setIsLoading(false);
-// }
-// };
-
-// const fetchPromotionDetails = async (promoId) => {
-// setIsLoading(true);
-// try {
-// const response = await fetch(`/promotions/${promoId}`, {
-// headers: getAuthHeaders()
-// });
-
-// if (!response.ok) {
-// throw new Error('获取促销详情失败');
-// }
-
-// const data = await response.json();
-// if (data.code === 0 && data.result) {
-// setCurrentPromotion(data.rows);
-// } else {
-// throw new Error(data.msg || '获取促销详情失败');
-// }
-// } catch (err) {
-// console.error('获取促销详情错误:', err);
-// setError(err.message);
-// } finally {
-// setIsLoading(false);
-// }
-// };
-
-// const createPromotion = async () => {
-// if (!formData.name || !formData.pStartTime || !formData.pEndTime) {
-// alert('请填写完整活动信息');
-// return;
-// }
-
-// if (new Date(formData.pStartTime) >= new Date(formData.pEndTime)) {
-// alert('活动时间设置不正确,请重新设定');
-// return;
-// }
-
-// setIsLoading(true);
-// try {
-// const response = await fetch('/promotions/add', {
-// method: 'POST',
-// headers: getAuthHeaders(),
-// body: JSON.stringify(formData)
-// });
-
-// if (!response.ok) {
-// throw new Error('创建促销活动失败');
-// }
-
-// const data = await response.json();
-// if (data.code === 0 && data.result) {
-// alert(`活动创建成功!活动ID: ${data.msg}`);
-// setIsCreating(false);
-// setFormData({
-// name: '',
-// uploadCoeff: 1,
-// downloadCoeff: 1,
-// timeRange: 0,
-// criteria: 1,
-// pStartTime: '',
-// pEndTime: ''
-// });
-// fetchPromotions();
-// } else {
-// throw new Error(data.msg || '创建促销活动失败');
-// }
-// } catch (err) {
-// console.error('创建促销活动错误:', err);
-// alert(err.message);
-// } finally {
-// setIsLoading(false);
-// }
-// };
-
-// const deletePromotion = async (promoId) => {
-// if (!window.confirm('确定要删除这个促销活动吗?')) {
-// return;
-// }
-
-// setIsLoading(true);
-// try {
-// const response = await fetch(`/promotions/delete/${promoId}`, {
-// method: 'DELETE',
-// headers: getAuthHeaders()
-// });
-
-// if (!response.ok) {
-// throw new Error('删除促销活动失败');
-// }
-
-// const data = await response.json();
-// if (data.code === 0 && data.result) {
-// alert('促销活动删除成功');
-// fetchPromotions();
-// if (currentPromotion && currentPromotion.promoId === promoId) {
-// setCurrentPromotion(null);
-// }
-// } else {
-// throw new Error(data.msg || '删除促销活动失败');
-// }
-// } catch (err) {
-// console.error('删除促销活动错误:', err);
-// alert(err.message);
-// } finally {
-// setIsLoading(false);
-// }
-// };
-
-// const checkAdminStatus = () => {
-// const role = localStorage.getItem('role');
-// setIsAdmin(role === 'admin');
-// };
-
-// useEffect(() => {
-// checkAdminStatus();
-// fetchPromotions();
-// }, []);
-
-// const handleInputChange = (e) => {
-// const { name, value } = e.target;
-// setFormData(prev => ({
-// ...prev,
-// [name]: name === 'uploadCoeff' || name === 'downloadCoeff' || name === 'timeRange' || name === 'criteria'
-// ? parseFloat(value)
-// : value
-// }));
-// };
-
-// const handlePageChange = (newPage) => {
-// setCurrentPage(newPage);
-// fetchPromotions(newPage);
-// };
-
-// const getPromotionType = (promo) => {
-// if (promo.downloadCoeff === 0 && promo.uploadCoeff > 1) {
-// return '免费';
-// } else if (promo.downloadCoeff < 1 && promo.uploadCoeff > 1) {
-// return '折扣+上传奖励';
-// } else if (promo.downloadCoeff < 1) {
-// return '折扣';
-// } else if (promo.uploadCoeff > 1) {
-// return '上传奖励';
-// }
-// return '普通';
-// };
-
-// return (
-// <div className="promotions-page">
-// <div className="promotions-container">
-// <h1>促销活动</h1>
-
-// {isAdmin && (
-// <div className="admin-actions">
-// <button
-// className="create-button"
-// onClick={() => setIsCreating(!isCreating)}
-// >
-// {isCreating ? '取消创建' : '创建新活动'}
-// </button>
-// </div>
-// )}
-
-// {isCreating && isAdmin && (
-// <div className="create-promotion-form">
-// <h2>创建新促销活动</h2>
-// <div className="form-group">
-// <label>活动名称</label>
-// <input
-// type="text"
-// name="name"
-// value={formData.name}
-// onChange={handleInputChange}
-// placeholder="例如: 春节特惠"
-// />
-// </div>
-
-// <div className="form-row">
-// <div className="form-group">
-// <label>上传量系数</label>
-// <input
-// type="number"
-// name="uploadCoeff"
-// min="0"
-// step="0.1"
-// value={formData.uploadCoeff}
-// onChange={handleInputChange}
-// />
-// </div>
-
-// <div className="form-group">
-// <label>下载量系数</label>
-// <input
-// type="number"
-// name="downloadCoeff"
-// min="0"
-// step="0.1"
-// value={formData.downloadCoeff}
-// onChange={handleInputChange}
-// />
-// </div>
-// </div>
-
-// <div className="form-row">
-// <div className="form-group">
-// <label>资源时间范围</label>
-// <select
-// name="timeRange"
-// value={formData.timeRange}
-// onChange={handleInputChange}
-// >
-// <option value="0">全站资源</option>
-// <option value="1">当天上传</option>
-// <option value="2">最近两天</option>
-// <option value="7">最近一周</option>
-// <option value="30">最近一个月</option>
-// </select>
-// </div>
-
-// <div className="form-group">
-// <label>最低用户等级</label>
-// <input
-// type="number"
-// name="criteria"
-// min="1"
-// value={formData.criteria}
-// onChange={handleInputChange}
-// />
-// </div>
-// </div>
-
-// <div className="form-row">
-// <div className="form-group">
-// <label>开始时间</label>
-// <input
-// type="datetime-local"
-// name="pStartTime"
-// value={formData.pStartTime}
-// onChange={handleInputChange}
-// />
-// </div>
-
-// <div className="form-group">
-// <label>结束时间</label>
-// <input
-// type="datetime-local"
-// name="pEndTime"
-// value={formData.pEndTime}
-// onChange={handleInputChange}
-// />
-// </div>
-// </div>
-
-// <button
-// className="submit-button"
-// onClick={createPromotion}
-// disabled={isLoading}
-// >
-// {isLoading ? '创建中...' : '提交创建'}
-// </button>
-// </div>
-// )}
-
-// {error && <div className="error-message">{error}</div>}
-
-// <div className="promotions-grid">
-// {/* 促销活动列表 */}
-// <div className="promotions-list">
-// <h2>当前促销活动</h2>
-// {isLoading && promotions.length === 0 ? (
-// <div className="loading">加载中...</div>
-// ) : promotions.length === 0 ? (
-// <div className="no-promotions">暂无促销活动</div>
-// ) : (
-// <div className="promotion-items">
-// {promotions.map(promo => (
-// <div
-// key={promo.promoId}
-// className={`promotion-item ${currentPromotion && currentPromotion.promoId === promo.promoId ? 'active' : ''}`}
-// onClick={() => fetchPromotionDetails(promo.promoId)}
-// >
-// <div className="promotion-header">
-// <h3>{promo.name}</h3>
-// <span className="promotion-type">{getPromotionType(promo)}</span>
-// </div>
-// <div className="promotion-dates">
-// {new Date(promo.pStartTime).toLocaleString()} - {new Date(promo.pEndTime).toLocaleString()}
-// </div>
-// <div className="promotion-coeffs">
-// <span>上传: {promo.uploadCoeff}x</span>
-// <span>下载: {promo.downloadCoeff}x</span>
-// </div>
-// {isAdmin && (
-// <button
-// className="delete-button"
-// onClick={(e) => {
-// e.stopPropagation();
-// deletePromotion(promo.promoId);
-// }}
-// disabled={isLoading}
-// >
-// 删除
-// </button>
-// )}
-// </div>
-// ))}
-// </div>
-// )}
-
-// {totalPromotions > perPage && (
-// <div className="pagination">
-// <button
-// disabled={currentPage === 1}
-// onClick={() => handlePageChange(currentPage - 1)}
-// >
-// 上一页
-// </button>
-// <span>第 {currentPage} 页</span>
-// <button
-// disabled={currentPage * perPage >= totalPromotions}
-// onClick={() => handlePageChange(currentPage + 1)}
-// >
-// 下一页
-// </button>
-// </div>
-// )}
-// </div>
-
-// {/* 促销活动详情 */}
-// <div className="promotion-details">
-// {currentPromotion ? (
-// <>
-// <h2>{currentPromotion.name}</h2>
-// <div className="detail-item">
-// <label>活动ID:</label>
-// <span>{currentPromotion.promoId}</span>
-// </div>
-// <div className="detail-item">
-// <label>活动时间:</label>
-// <span>
-// {new Date(currentPromotion.pStartTime).toLocaleString()} - {new Date(currentPromotion.pEndTime).toLocaleString()}
-// </span>
-// </div>
-// <div className="detail-item">
-// <label>促销类型:</label>
-// <span>{getPromotionType(currentPromotion)}</span>
-// </div>
-// <div className="detail-item">
-// <label>上传量系数:</label>
-// <span>{currentPromotion.uploadCoeff}x</span>
-// </div>
-// <div className="detail-item">
-// <label>下载量系数:</label>
-// <span>{currentPromotion.downloadCoeff}x</span>
-// </div>
-// <div className="detail-item">
-// <label>适用资源:</label>
-// <span>
-// {currentPromotion.timeRange === 0
-// ? '全站资源'
-// : `最近${currentPromotion.timeRange}天内上传的资源`}
-// </span>
-// </div>
-// <div className="detail-item">
-// <label>参与条件:</label>
-// <span>用户等级 ≥ {currentPromotion.criteria}</span>
-// </div>
-// </>
-// ) : (
-// <div className="no-selection">请从左侧选择一个促销活动查看详情</div>
-// )}
-// </div>
-// </div>
-// </div>
-// </div>
-// );
-// }
-
-// export default PromotionsPage;
\ No newline at end of file
diff --git a/src/pages/SeedList/Recommend/PlaylistDetailPage.css b/src/pages/SeedList/Recommend/PlaylistDetailPage.css
index 78e7068..f9c09e6 100644
--- a/src/pages/SeedList/Recommend/PlaylistDetailPage.css
+++ b/src/pages/SeedList/Recommend/PlaylistDetailPage.css
@@ -12,7 +12,7 @@
font-weight: 700;
}
-.cover-image {
+.cover-img {
width: 100%;
max-height: 400px;
object-fit: cover;
diff --git a/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx b/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx
index 07558d6..54380e1 100644
--- a/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx
+++ b/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx
@@ -72,7 +72,7 @@
src={detail.coverUrl || '/default-cover.jpg'}
alt={detail.title}
onError={e => { e.target.src = '/default-cover.jpg'; }}
- className="cover-image"
+ className="cover-img"
/>
<p>{detail.description}</p>
diff --git a/src/pages/UserCenter/UserLevelExperience.jsx b/src/pages/UserCenter/UserLevelExperience.jsx
index 60dcd55..81135c1 100644
--- a/src/pages/UserCenter/UserLevelExperience.jsx
+++ b/src/pages/UserCenter/UserLevelExperience.jsx
@@ -7,6 +7,9 @@
// const [isLoading, setIsLoading] = useState(false);
// const [upgradeResult, setUpgradeResult] = useState(null);
// const [hasCheckedIn, setHasCheckedIn] = useState(false);
+// const [isUpgrading, setIsUpgrading] = useState(false);
+// const [lastUpgradeTime, setLastUpgradeTime] = useState(null);
+// const [justUpgraded, setJustUpgraded] = useState(false); // 新增
// useEffect(() => {
// if (!userId) return;
@@ -14,14 +17,16 @@
// }, [userId]);
// useEffect(() => {
-// // 自动触发升级判断
// if (
// experienceInfo &&
-// experienceInfo.current_experience >= experienceInfo.next_level_experience
+// experienceInfo.current_experience >= experienceInfo.next_level_experience &&
+// !isUpgrading &&
+// !justUpgraded &&
+// (!lastUpgradeTime || Date.now() - lastUpgradeTime > 2000)
// ) {
// checkUpgrade();
// }
-// }, [experienceInfo]);
+// }, [experienceInfo, isUpgrading, lastUpgradeTime, justUpgraded]);
// const fetchAllLevelData = async () => {
// try {
@@ -81,8 +86,11 @@
// };
// const checkUpgrade = async () => {
+// if (isUpgrading) return;
+
// try {
// setIsLoading(true);
+// setIsUpgrading(true);
// setError(null);
// const { data } = await axios.get('/echo/level/upgrade-check', {
@@ -90,46 +98,66 @@
// });
// if (data.can_upgrade) {
-// await performUpgrade(); // 自动触发
+// if (window.confirm('您已满足升级条件,是否要升级?')) {
+// await performUpgrade();
+// }
+// } else {
+// if (data.is_max_level) {
+// alert('您已达到最高等级!');
+// } else {
+// alert(`还不能升级,还需要${data.next_level_experience - data.current_experience}点经验值`);
+// }
// }
// } catch (err) {
// console.error('检查升级失败:', err);
// setError(err.response?.data?.message || '检查升级失败');
// } finally {
// setIsLoading(false);
+// setIsUpgrading(false);
// }
// };
+// const performUpgrade = async () => {
+// try {
+// setIsUpgrading(true);
+// setIsLoading(true);
+// setError(null);
-// const performUpgrade = async () => {
-// try {
-// setIsLoading(true);
-// setError(null);
+// const { data } = await axios.post('/echo/level/upgrades', {
+// user_id: userId,
+// can_upgrade: true,
+// });
-// const { data } = await axios.post('/echo/level/upgrades', {
-// user_id: userId,
-// can_upgrade: true,
-// });
+// console.log('升级响应数据:', data);
-// console.log('升级响应数据:', data); // 保留调试日志
+// if (data.status === 'success') {
+// setExperienceInfo((prev) => ({
+// ...prev,
+// current_level: data.new_level,
+// current_experience: data.current_experience || 0,
+// next_level_experience: data.next_level_experience || prev.next_level_experience * 2,
+// }));
-// setExperienceInfo((prev) => ({
-// ...prev,
-// current_level: data.new_level, // 修复:使用正确的字段名
-// current_experience: 0,
-// next_level_experience: prev.next_level_experience * 2,
-// }));
+// setUpgradeResult(data);
+// setLastUpgradeTime(Date.now());
+// setJustUpgraded(true); // 标记为刚升级过
+// setTimeout(() => setJustUpgraded(false), 3000); // 3 秒冷却期
+// alert(`恭喜!您已升级到等级 ${data.new_level}!`);
-// setUpgradeResult(data);
-// alert(`恭喜!您已升级到等级 ${data.new_level}!`); // 修复:使用正确的字段名
-// } catch (err) {
-// console.error('升级失败:', err);
-// setError(err.response?.data?.message || '升级失败');
-// } finally {
-// setIsLoading(false);
-// }
-// };
-
+// // 再次拉取最新经验数据,避免经验值仍然满足升级条件
+// await fetchAllLevelData();
+// } else {
+// throw new Error(data.message || '升级失败');
+// }
+// } catch (err) {
+// console.error('升级失败:', err);
+// setError(err.message || '升级失败');
+// alert(err.message || '升级失败,请稍后再试');
+// } finally {
+// setIsLoading(false);
+// setIsUpgrading(false);
+// }
+// };
// if (error) return <p className="error">{error}</p>;
// if (isLoading) return <p>加载中...</p>;
@@ -141,11 +169,11 @@
// (current_experience / (next_level_experience || 1)) * 100
// ).toFixed(2);
-// const expToNextLevel = Math.max(0, next_level_experience - current_experience); // 防止负数
+// const expToNextLevel = Math.max(0, next_level_experience - current_experience);
// return (
// <div className="level-experience-section">
-// {/* <h3>等级与经验</h3> */}
+// <h3>等级与经验</h3>
// <p><strong>当前等级:</strong>{current_level || '未知'}</p>
// <p><strong>当前经验:</strong>{current_experience}</p>
// <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
@@ -155,14 +183,17 @@
// </div>
// <p className="exp-progress-text">{progressPercent}%</p>
-
// {upgradeResult && (
// <div className="upgrade-success">
-// {/* 使用与状态一致的字段名 */}
-// <p>恭喜!您已成功升级到等级 {upgradeResult.new_level}!</p>
+// <p>恭喜!您已成功升级到等级 {upgradeResult.new_level}!</p>
// </div>
-// )}
-
+// )}
+
+// {error && (
+// <div className="upgrade-error">
+// <p>{error}</p>
+// </div>
+// )}
// <div className="level-actions">
// <button onClick={() => updateExperience('check-in', 15)} disabled={hasCheckedIn}>
@@ -170,7 +201,9 @@
// </button>
// <button onClick={() => updateExperience('task', 30)}>完成任务 (+30经验)</button>
// <button onClick={() => updateExperience('upload', 50)}>上传种子 (+50经验)</button>
-// {/* <button onClick={checkUpgrade}>检查升级</button> */}
+// <button onClick={checkUpgrade} disabled={isUpgrading}>
+// {isUpgrading ? '升级中...' : '检查升级'}
+// </button>
// </div>
// </div>
// );
@@ -178,198 +211,3 @@
// export default UserLevelExperience;
-import React, { useState, useEffect } from 'react';
-import axios from 'axios';
-
-const UserLevelExperience = ({ userId }) => {
- const [experienceInfo, setExperienceInfo] = useState(null);
- const [error, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [upgradeResult, setUpgradeResult] = useState(null);
- const [hasCheckedIn, setHasCheckedIn] = useState(false);
-
- useEffect(() => {
- if (!userId) return;
- fetchAllLevelData();
- }, [userId]);
-
- useEffect(() => {
- // 自动触发升级判断
- if (
- experienceInfo &&
- experienceInfo.current_experience >= experienceInfo.next_level_experience
- ) {
- checkUpgrade();
- }
- }, [experienceInfo]);
-
- const fetchAllLevelData = async () => {
- try {
- setIsLoading(true);
- setError(null);
-
- const { data } = await axios.get('/echo/level/getExperience', {
- params: { user_id: userId },
- });
-
- const normalizedData = {
- ...data,
- current_level: data.current_level || data.level,
- };
-
- setExperienceInfo(normalizedData);
-
- const today = new Date().toDateString();
- const lastCheckIn = localStorage.getItem('lastCheckIn');
- setHasCheckedIn(lastCheckIn === today);
- } catch (err) {
- console.error('经验信息获取失败:', err);
- setError('获取经验信息失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- const updateExperience = async (source, amount = 10) => {
- try {
- setIsLoading(true);
- setError(null);
-
- const { data } = await axios.post('/echo/level/updateExperience', {
- user_id: userId,
- experience: amount,
- source: source,
- });
-
- setExperienceInfo((prev) => ({
- ...prev,
- current_experience: data.current_experience,
- }));
-
- alert(`获得${amount}点经验值!来源:${source}`);
-
- if (source === 'check-in') {
- localStorage.setItem('lastCheckIn', new Date().toDateString());
- setHasCheckedIn(true);
- }
- } catch (err) {
- console.error('更新经验失败:', err);
- setError(err.response?.data?.message || '更新经验失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- const checkUpgrade = async () => {
- try {
- setIsLoading(true);
- setError(null);
-
- const { data } = await axios.get('/echo/level/upgrade-check', {
- params: { user_id: userId },
- });
-
- if (data.can_upgrade) {
- if (window.confirm('您已满足升级条件,是否要升级?')) {
- await performUpgrade();
- }
- } else {
- // 区分是经验不足还是已达最高等级
- if (data.is_max_level) {
- alert('您已达到最高等级!');
- } else {
- alert(`还不能升级,还需要${data.next_level_experience - data.current_experience}点经验值`);
- }
- }
- } catch (err) {
- console.error('检查升级失败:', err);
- setError(err.response?.data?.message || '检查升级失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- const performUpgrade = async () => {
- try {
- setIsLoading(true);
- setError(null);
-
- const { data } = await axios.post('/echo/level/upgrades', {
- user_id: userId,
- can_upgrade: true,
- });
-
- console.log('升级响应数据:', data);
-
- // 正确处理升级结果
- if (data.status === 'success') {
- setExperienceInfo((prev) => ({
- ...prev,
- current_level: data.new_level,
- current_experience: 0,
- next_level_experience: prev.next_level_experience * 2,
- }));
-
- setUpgradeResult(data);
- alert(`恭喜!您已升级到等级 ${data.new_level}!`);
- } else {
- throw new Error(data.message || '升级失败');
- }
- } catch (err) {
- console.error('升级失败:', err);
- setError(err.message || '升级失败');
- alert(err.message || '升级失败,请稍后再试');
- } finally {
- setIsLoading(false);
- }
- };
-
- if (error) return <p className="error">{error}</p>;
- if (isLoading) return <p>加载中...</p>;
- if (!experienceInfo) return <p>加载经验信息中...</p>;
-
- const { current_experience, next_level_experience, current_level } = experienceInfo;
- const progressPercent = Math.min(
- 100,
- (current_experience / (next_level_experience || 1)) * 100
- ).toFixed(2);
-
- const expToNextLevel = Math.max(0, next_level_experience - current_experience);
-
- return (
- <div className="level-experience-section">
- <h3>等级与经验</h3>
- <p><strong>当前等级:</strong>{current_level || '未知'}</p>
- <p><strong>当前经验:</strong>{current_experience}</p>
- <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
-
- <div className="exp-bar-wrapper">
- <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
- </div>
- <p className="exp-progress-text">{progressPercent}%</p>
-
- {upgradeResult && (
- <div className="upgrade-success">
- <p>恭喜!您已成功升级到等级 {upgradeResult.new_level}!</p>
- </div>
- )}
-
- {error && (
- <div className="upgrade-error">
- <p>{error}</p>
- </div>
- )}
-
- <div className="level-actions">
- <button onClick={() => updateExperience('check-in', 15)} disabled={hasCheckedIn}>
- {hasCheckedIn ? '今日已签到' : '每日签到 (+15经验)'}
- </button>
- <button onClick={() => updateExperience('task', 30)}>完成任务 (+30经验)</button>
- <button onClick={() => updateExperience('upload', 50)}>上传种子 (+50经验)</button>
- <button onClick={checkUpgrade}>检查升级</button>
- </div>
- </div>
- );
-};
-
-export default UserLevelExperience;
diff --git a/src/pages/UserCenter/UserNav.jsx b/src/pages/UserCenter/UserNav.jsx
index 63b83ca..7e09e2c 100644
--- a/src/pages/UserCenter/UserNav.jsx
+++ b/src/pages/UserCenter/UserNav.jsx
@@ -10,7 +10,7 @@
{ to: '/user/profile', label: '个人资料' },
{ to: '/user/dynamics', label: '我的动态' },
{ to: '/user/friends', label: '我的好友' },
- { to: '/user/groups', label: '我的群组' },
+ // { to: '/user/groups', label: '我的群组' },
{ to: '/user/collections', label: '我的收藏' },
// { to: '/user/newbie-tasks', label: '用户考核' },
{ to: '/user/invite', label: '邀请新用户' },
diff --git a/src/pages/UserCenter/UserProfile.jsx b/src/pages/UserCenter/UserProfile.jsx
index bcfbf0b..79d8313 100644
--- a/src/pages/UserCenter/UserProfile.jsx
+++ b/src/pages/UserCenter/UserProfile.jsx
@@ -1,7 +1,8 @@
import React from 'react';
import UserProfileBase from './UserProfileBase';
-import UserLevelExperience from './UserLevelExperience';
+// import UserLevelExperience from './UserLevelExperience';
import './UserProfile.css';
+import UserStatusChecker from './UserStatusChecker';
const UserProfile = () => {
const [userId, setUserId] = React.useState(null);
@@ -13,7 +14,8 @@
return (
<div>
<UserProfileBase onLoadExperienceInfo={loadExperienceInfo} />
- {userId && <UserLevelExperience userId={userId} />}
+ {/* {userId && <UserLevelExperience userId={userId} />} */}
+ {/* <UserStatusChecker /> */}
</div>
);
};
diff --git a/src/pages/UserCenter/UserStatusChecker.jsx b/src/pages/UserCenter/UserStatusChecker.jsx
new file mode 100644
index 0000000..7b8b303
--- /dev/null
+++ b/src/pages/UserCenter/UserStatusChecker.jsx
@@ -0,0 +1,58 @@
+import React, { useEffect, useState, useContext } from 'react';
+import axios from 'axios';
+import { UserContext } from '../../context/UserContext';
+
+const UserStatusChecker = () => {
+ const { user } = useContext(UserContext); // 假设你在上下文中已经有 user 信息
+ const [statusMessage, setStatusMessage] = useState('');
+ const [statusType, setStatusType] = useState('normal'); // normal / warning / danger
+
+ useEffect(() => {
+ if (user?.id) {
+ checkUserShareRate(user.id);
+ }
+ }, [user]);
+
+ const checkUserShareRate = async (userId) => {
+ try {
+ const response = await axios.get(`/users/${userId}/share-rate`);
+ const message = response.data;
+
+ setStatusMessage(message);
+
+ if (message.includes('账号已被注销')) {
+ setStatusType('danger');
+ } else if (message.includes('警告')) {
+ setStatusType('warning');
+ } else {
+ setStatusType('normal');
+ }
+
+ } catch (error) {
+ console.error('获取用户状态失败:', error);
+ setStatusMessage('无法获取用户状态');
+ setStatusType('danger');
+ }
+ };
+
+ const getStatusStyle = () => {
+ switch (statusType) {
+ case 'warning':
+ return { backgroundColor: '#fff3cd', color: '#856404', padding: '10px', borderRadius: '8px' };
+ case 'danger':
+ return { backgroundColor: '#f8d7da', color: '#721c24', padding: '10px', borderRadius: '8px' };
+ default:
+ return { backgroundColor: '#d4edda', color: '#155724', padding: '10px', borderRadius: '8px' };
+ }
+ };
+
+ return (
+ <div style={{ marginTop: '20px' }}>
+ <div style={getStatusStyle()}>
+ <strong>账号状态提示:</strong> {statusMessage}
+ </div>
+ </div>
+ );
+};
+
+export default UserStatusChecker;