修改密码、管理员删帖、促销、退出登录
Change-Id: I2cc0e211ac5a04f9e89d0736fadd25541a5fccb9
diff --git a/src/pages/Forum/posts-main/components/PostList.jsx b/src/pages/Forum/posts-main/components/PostList.jsx
index 5d14fdd..533e726 100644
--- a/src/pages/Forum/posts-main/components/PostList.jsx
+++ b/src/pages/Forum/posts-main/components/PostList.jsx
@@ -4,9 +4,9 @@
import { GoodTwo, Star, Delete } from '@icon-park/react';
import { likePost } from '../../posts-detail/api';
import { formatAvatarUrl } from '../../../../components/utils/avatar';
+import { useUser } from '../../../../context/UserContext'; // 路径根据实际调整
import './PostList.css';
-// 修改后的封面图 URL 拼接函数
const formatImageUrl = (url) => {
if (!url) return '';
const filename = url.split('/').pop(); // 提取文件名部分
@@ -14,6 +14,7 @@
};
const PostList = ({ search }) => {
+ const { user } = useUser(); // 获取当前登录用户
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
@@ -116,7 +117,17 @@
}
};
- const handleDeletePost = async (postNo) => {
+ const handleDeletePost = async (postNo, postUserId) => {
+ // 权限判断:管理员或者帖子发布者本人
+ if (!user) {
+ alert('请先登录');
+ return;
+ }
+ if (user.role !== 'admin' && user.userId !== postUserId) {
+ alert('您没有权限删除此帖子');
+ return;
+ }
+
if (window.confirm('确定要删除这篇帖子吗?')) {
try {
await axios.delete(`/echo/forum/posts/${postNo}/deletePost`);
@@ -144,17 +155,10 @@
coverImage = imgs.length > 0 ? formatImageUrl(imgs[0]) : null;
}
+ const canDelete = user && (user.role === 'admin' || user.userId === post.user_id);
+
return (
<div key={post.postNo} className="post-card" style={{ backgroundColor: '#e9ded2' }}>
- {/* <div className="user-info">
- <img
- className="avatar"
- src={post.avatarUrl}
- alt="头像"
- />
- <span className="nickname" style={{ color: '#755e50' }}>{post.username}</span>
- </div> */}
-
<div className="user-info">
<Link href={`/information/${post.user_id}`}>
<img
@@ -167,7 +171,6 @@
<span className="nickname" style={{ color: '#755e50' }}>{post.username}</span>
</div>
-
{coverImage && <img className="cover-image" src={coverImage} alt="封面" />}
<h3 style={{ color: '#000000' }}>{post.title || '无标题'}</h3>
@@ -182,9 +185,12 @@
<Star theme="outline" size="24" fill={post.collected ? '#ffd700' : '#fff'} />
<span>{post.collectCount}</span>
</button>
- <button className="icon-btn" onClick={() => handleDeletePost(post.postNo)}>
- <Delete theme="outline" size="24" fill="#333" />
- </button>
+
+ {canDelete && (
+ <button className="icon-btn" onClick={() => handleDeletePost(post.postNo, post.user_id)}>
+ <Delete theme="outline" size="24" fill="#333" />
+ </button>
+ )}
</div>
</div>
<div className="detail-button-wrapper">
@@ -205,3 +211,4 @@
};
export default PostList;
+
diff --git a/src/pages/Forum/promotion-part/Promotion.css b/src/pages/Forum/promotion-part/Promotion.css
index 2232ba5..46e7a0d 100644
--- a/src/pages/Forum/promotion-part/Promotion.css
+++ b/src/pages/Forum/promotion-part/Promotion.css
@@ -108,3 +108,49 @@
font-size: 16px;
text-align: center;
}
+
+.dialog-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;
+}
+.dialog {
+ background: #fff;
+ padding: 20px;
+ border-radius: 6px;
+ width: 400px;
+ max-width: 90%;
+ box-shadow: 0 0 10px rgba(0,0,0,0.25);
+}
+.form-item {
+ margin-bottom: 12px;
+ display: flex;
+ flex-direction: column;
+}
+.form-item label {
+ font-weight: bold;
+ margin-bottom: 4px;
+}
+.form-item input, .form-item textarea {
+ padding: 6px 8px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+.dialog-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+.dialog-buttons button {
+ padding: 6px 16px;
+ cursor: pointer;
+}
+
diff --git a/src/pages/Forum/promotion-part/Promotion.jsx b/src/pages/Forum/promotion-part/Promotion.jsx
index 28fd7ac..8ca4067 100644
--- a/src/pages/Forum/promotion-part/Promotion.jsx
+++ b/src/pages/Forum/promotion-part/Promotion.jsx
@@ -1,17 +1,32 @@
-// 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 [showCreateDialog, setShowCreateDialog] = useState(false);
+
+ // 创建促销活动表单状态
+ const [formData, setFormData] = useState({
+ name: '',
+ startTime: '',
+ endTime: '',
+ discountPercentage: '',
+ uploadCoeff: '',
+ downloadCoeff: '',
+ description: ''
+ });
+
useEffect(() => {
fetchData();
+ fetchTorrentList();
}, []);
useEffect(() => {
@@ -25,11 +40,9 @@
const fetchData = async () => {
try {
- // ✅ 获取促销活动列表(新接口)
- const promoResponse = await fetch(`/seeds/promotions`);
- const promoJson = await promoResponse.json();
- console.log('接口返回数据:', promoJson);
- const promoData = Array.isArray(promoJson?.result) ? promoJson.result : [];
+ 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);
@@ -38,19 +51,140 @@
}
};
+ 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>;
}
- const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
- const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
- const currentPromo = promotions[promoIndex];
-
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>
) : (
@@ -76,14 +210,616 @@
{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;
+
diff --git a/src/pages/InterestGroup/GroupItem.jsx b/src/pages/InterestGroup/GroupItem.jsx
index 8fdc640..736f88d 100644
--- a/src/pages/InterestGroup/GroupItem.jsx
+++ b/src/pages/InterestGroup/GroupItem.jsx
@@ -1,20 +1,182 @@
-import React, { useState } from 'react';
+// import React, { useState, useEffect } from 'react';
+// import { useGroupStore } from '../../context/useGroupStore';
+// import { useUser } from '../../context/UserContext';
+// import CreatePostForm from './CreatePostForm';
+// import axios from 'axios'; // 新增
+
+// const GroupItem = ({ group }) => {
+// const { handleJoinGroup, joinStatus, setJoinStatus } = useGroupStore(); // 假设你有 setJoinStatus 方法
+// const { user } = useUser();
+
+// const userId = user?.userId;
+// const groupId = group.groupId;
+
+// const [isMember, setIsMember] = useState(false);
+
+// useEffect(() => {
+// setIsMember(joinStatus[groupId] === '加入成功');
+// }, [joinStatus, groupId]);
+
+// const [showCreatePost, setShowCreatePost] = useState(false);
+
+// // 退出小组函数(新增)
+// const handleLeaveGroup = async () => {
+// try {
+// const res = await axios.post(`/echo/groups/${groupId}/leave`, {
+// user_id: userId,
+// });
+// if (res.data.status === 'success') {
+// setJoinStatus(groupId, '未加入'); // 更新全局状态(需确保 useGroupStore 中有此方法)
+// setIsMember(false); // 本地状态也更新
+// } else {
+// alert(res.data.message || '退出失败');
+// }
+// } catch (error) {
+// console.error('退出小组失败:', error);
+// alert('退出小组失败');
+// }
+// };
+
+// return (
+// <div className="group-item">
+// <div className="group-content">
+// <img
+// style={{ width: '40%', height: '40%' }}
+// src={group.coverImage || 'https://picsum.photos/200/200'}
+// alt={group.groupName}
+// className="group-cover"
+// />
+// <div className="group-info-right">
+// <h3>{group.groupName}</h3>
+// <p style={{ color: '#BA929A' }}>{group.memberCount || 0}人加入了小组</p>
+
+// {/* 加入/退出按钮逻辑 */}
+// {userId && (
+// <button
+// onClick={() => {
+// if (isMember) {
+// handleLeaveGroup(); // 已加入 -> 退出
+// } else {
+// handleJoinGroup(groupId, userId); // 未加入 -> 加入
+// }
+// }}
+// >
+// {isMember ? '退出小组' : '+加入小组'}
+// </button>
+// )}
+// {!userId && <button disabled>请登录</button>}
+
+// {/* 发布帖子按钮 */}
+// {userId && isMember && (
+// <button onClick={() => setShowCreatePost(!showCreatePost)}>
+// +发布帖子
+// </button>
+// )}
+// </div>
+// </div>
+
+// <div className="group-description">
+// <p>{group.description}</p>
+// </div>
+// <p>分类:{group.category}</p>
+
+// {showCreatePost && (
+// <CreatePostForm
+// groupId={groupId}
+// onClose={() => setShowCreatePost(false)}
+// />
+// )}
+// </div>
+// );
+// };
+
+// export default GroupItem;
+
+import React, { useState, useEffect } from 'react';
import { useGroupStore } from '../../context/useGroupStore';
import { useUser } from '../../context/UserContext';
import CreatePostForm from './CreatePostForm';
+import axios from 'axios';
const GroupItem = ({ group }) => {
-
- console.log('group:', group);
- const { handleJoinGroup, joinStatus } = useGroupStore();
+ const { handleJoinGroup, joinStatus, setJoinStatus } = useGroupStore();
const { user } = useUser();
-
+
const userId = user?.userId;
- const groupId = group.groupId; // ✅ 使用正确字段
-console.log('加入小组请求 - groupId:', group.group_id, 'userId:', userId);
+ const groupId = group.groupId;
+
+ const [isMember, setIsMember] = useState(false);
+ const [loading, setLoading] = useState(false); // 新增:加载状态
+ const [error, setError] = useState(''); // 新增:错误信息
+
+ useEffect(() => {
+ console.log('joinStatus updated:', joinStatus);
+ setIsMember(joinStatus[groupId] === '加入成功');
+ }, [joinStatus, groupId]);
+
+ // 初始挂载时检查成员状态(新增)
+ useEffect(() => {
+ if (userId && groupId) {
+ checkMembershipStatus();
+ }
+ }, [userId, groupId]);
+
+ // 检查成员状态(新增)
+ const checkMembershipStatus = async () => {
+ try {
+ const res = await axios.get(`/echo/groups/${groupId}/members`);
+ const isMember = res.data.members.some(member => member.user_id === userId);
+ setJoinStatus(groupId, isMember ? '加入成功' : '未加入');
+ } catch (error) {
+ console.error('检查成员状态失败:', error);
+ }
+ };
const [showCreatePost, setShowCreatePost] = useState(false);
+ const handleLeaveGroup = async () => {
+ setLoading(true);
+ try {
+ const res = await axios.post(`/echo/groups/${groupId}/leave`, {
+ user_id: userId,
+ });
+ if (res.data.status === 'success') {
+ setJoinStatus(groupId, '未加入');
+ setIsMember(false);
+ // 可选:刷新小组成员计数
+ group.memberCount = (group.memberCount || 0) - 1;
+ } else {
+ setError(res.data.message || '退出失败');
+ }
+ } catch (error) {
+ console.error('退出小组失败:', error);
+ setError('退出小组失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 修改加入小组逻辑(新增)
+ const handleJoin = async () => {
+ setLoading(true);
+ try {
+ const res = await handleJoinGroup(groupId, userId);
+ if (res && res.status === 'success') {
+ setJoinStatus(groupId, '加入成功');
+ setIsMember(true);
+ // 可选:刷新小组成员计数
+ group.memberCount = (group.memberCount || 0) + 1;
+ } else {
+ setError(res?.message || '加入失败');
+ }
+ } catch (error) {
+ console.error('加入小组失败:', error);
+ setError('加入小组失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
return (
<div className="group-item">
<div className="group-content">
@@ -25,18 +187,31 @@
className="group-cover"
/>
<div className="group-info-right">
- <h3>{group.groupName}</h3> {/* ✅ 使用 groupName */}
+ <h3>{group.groupName}</h3>
<p style={{ color: '#BA929A' }}>{group.memberCount || 0}人加入了小组</p>
- <button
- onClick={() => handleJoinGroup(groupId, userId)}
- disabled={joinStatus[groupId] === '加入成功' || !userId}
- >
-
- {joinStatus[groupId] === '加入成功' ? '已加入' : userId ? '+加入小组' : '请登录'}
- </button>
+ {/* 加入/退出按钮逻辑 */}
+ {userId && (
+ <button
+ onClick={() => {
+ if (isMember) {
+ handleLeaveGroup();
+ } else {
+ handleJoin();
+ }
+ }}
+ disabled={loading}
+ >
+ {loading ? '处理中...' : isMember ? '退出小组' : '+加入小组'}
+ </button>
+ )}
+ {!userId && <button disabled>请登录</button>}
- {userId && joinStatus[groupId] === '加入成功' && (
+ {/* 显示错误信息(新增) */}
+ {error && <p style={{ color: 'red' }}>{error}</p>}
+
+ {/* 发布帖子按钮 */}
+ {userId && isMember && (
<button onClick={() => setShowCreatePost(!showCreatePost)}>
+发布帖子
</button>
@@ -59,4 +234,4 @@
);
};
-export default GroupItem;
\ No newline at end of file
+export default GroupItem;
diff --git a/src/pages/PromotionsPage/PromotionsPage.jsx b/src/pages/PromotionsPage/PromotionsPage.jsx
index 834ad73..ff01ef3 100644
--- a/src/pages/PromotionsPage/PromotionsPage.jsx
+++ b/src/pages/PromotionsPage/PromotionsPage.jsx
@@ -1,435 +1,435 @@
-import React, { useState, useEffect } from 'react';
-import './PromotionsPage.css';
+// 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);
+// 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 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()
- });
+// 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('获取促销活动失败');
- }
+// 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 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()
- });
+// const fetchPromotionDetails = async (promoId) => {
+// setIsLoading(true);
+// try {
+// const response = await fetch(`/promotions/${promoId}`, {
+// headers: getAuthHeaders()
+// });
- if (!response.ok) {
- throw new Error('获取促销详情失败');
- }
+// 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 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;
- }
+// const createPromotion = async () => {
+// if (!formData.name || !formData.pStartTime || !formData.pEndTime) {
+// alert('请填写完整活动信息');
+// return;
+// }
- if (new Date(formData.pStartTime) >= new Date(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)
- });
+// setIsLoading(true);
+// try {
+// const response = await fetch('/promotions/add', {
+// method: 'POST',
+// headers: getAuthHeaders(),
+// body: JSON.stringify(formData)
+// });
- if (!response.ok) {
- throw new Error('创建促销活动失败');
- }
+// 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 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;
- }
+// const deletePromotion = async (promoId) => {
+// if (!window.confirm('确定要删除这个促销活动吗?')) {
+// return;
+// }
- setIsLoading(true);
- try {
- const response = await fetch(`/promotions/delete/${promoId}`, {
- method: 'DELETE',
- headers: getAuthHeaders()
- });
+// setIsLoading(true);
+// try {
+// const response = await fetch(`/promotions/delete/${promoId}`, {
+// method: 'DELETE',
+// headers: getAuthHeaders()
+// });
- if (!response.ok) {
- throw new Error('删除促销活动失败');
- }
+// 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 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');
- };
+// const checkAdminStatus = () => {
+// const role = localStorage.getItem('role');
+// setIsAdmin(role === 'admin');
+// };
- useEffect(() => {
- checkAdminStatus();
- fetchPromotions();
- }, []);
+// 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 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 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 '普通';
- };
+// 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>
+// 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>
- )}
+// {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>
+// {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-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-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-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-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-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>
+// <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>
- )}
+// <button
+// className="submit-button"
+// onClick={createPromotion}
+// disabled={isLoading}
+// >
+// {isLoading ? '创建中...' : '提交创建'}
+// </button>
+// </div>
+// )}
- {error && <div className="error-message">{error}</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>
- )}
+// <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>
+// {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>
- );
-}
+// {/* 促销活动详情 */}
+// <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
+// export default PromotionsPage;
\ No newline at end of file
diff --git a/src/pages/SeedList/SeedList.jsx b/src/pages/SeedList/SeedList.jsx
index a010840..adb2bb3 100644
--- a/src/pages/SeedList/SeedList.jsx
+++ b/src/pages/SeedList/SeedList.jsx
@@ -74,6 +74,7 @@
const params = buildQueryParams();
const response = await axios.get('/seeds/list', params);
// const response = await axios.get('/seeds/list', { params });
+
const data = response.data;
if (data.code !== 0) {
diff --git a/src/pages/UserCenter/UserDynamics.css b/src/pages/UserCenter/UserDynamics.css
index e69de29..40a11b4 100644
--- a/src/pages/UserCenter/UserDynamics.css
+++ b/src/pages/UserCenter/UserDynamics.css
@@ -0,0 +1,79 @@
+.user-dynamics-container {
+ padding: 40px 10% 40px 5%;
+ max-width: 900px;
+}
+
+.user-dynamics-title {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 20px;
+}
+
+.user-dynamics-loading,
+.user-dynamics-empty {
+ font-size: 16px;
+ color: #888;
+ margin-top: 20px;
+}
+
+.dynamic-card {
+ background: #fff;
+ border: 1px solid #eee;
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
+}
+
+.dynamic-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.dynamic-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ margin-right: 12px;
+}
+
+.dynamic-userinfo {
+ display: flex;
+ flex-direction: column;
+}
+
+.dynamic-username {
+ font-weight: bold;
+ font-size: 16px;
+}
+
+.dynamic-time {
+ font-size: 12px;
+ color: #999;
+}
+
+.dynamic-title {
+ font-size: 18px;
+ margin: 10px 0 5px;
+}
+
+.dynamic-content p {
+ font-size: 15px;
+ line-height: 1.5;
+}
+
+.dynamic-images {
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 10px;
+ gap: 10px;
+}
+
+.dynamic-images img {
+ width: 120px;
+ height: 120px;
+ object-fit: cover;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+}
diff --git a/src/pages/UserCenter/UserDynamics.jsx b/src/pages/UserCenter/UserDynamics.jsx
index e69de29..05d9046 100644
--- a/src/pages/UserCenter/UserDynamics.jsx
+++ b/src/pages/UserCenter/UserDynamics.jsx
@@ -0,0 +1,88 @@
+import React, { useEffect, useState } from 'react';
+import { useUser } from '../../context/UserContext';
+import './UserDynamics.css';
+
+const UserDynamics = () => {
+ const { user } = useUser();
+ const [dynamics, setDynamics] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!user?.userId) return;
+
+ const fetchDynamics = async () => {
+ try {
+ const res = await fetch(`/echo/dynamic/${user.userId}/getAdynamic`);
+ const data = await res.json();
+ setDynamics(data.dynamic || []);
+ } catch (err) {
+ console.error('获取动态失败:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchDynamics();
+ }, [user]);
+
+ if (loading) return <div className="user-dynamics-loading">加载中...</div>;
+
+ return (
+ <div className="user-dynamics-container">
+ <h2 className="user-dynamics-title">我的动态</h2>
+ {dynamics.length === 0 ? (
+ <div className="user-dynamics-empty">暂无动态</div>
+ ) : (
+ dynamics.map((item) => (
+ <div key={item.dynamic_id} className="dynamic-card">
+ <div className="dynamic-header">
+ <img
+ className="dynamic-avatar"
+ src={item.avatar_url}
+ alt={item.username}
+ />
+ <div className="dynamic-userinfo">
+ <span className="dynamic-username">{item.username}</span>
+ <span className="dynamic-time">{new Date(item.time).toLocaleString()}</span>
+ </div>
+ </div>
+ <div className="dynamic-content">
+ {item.title && <h4 className="dynamic-title">{item.title}</h4>}
+ <p>{item.content}</p>
+ {/* {item.images && (
+ <div className="dynamic-images">
+ {JSON.parse(item.images).map((img, index) => (
+ <img key={index} src={img} alt={`图${index + 1}`} />
+ ))}
+ </div>
+ )} */}
+ {item.images && (
+ <div className="dynamic-images">
+ {(() => {
+ let imageList = [];
+ try {
+ if (item.images.startsWith('[')) {
+ imageList = JSON.parse(item.images);
+ } else {
+ imageList = [item.images];
+ }
+ } catch (e) {
+ console.error('解析 images 出错:', e);
+ }
+
+ return imageList.map((img, index) => (
+ <img key={index} src={img} alt={`图${index + 1}`} />
+ ));
+ })()}
+ </div>
+ )}
+
+ </div>
+ </div>
+ ))
+ )}
+ </div>
+ );
+};
+
+export default UserDynamics;
diff --git a/src/pages/UserCenter/UserProfile.css b/src/pages/UserCenter/UserProfile.css
index de2ecac..b230c1e 100644
--- a/src/pages/UserCenter/UserProfile.css
+++ b/src/pages/UserCenter/UserProfile.css
@@ -52,9 +52,11 @@
border-radius: 16px;
margin: 0 auto;
margin-top: 40px;
- padding: 10% 20%;
- margin-left: 5%;
- margin-right: 5%;
+ width: 80%;
+ padding-top: 10%;
+ padding-right: 15%;
+ padding-bottom: 10%;
+ padding-left: 10%;
}
.avatar-wrapper {
position: relative;
@@ -208,7 +210,7 @@
}
.task-btn:hover {
- background-color: #357abd;
+ background-color: #bd7035;
}
.task-btn-group {
@@ -218,10 +220,69 @@
gap: 8px;
}
-.loading {
- text-align: center;
- font-size: 18px;
- color: #333;
+
+.exp-bar-wrapper {
+ width: 100%;
+ height: 14px;
+ background-color: #fbfafa;
+ border-radius: 8px;
+ margin: 10px 0;
+ overflow: hidden;
}
+.exp-bar {
+ height: 100%;
+ background-color: #0eb813;
+ transition: width 0.3s ease;
+}
+.exp-progress-text {
+ font-size: 0.9em;
+ color: #555;
+ margin-bottom: 10px;
+}
+
+.profile-actions {
+ margin-top: 1em;
+}
+
+.profile-actions button {
+ padding: 8px 16px;
+ background-color: #4677f5;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.4);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 99;
+}
+
+.modal-content {
+ background-color: white;
+ padding: 20px;
+ border-radius: 8px;
+ width: 300px;
+}
+
+.modal-content input {
+ display: block;
+ width: 100%;
+ margin: 10px 0;
+ padding: 8px;
+}
+
+.modal-buttons {
+ display: flex;
+ justify-content: space-between;
+}
diff --git a/src/pages/UserCenter/UserProfile.jsx b/src/pages/UserCenter/UserProfile.jsx
index 9170652..c6e80e0 100644
--- a/src/pages/UserCenter/UserProfile.jsx
+++ b/src/pages/UserCenter/UserProfile.jsx
@@ -2,13 +2,25 @@
import axios from 'axios';
import './UserProfile.css';
import { useUser } from '../../context/UserContext';
+import { useLocation } from 'wouter';
+
const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
const UserProfile = () => {
- const { user, loading } = useUser();
+ const { user, loading, logout } = useUser();
const [userProfile, setUserProfile] = useState(null);
+ const [experienceInfo, setExperienceInfo] = useState(null);
const [error, setError] = useState(null);
+ // 修改密码状态
+ const [showPwdModal, setShowPwdModal] = useState(false);
+ const [oldPassword, setOldPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+
+ // 退出登录
+ const [, setLocation] = useLocation();
+
useEffect(() => {
if (loading) return;
if (!user || !user.userId) {
@@ -21,7 +33,6 @@
try {
setError(null);
const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
-
if (!raw) {
setError('用户数据为空');
setUserProfile(null);
@@ -29,9 +40,9 @@
}
const profile = {
- avatarUrl: raw.avatarUrl
- ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
- : DEFAULT_AVATAR_URL,
+ avatarUrl: raw.avatarUrl
+ ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
+ : DEFAULT_AVATAR_URL,
nickname: raw.username || '未知用户',
email: raw.email || '未填写',
gender: raw.gender || '保密',
@@ -52,7 +63,19 @@
}
};
+ const fetchExperienceInfo = async () => {
+ try {
+ const { data } = await axios.get('/echo/level/getExperience', {
+ params: { user_id: user.userId },
+ });
+ setExperienceInfo(data);
+ } catch (err) {
+ console.error('经验信息获取失败:', err);
+ }
+ };
+
fetchUserProfile();
+ fetchExperienceInfo();
}, [user, loading]);
const handleAvatarUpload = async (e) => {
@@ -63,11 +86,11 @@
formData.append('file', file);
try {
- const { data } = await axios.post(
- `/echo/user/${user.userId}/uploadAvatar`,
- formData,
- { headers: { 'Content-Type': 'multipart/form-data' } }
- );
+ const { data } = await axios.post(
+ `/echo/user/${user.userId}/uploadAvatar`,
+ formData,
+ { headers: { 'Content-Type': 'multipart/form-data' } }
+ );
if (data?.avatarUrl) {
setUserProfile((prev) => ({
@@ -84,6 +107,42 @@
}
};
+ const handleLogout = () => {
+ logout();
+ setLocation('/auth'); // 退出后跳转登录页
+ // window.location.reload(); // 或跳转登录页
+ };
+
+ const handleChangePassword = async () => {
+ if (!oldPassword || !newPassword || !confirmPassword) {
+ alert('请填写所有字段');
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ alert('两次输入的新密码不一致');
+ return;
+ }
+
+ try {
+ // await axios.post('/echo/user/password', {
+ // user_id: user.userId,
+ // oldPassword,
+ // newPassword,
+ // });
+ await axios.post('/echo/user/password', {
+ user_id: user.userId,
+ old_password: oldPassword,
+ new_password: newPassword,
+ confirm_password: confirmPassword,
+ });
+ alert('密码修改成功,请重新登录');
+ logout();
+ window.location.reload();
+ } catch (err) {
+ alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
+ }
+ };
+
if (loading) return <p>正在加载用户信息...</p>;
if (error) return <p className="error">{error}</p>;
if (!userProfile) return null;
@@ -103,6 +162,19 @@
joinedDate,
} = userProfile;
+ const progressPercent = experienceInfo
+ ? Math.min(
+ 100,
+ ((experienceInfo.current_experience || 0) /
+ (experienceInfo.next_level_experience || 1)) *
+ 100
+ ).toFixed(2)
+ : 0;
+
+ const expToNextLevel = experienceInfo
+ ? (experienceInfo.next_level_experience - experienceInfo.current_experience)
+ : null;
+
return (
<div className="common-card">
<div className="right-content">
@@ -134,10 +206,58 @@
<p><strong>下载量:</strong>{downloadAmount}</p>
<p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
<p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
+
+ {experienceInfo && (
+ <>
+ <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>
+ </>
+ )}
+
+ {/* 修改密码与退出登录按钮 */}
+ <div className="profile-actions">
+ <button onClick={() => setShowPwdModal(true)}>修改密码</button>
+ <button onClick={handleLogout}>退出登录</button>
+ </div>
+
+ {/* 修改密码弹窗 */}
+ {showPwdModal && (
+ <div className="modal">
+ <div className="modal-content">
+ <h3>修改密码</h3>
+ <input
+ type="password"
+ placeholder="原密码"
+ value={oldPassword}
+ onChange={(e) => setOldPassword(e.target.value)}
+ />
+ <input
+ type="password"
+ placeholder="新密码"
+ value={newPassword}
+ onChange={(e) => setNewPassword(e.target.value)}
+ />
+ <input
+ type="password"
+ placeholder="确认新密码"
+ value={confirmPassword}
+ onChange={(e) => setConfirmPassword(e.target.value)}
+ />
+ <div className="modal-buttons">
+ <button onClick={handleChangePassword}>确认修改</button>
+ <button onClick={() => setShowPwdModal(false)}>取消</button>
+ </div>
+ </div>
+ </div>
+ )}
</div>
</div>
</div>
);
};
-export default UserProfile;
\ No newline at end of file
+export default UserProfile;
+