blob: 836daa7622ead0daac015df62d6ba267f8d99195 [file] [log] [blame]
import React, {useEffect, useState} from 'react';
import {useNavigate, useLocation, useParams} from 'react-router-dom';
// import { getUserInfo } from '../api/auth';
import {createTorrent, getTorrents} from '../api/torrent';
import './Dashboard.css';
import {createPost, getPosts, getPostDetail} from '../api/helpPost';
const Dashboard = ({onLogout}) => {
const location = useLocation();
const [userInfo, setUserInfo] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [currentSlide, setCurrentSlide] = useState(0);
const navigate = useNavigate();
const {tab} = useParams();
const [showUploadModal, setShowUploadModal] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [uploadData, setUploadData] = useState({
name: '',
type: '',
region: '',
subtitle: '',
resolution: '', // 新增分辨率字段
file: null,
description: ''
});
const [showPostModal, setShowPostModal] = useState(false);
const [postTitle, setPostTitle] = useState('');
const [postContent, setPostContent] = useState('');
const [selectedImage, setSelectedImage] = useState(null);
const [helpPosts, setHelpPosts] = useState([]);
const [helpLoading, setHelpLoading] = useState(false);
const [helpError, setHelpError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [likedPosts,setLikedPosts] = useState({});
// 添加状态
const [torrentPosts, setTorrentPosts] = useState([]);
const [torrentLoading, setTorrentLoading] = useState(false);
const [torrentError, setTorrentError] = useState(null);
const [filteredResources, setFilteredResources] = useState(torrentPosts);
const [isAdmin, setIsAdmin] = useState(false);
const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
// 从location.state中初始化状态
const handleTabChange = (tabName) => {
navigate(`/dashboard/${tabName}`, {
state: {
savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
activeTab: tabName // 可选,如果其他组件依赖这个 state
}
});
};
//公告区
const [announcements] = useState([
{
id: 1,
title: '系统维护与更新',
content: '2023-10-15 02:00-06:00将进行系统维护升级,期间网站将无法访问。本次更新包含:\n1. 数据库服务器迁移\n2. 安全补丁更新\n3. CDN节点优化\n\n请提前做好下载安排。',
author: '系统管理员',
date: '2023-10-10',
excerpt: '2023-10-15 02:00-06:00将进行系统维护,期间无法访问',
category: '系统'
},
{
id: 2,
title: '资源上新',
content: '最新热门电影《奥本海默》4K REMUX资源已上线,包含:\n- 4K HDR版本 (56.8GB)\n- 1080P标准版 (12.3GB)\n- 中英双语字幕\n\n欢迎下载保种!',
author: '资源组',
date: '2023-10-08',
excerpt: '最新热门电影《奥本海默》4K资源已上线',
category: '资源'
},
{
id: 3,
title: '积分规则调整',
content: '自11月1日起,上传资源积分奖励提升20%,具体规则如下:\n- 上传电影资源:每GB 10积分\n- 上传电视剧资源:每GB 8积分\n- 上传动漫资源:每GB 6积分\n\n感谢大家的支持与贡献!',
author: '管理员',
date: '2023-10-05',
excerpt: '自11月1日起,上传资源积分奖励提升20%',
category: '公告'
},
{
id: 4,
title: '违规处理公告',
content: '用户user123因发布虚假资源已被封禁,相关资源已删除。请大家遵守社区规则,维护良好的分享环境。',
author: '管理员',
date: '2023-10-03',
excerpt: '用户user123因发布虚假资源已被封禁',
category: '违规'
},
{
id: 5,
title: '节日活动',
content: '国庆期间所有资源下载积分减半,活动时间:2023年10月1日至2023年10月7日。',
author: '活动组',
date: '2023-09-30',
excerpt: '国庆期间所有资源下载积分减半',
category: '活动'
},
{
id: 6,
title: '客户端更新',
content: 'PT客户端v2.5.0已发布,修复多个BUG,新增资源搜索功能。请尽快更新到最新版本以获得更好的使用体验。',
author: '开发组',
date: '2023-09-28',
excerpt: 'PT客户端v2.5.0已发布,修复多个BUG',
category: '更新'
},
// 其他公告...
]);
const handleAnnouncementClick = (announcement, e) => {
if (!e.target.closest('.exclude-click')) {
navigate(`/announcement/${announcement.id}`, {
state: {
announcement,
returnPath: `/dashboard/${activeTab}`,
scrollPosition: window.scrollY,
activeTab
}
});
}
};
//资源区
const handleFileChange = (e) => {
setUploadData({...uploadData, file: e.target.files[0]});
};
const handleUploadSubmit = async (e) => {
e.preventDefault();
try {
setIsUploading(true);
const torrentData = {
torrentName: uploadData.name,
description: uploadData.description,
category: uploadData.type,
region: uploadData.region,
resolution: uploadData.resolution,
subtitle: uploadData.subtitle
};
await createTorrent(torrentData, uploadData.file);
// 上传成功处理
setShowUploadModal(false);
setUploadData({
name: '',
type: '',
region: '',
subtitle: '',
resolution: '',
file: null,
description: ''
});
alert('种子创建成功!');
// 刷新列表
await fetchTorrentPosts(currentPage);
} catch (error) {
console.error('创建失败:', error);
alert('创建失败: ' + (error.response?.data?.message || error.message));
} finally {
setIsUploading(false);
}
};
const handlePostSubmit = async (e) => {
e.preventDefault();
try {
const username = localStorage.getItem('username');
const response = await createPost(
postTitle,
postContent,
username,
selectedImage
);
if (response.data.code === 200) {
// 刷新帖子列表
await fetchHelpPosts(currentPage);
// 重置表单
setShowPostModal(false);
setPostTitle('');
setPostContent('');
setSelectedImage(null);
} else {
setHelpError(response.data.message || '发帖失败');
}
} catch (err) {
setHelpError(err.message || '发帖失败');
}
};
// 获取Torrent帖子列表
const fetchTorrentPosts = async (page = 1) => {
setTorrentLoading(true);
try {
const response = await getTorrents(page);
setTorrentPosts(response.data.data.records);
setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
setCurrentPage(page);
} catch (err) {
setTorrentError(err.message);
} finally {
setTorrentLoading(false);
}
};
// 在useEffect中调用
useEffect(() => {
if (activeTab === 'share') {
fetchTorrentPosts();
}
}, [activeTab]);
const handleImageUpload = (e) => {
setSelectedImage(e.target.files[0]);
};
const fetchHelpPosts = async (page = 1) => {
setHelpLoading(true);
try {
const response = await getPosts(page);
if (response.data.code === 200) {
const postsWithCounts = await Promise.all(
response.data.data.records.map(async (post) => {
try {
const detailResponse = await getPostDetail(post.id);
if (detailResponse.data.code === 200) {
return {
...post,
replyCount: detailResponse.data.data.post.replyCount || 0,
isLiked: false // 根据需要添加其他字段
};
}
return post; // 如果获取详情失败,返回原始帖子数据
} catch (err) {
console.error(`获取帖子${post.id}详情失败:`, err);
return post;
}
})
);
setHelpPosts(postsWithCounts);
setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
setCurrentPage(page);
} else {
setHelpError(response.data.message || '获取求助帖失败');
}
} catch (err) {
setHelpError(err.message || '获取求助帖失败');
} finally {
setHelpLoading(false);
}
};
useEffect(() => {
if (activeTab === 'help') {
fetchHelpPosts(currentPage);
}
}, [activeTab, currentPage]); // 添加 currentPage 作为依赖
// 分类维度配置
const filterCategories = {
type: {
label: '类型',
options: {
'all': '全部',
'电影': '电影',
'电视剧': '电视剧',
'动漫': '动漫',
'综艺': '综艺'
}
},
subtitle: {
label: '字幕',
options: {
'all': '全部',
'yes': '有字幕',
'no': '无字幕'
}
},
region: {
label: '地区',
options: {
'all': '全部',
'cn': '大陆',
'us': '欧美',
'jp': '日本'
}
}
};
const [selectedFilters, setSelectedFilters] = useState(
location.state?.savedFilters ||
Object.keys(filterCategories).reduce((acc, category) => {
acc[category] = 'all';
return acc;
}, {})
);
// 处理筛选条件变更
const handleFilterSelect = (category, value) => {
setSelectedFilters(prev => ({
...prev,
[category]: prev[category] === value ? null : value // 点击已选中的则取消
}));
};
//应用筛选条件
const applyFilters = () => {
const result = torrentPosts.filter(resource => {
return Object.entries(selectedFilters).every(([category, selectedValue]) => {
if (selectedValue === 'all') return true;
if (category === 'subtitle') {
return resource.subtitle === (selectedValue === 'yes');
}
return resource[category] === selectedValue;
});
});
setFilteredResources(result);
};
// 恢复滚动位置
useEffect(() => {
if (location.state?.scrollPosition) {
window.scrollTo(0, location.state.scrollPosition);
}
}, [location.state]);
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/login');
return;
}
/* 保留但注释掉实际的用户信息获取
    const fetchUserInfo = async () => {
      try {
        const response = await getUserInfo(token);
        if (response.data.code === 200) {
          setUserInfo(response.data.data);
        } else {
          setError('获取用户信息失败');
        }
      } catch (err) {
        setError('获取用户信息失败');
      } finally {
        setLoading(false);
      }
    };
    fetchUserInfo();
    */
// 模拟用户信息
setUserInfo({
name: localStorage.getItem('username') || '演示用户', // 确保这里读取的是最新值
avatar: 'https://via.placeholder.com/40',
isAdmin: true
});
setLoading(false);
}, [navigate]);
// 轮播图自动切换效果
useEffect(() => {
if (activeTab === 'announcement') {
const timer = setInterval(() => {
setCurrentSlide(prev => (prev + 1) % 3); // 3张轮播图循环
}, 3000);
return () => clearInterval(timer);
}
}, [activeTab]);
useEffect(() => {
if (activeTab === 'help') {
fetchHelpPosts();
}
}, [activeTab]);
const renderContent = () => {
switch (activeTab) {
case 'announcement':
return (
<div className="content-area" data-testid="announcement-section">
{/* 轮播图区域 */}
<div className="carousel-container">
<div className={`carousel-slide ${currentSlide === 0 ? 'active' : ''}`}>
<div className="carousel-image gray-bg">促销活动1</div>
</div>
<div className={`carousel-slide ${currentSlide === 1 ? 'active' : ''}`}>
<div className="carousel-image gray-bg">促销活动2</div>
</div>
<div className={`carousel-slide ${currentSlide === 2 ? 'active' : ''}`}>
<div className="carousel-image gray-bg">促销活动3</div>
</div>
<div className="carousel-dots">
<span className={`dot ${currentSlide === 0 ? 'active' : ''}`}></span>
<span className={`dot ${currentSlide === 1 ? 'active' : ''}`}></span>
<span className={`dot ${currentSlide === 2 ? 'active' : ''}`}></span>
</div>
</div>
{/* 公告区块区域 */}
<div className="announcement-grid">
{announcements.map(announcement => (
<div
key={announcement.id}
className="announcement-card"
onClick={(e) => handleAnnouncementClick(announcement, e)}
>
<h3>{announcement.title}</h3>
<p>{announcement.excerpt}</p>
<div className="announcement-footer exclude-click">
<span>{announcement.author}</span>
<span>{announcement.date}</span>
</div>
</div>
))}
</div>
</div>
);
case 'share':
return (
<div className="content-area" data-testid="share-section">
{/* 上传按钮 - 添加在筛选区上方 */}
<div className="upload-header">
<button
className="upload-btn"
onClick={() => setShowUploadModal(true)}
>
上传种子
</button>
</div>
{/* 分类筛选区 */}
<div className="filter-section">
{Object.entries(filterCategories).map(([category, config]) => (
<div key={category} className="filter-group">
<h4>{config.label}:</h4>
<div className="filter-options">
{Object.entries(config.options).map(([value, label]) => (
<button
key={value}
className={`filter-btn ${
selectedFilters[category] === value ? 'active' : ''
}`}
onClick={() => handleFilterSelect(category, value)}
>
{label}
</button>
))}
</div>
</div>
))}
<button
className="confirm-btn"
onClick={applyFilters}
>
确认筛选
</button>
</div>
{/* 上传弹窗 */}
{showUploadModal && (
<div className="modal-overlay">
<div className="upload-modal">
<h3>上传新种子</h3>
<button
className="close-btn"
onClick={() => setShowUploadModal(false)}
>
×
</button>
<form onSubmit={handleUploadSubmit}>
<div className="form-group">
<label>种子名称</label>
<input
type="text"
value={uploadData.name}
onChange={(e) => setUploadData({...uploadData, name: e.target.value})}
required
/>
</div>
<div className="form-group">
<label>资源类型</label>
<select
value={uploadData.type}
onChange={(e) => setUploadData({...uploadData, type: e.target.value})}
required
>
<option value="">请选择</option>
<option value="电影">电影</option>
<option value="电视剧">电视剧</option>
<option value="动漫">动漫</option>
<option value="综艺">综艺</option>
<option value="音乐">音乐</option>
</select>
</div>
{/* 新增地区输入框 */}
<div className="form-group">
<label>地区</label>
<input
type="text"
value={uploadData.region || ''}
onChange={(e) => setUploadData({...uploadData, region: e.target.value})}
placeholder="例如: 美国, 中国, 日本等"
required
/>
</div>
{/* 添加分辨率下拉框 */}
<div className="form-group">
<label>分辨率</label>
<select
value={uploadData.resolution || ''}
onChange={(e) => setUploadData({
...uploadData,
resolution: e.target.value
})}
required
>
<option value="">请选择</option>
<option value="4K">4K</option>
<option value="2K">2K</option>
<option value="1080P">1080P</option>
<option value="720P">720P</option>
<option value="SD">SD</option>
<option value="无损音源">无损音源</option>
<option value="杜比全景声">杜比全景声</option>
<option value="其他">其他</option>
</select>
</div>
{/* 新增字幕语言下拉框 */}
<div className="form-group">
<label>字幕语言</label>
<select
value={uploadData.subtitle || ''}
onChange={(e) => setUploadData({
...uploadData,
subtitle: e.target.value
})}
required
>
<option value="">请选择</option>
<option value="无需字幕">无需字幕</option>
<option value="暂无字幕">暂无字幕</option>
<option value="自带中文字幕">自带中文字幕</option>
<option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>
<option value="附件中文字幕">附件中文字幕</option>
<option value="附件双语字幕">附件双语字幕</option>
</select>
</div>
<div className="form-group">
<label>种子文件</label>
<input
type="file"
accept=".torrent"
onChange={handleFileChange}
/>
</div>
<div className="form-group">
<label>简介</label>
<textarea
value={uploadData.description}
onChange={(e) => setUploadData({
...uploadData,
description: e.target.value
})}
/>
</div>
<div className="form-actions">
<button
type="button"
className="cancel-btn"
onClick={() => setShowUploadModal(false)}
>
取消
</button>
<button
type="submit"
className="confirm-btn"
disabled={isUploading}
>
{isUploading ? '上传中...' : '确认上传'}
</button>
</div>
</form>
</div>
</div>
)}
<div className="resource-list">
{torrentPosts.map(torrent => (
<div
key={torrent.id}
className="resource-item"
onClick={() => navigate(`/torrent/${torrent.id}`)}
>
<div className="resource-poster">
<div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>
</div>
<div className="resource-info">
<h3 className="resource-title">{torrent.torrentName}</h3>
<p className="resource-meta">
{torrent.resolution} | {torrent.region} | {torrent.category}
</p>
<p className="resource-subtitle">字幕: {torrent.subtitle}</p>
</div>
<div className="resource-stats">
<span className="stat">{torrent.size}</span>
<span className="stat">发布者: {torrent.username}</span>
</div>
<button
className="download-btn"
onClick={(e) => {
e.stopPropagation();
// 下载逻辑
}}
>
立即下载
</button>
</div>
))}
</div>
{/* 分页控件 */}
<div className="pagination">
<button
onClick={() => fetchTorrentPosts(currentPage - 1)}
disabled={currentPage === 1}
>
上一页
</button>
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => fetchTorrentPosts(page)}
className={currentPage === page ? 'active' : ''}
>
{page}
</button>
))}
<button
onClick={() => fetchTorrentPosts(currentPage + 1)}
disabled={currentPage === totalPages}
>
下一页
</button>
</div>
</div>
);
// 在Dashboard.jsx的renderContent函数中修改case 'request'部分
case 'request':
return (
<div className="content-area" data-testid="request-section">
{/* 求种区帖子列表 */}
<div className="request-list">
{[
{
id: 1,
title: '求《药屋少女的呢喃》第二季全集',
content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的',
author: '动漫爱好者',
authorAvatar: 'https://via.placeholder.com/40',
date: '2023-10-15',
likeCount: 24,
commentCount: 8
},
{
id: 2,
title: '求《奥本海默》IMAX版',
content: '最好是原盘或者高码率的版本,谢谢各位大佬',
author: '电影收藏家',
authorAvatar: 'https://via.placeholder.com/40',
date: '2023-10-14',
likeCount: 15,
commentCount: 5
}
].map(post => (
<div
key={post.id}
className="request-post"
onClick={() => navigate(`/request/${post.id}`)}
>
<div className="post-header">
<img src={post.authorAvatar} alt={post.author} className="post-avatar"/>
<div className="post-author">{post.author}</div>
<div className="post-date">{post.date}</div>
</div>
<h3 className="post-title">{post.title}</h3>
<p className="post-content">{post.content}</p>
<div className="post-stats">
<span className="post-likes">👍 {post.likeCount}</span>
<span className="post-comments">💬 {post.commentCount}</span>
</div>
</div>
))}
</div>
</div>
);
// 在Dashboard.jsx的renderContent函数中修改case 'help'部分
case 'help':
return (
<div className="content-area" data-testid="help-section">
{/* 新增发帖按钮 */}
<div className="post-header">
<button
className="create-post-btn"
onClick={() => setShowPostModal(true)}
>
发帖求助
</button>
</div>
{/* 加载状态和错误提示 */}
{helpLoading && <div className="loading">加载中...</div>}
{helpError && <div className="error">{helpError}</div>}
{/* 求助区帖子列表 */}
<div className="help-list">
{helpPosts.map(post => (
<div
key={post.id}
className={`help-post ${post.isSolved ? 'solved' : ''}`}
onClick={() => navigate(`/help/${post.id}`)}
>
<div className="post-header">
<img
src={post.authorAvatar || 'https://via.placeholder.com/40'}
alt={post.authorId}
className="post-avatar"
/>
<div className="post-author">{post.authorId}</div>
<div className="post-date">
{new Date(post.createTime).toLocaleDateString()}
</div>
{post.isSolved && <span className="solved-badge">已解决</span>}
</div>
<h3 className="post-title">{post.title}</h3>
<p className="post-content">{post.content}</p>
<div className="post-stats">
<span className="post-likes">👍 {post.likeCount || 0}</span>
<span className="post-comments">💬 {post.replyCount || 0}</span>
</div>
</div>
))}
</div>
{/* 在帖子列表后添加分页控件 */}
<div className="pagination">
<button
onClick={() => fetchHelpPosts(currentPage - 1)}
disabled={currentPage === 1}
>
上一页
</button>
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => fetchHelpPosts(page)}
className={currentPage === page ? 'active' : ''}
>
{page}
</button>
))}
<button
onClick={() => fetchHelpPosts(currentPage + 1)}
disabled={currentPage === totalPages}
>
下一页
</button>
</div>
{/* 新增发帖弹窗 */}
{showPostModal && (
<div className="post-modal-overlay">
<div className="post-modal">
<h3>发布求助帖</h3>
<button
className="modal-close-btn"
onClick={() => setShowPostModal(false)}
>
×
</button>
<form onSubmit={handlePostSubmit}>
<div className="form-group">
<label>帖子标题</label>
<input
type="text"
value={postTitle}
onChange={(e) => setPostTitle(e.target.value)}
placeholder="请输入标题"
required
/>
</div>
<div className="form-group">
<label>帖子内容</label>
<textarea
value={postContent}
onChange={(e) => setPostContent(e.target.value)}
placeholder="详细描述你的问题"
required
/>
</div>
<div className="form-group">
<label>上传图片</label>
<div className="upload-image-btn">
<input
type="file"
id="image-upload"
accept="image/*"
onChange={handleImageUpload}
style={{display: 'none'}}
/>
<label htmlFor="image-upload">
{selectedImage ? '已选择图片' : '选择图片'}
</label>
{selectedImage && (
<span className="image-name">{selectedImage.name}</span>
)}
</div>
</div>
<div className="form-actions">
<button
type="button"
className="cancel-btn"
onClick={() => setShowPostModal(false)}
>
取消
</button>
<button
type="submit"
className="submit-btn"
>
确认发帖
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
default:
return <div className="content-area" data-testid="default-section">公告区内容</div>;
}
};
if (loading) return <div className="loading">加载中...</div>;
if (error) return <div className="error">{error}</div>;
return (
<div className="dashboard-container" data-testid="dashboard-container">
{/* 顶部栏 */}
<div className="top-bar" data-testid="top-bar">
{/* 搜索框 */}
<div className="search-container">
<input
type="text"
placeholder="搜索种子、用户..."
className="search-input"
/>
<button className="search-button">搜索</button>
</div>
<div className="user-actions">
{/* 新增管理员按钮 - 只有管理员可见 */}
{userInfo?.isAdmin && (
<button
className="admin-center-button"
onClick={() => navigate('/administer')}
>
管理员中心
</button>
)}
<div className="user-info" data-testid="user-info">
<img
src={userInfo?.avatar || 'https://via.placeholder.com/40'}
alt="用户头像"
className="user-avatar"
onClick={() => navigate('/personal')}
style={{cursor: 'pointer'}}
/>
<span className="username">{userInfo?.name || '用户'}</span>
<button onClick={onLogout} className="logout-button">退出</button>
</div>
</div>
</div>
{/* 导航栏 */}
{/* handleTabchange函数替换了原本的setactivetab函数 */}
<div className="nav-tabs">
<button
className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}
onClick={() => handleTabChange('announcement')}
>
公告区
</button>
<button
className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}
onClick={() => handleTabChange('share')}
>
分享区
</button>
<button
className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
onClick={() => handleTabChange('request')}
>
求种区
</button>
<button
className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}
onClick={() => handleTabChange('help')}
>
求助区
</button>
</div>
{/* 内容区 */}
{renderContent()}
</div>
);
};
export default Dashboard;