blob: 61111f3b64b6238da9c10f3ef9a685a7ef9101d5 [file] [log] [blame]
import React, {useEffect, useState} from 'react';
import {useNavigate, useLocation, useParams} from 'react-router-dom';
import {createTorrent, getTorrents, downloadTorrent, getDownloadProgress, deleteTorrent, searchTorrents} from '../api/torrent';
import './Dashboard.css';
import {createHelpPost, getHelpPosts, getHelpPostDetail, searchHelpPosts} from '../api/helpPost';
import {createRequestPost, getRequestPosts, getRequestPostDetail, searchRequestPosts} from '../api/requestPost';
import { message } from 'antd'; // 用于显示提示消息
import { getAnnouncements,getLatestAnnouncements,getAnnouncementDetail } from '../api/announcement';
import { getAllDiscounts } from '../api/administer';
import { getUserInfo, isAdmin } from '../api/auth';
import { api } from '../api/auth';
import { getRecommendations, markRecommendationShown, markRecommendationClicked } from '../api/recommend';
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 [announcements, setAnnouncements] = useState([]);
const [carouselDiscounts, setCarouselDiscounts] = useState([]);
const [requestLoading, setRequestLoading] = useState(false);
const [requestError, setRequestError] = useState(null);
const [requestPosts, setRequestPosts] = 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 [showDownloadModal, setShowDownloadModal] = useState(false);
const [selectedTorrent, setSelectedTorrent] = useState(null);
const [downloadProgress, setDownloadProgress] = useState(0);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadPath, setDownloadPath] = useState('D:/studies/ptPlatform/torrent');
// 新增搜索状态
const [announcementSearch, setAnnouncementSearch] = useState('');
const [shareSearch, setShareSearch] = useState('');
const [requestSearch, setRequestSearch] = useState('');
const [helpSearch, setHelpSearch] = useState('');
const [recommendations, setRecommendations] = useState([]);
const [recommendLoading, setRecommendLoading] = useState(false);
const [recommendError, setRecommendError] = useState(null);
const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
// 从location.state中初始化状态
const handleTabChange = (tabName) => {
navigate(`/dashboard/${tabName}`, {
state: {
savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
activeTab: tabName // 可选,如果其他组件依赖这个 state
}
});
};
//公告区
// 添加获取公告的方法
const fetchAnnouncements = async () => {
try {
const response = await getLatestAnnouncements();
setAnnouncements(response.data.data.announcements || []);
} catch (error) {
console.error('获取公告失败:', error);
}
};
useEffect(() => {
if (activeTab === 'announcement') {
fetchAnnouncements();
fetchDiscountsForCarousel();
fetchRecommendations();
}
}, [activeTab]);
const fetchDiscountsForCarousel = async () => {
try {
const all = await getAllDiscounts();
console.log("返回的折扣数据:", all);
const now = new Date();
// ⚠️ 使用 Date.parse 确保兼容 ISO 格式
const ongoing = all.filter(d =>
Date.parse(d.startTime) <= now.getTime() && Date.parse(d.endTime) >= now.getTime()
);
const upcoming = all
.filter(d => Date.parse(d.startTime) > now.getTime())
.sort((a, b) => Date.parse(a.startTime) - Date.parse(b.startTime));
const selected = [...ongoing.slice(0, 3)];
while (selected.length < 3 && upcoming.length > 0) {
selected.push(upcoming.shift());
}
setCarouselDiscounts(selected);
} catch (e) {
console.error("获取折扣失败:", e);
}
};
// 修改handleAnnouncementClick函数中的state传递,移除不必要的字段
const handleAnnouncementClick = (announcement, e) => {
if (!e.target.closest('.exclude-click')) {
navigate(`/announcement/${announcement.id}`, {
state: {
announcement,
returnPath: `/dashboard/${activeTab}`,
scrollPosition: window.scrollY,
activeTab
}
});
}
};
const handleRecommendClick = async (torrent) => {
try {
// 标记为已点击
await markRecommendationClicked(torrent.id);
// 导航到种子详情页
navigate(`/torrent/${torrent.id}`, {
state: {
fromTab: 'announcement',
scrollPosition: window.scrollY
}
});
} catch (error) {
console.error('标记推荐点击失败:', error);
}
};
   // 公告区搜索处理
    const handleSearchAnnouncement = (e) => {
        setAnnouncementSearch(e.target.value);
    };
    // 修改后的搜索函数
    const handleSearchShare = async () => {
        try {
            setTorrentLoading(true);
            const response = await searchTorrents(shareSearch, 1);
            if (response.data.code === 200) {
                setTorrentPosts(response.data.data.records);
                const total = response.data.data.total;
                setTotalPages(Math.ceil(total / 5));
                setCurrentPage(1);
            } else {
                setTorrentError(response.data.message || '搜索失败');
            }
        } catch (err) {
            setTorrentError(err.message || '搜索失败');
        } finally {
            setTorrentLoading(false);
        }
    };
    const handleResetShareSearch = async () => {
        setShareSearch('');
        setSelectedFilters(
            Object.keys(filterCategories).reduce((acc, category) => {
                acc[category] = 'all';
                return acc;
            }, {})
        );
        await fetchTorrentPosts(1, true);
    };
    // 添加搜索函数
    const handleSearchRequest = async () => {
        try {
        setRequestLoading(true);
        const response = await searchRequestPosts(requestSearch, currentPage);
        if (response.data.code === 200) {
            const postsWithCounts = await Promise.all(
            response.data.data.records.map(async (post) => {
                try {
                const detailResponse = await getRequestPostDetail(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;
                }
            })
            );
            setRequestPosts(postsWithCounts);
            setTotalPages(Math.ceil(response.data.data.total / 5));
        } else {
            setRequestError(response.data.message || '搜索失败');
        }
        } catch (err) {
        setRequestError(err.message || '搜索失败');
        } finally {
        setRequestLoading(false);
        }
    };
   
    // 添加重置搜索函数
    const handleResetRequestSearch = async () => {
        setRequestSearch('');
await fetchRequestPosts(1); // 重置到第一页
};
   // 添加搜索函数
    const handleSearchHelp = async () => {
        try {
        setHelpLoading(true);
        const response = await searchHelpPosts(helpSearch, currentPage);
        if (response.data.code === 200) {
            const postsWithCounts = await Promise.all(
            response.data.data.records.map(async (post) => {
                try {
                const detailResponse = await getHelpPostDetail(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));
        } else {
            setHelpError(response.data.message || '搜索失败');
        }
        } catch (err) {
        setHelpError(err.message || '搜索失败');
        } finally {
        setHelpLoading(false);
        }
    };
   
    // 添加重置搜索函数
    const handleResetHelpSearch = async () => {
        setHelpSearch('');
await fetchHelpPosts(1); // 重置到第一页
};
//资源区
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(uploadData.file, torrentData, (progress) => {
console.log(`上传进度: ${progress}%`);
// 这里可以添加进度条更新逻辑
});
// 上传成功处理
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 handleDownloadClick = (torrent, e) => {
e.stopPropagation();
setSelectedTorrent(torrent);
setShowDownloadModal(true);
};
// 执行下载
const handleDownload = async () => {
if (!selectedTorrent || !downloadPath) return;
setIsDownloading(true);
setDownloadProgress(0);
try {
// 标准化路径
const cleanPath = downloadPath
.replace(/\\/g, '/') // 统一使用正斜杠
.replace(/\/+/g, '/') // 去除多余斜杠
.trim();
// 确保路径以斜杠结尾
const finalPath = cleanPath.endsWith('/') ? cleanPath : cleanPath + '/';
// 发起下载请求
await downloadTorrent(selectedTorrent.id, finalPath);
// 开始轮询进度
const interval = setInterval(async () => {
try {
const res = await getDownloadProgress();
const progresses = res.data.progresses;
if (progresses) {
// 使用完整的 torrent 文件路径作为键
const torrentPath = selectedTorrent.filePath.replace(/\\/g, '/');
const torrentHash = selectedTorrent.hash;
// 查找匹配的进度
let foundProgress = null;
for (const [key, value] of Object.entries(progresses)) {
const normalizedKey = key.replace(/\\/g, '/');
if (normalizedKey.includes(selectedTorrent.hash) ||
normalizedKey.includes(selectedTorrent.torrentName)) {
foundProgress = value;
break;
}
}
if (foundProgress !== null) {
const newProgress = Math.round(foundProgress * 100);
setDownloadProgress(newProgress);
// 检查是否下载完成
if (newProgress >= 100) {
clearInterval(interval);
setIsDownloading(false);
message.success('下载完成!');
setTimeout(() => setShowDownloadModal(false), 2000);
}
} else {
console.log('当前下载进度:', progresses); // 添加日志
}
}
} catch (error) {
console.error('获取进度失败:', error);
// 如果获取进度失败但文件已存在,也视为完成
const filePath = `${finalPath}${selectedTorrent.torrentName || 'downloaded_file'}`;
try {
const exists = await checkFileExists(filePath);
if (exists) {
clearInterval(interval);
setDownloadProgress(100);
setIsDownloading(false);
message.success('下载完成!');
setTimeout(() => setShowDownloadModal(false), 2000);
}
} catch (e) {
console.error('文件检查失败:', e);
}
}
}, 2000);
return () => clearInterval(interval);
} catch (error) {
setIsDownloading(false);
if (error.response && error.response.status === 409) {
message.error('分享率不足,无法下载此资源');
} else {
message.error('下载失败: ' + (error.message || '未知错误'));
}
}
};
const checkFileExists = async (filePath) => {
try {
// 这里需要根据您的实际环境实现文件存在性检查
// 如果是Electron应用,可以使用Node.js的fs模块
// 如果是纯前端,可能需要通过API请求后端检查
return true; // 暂时返回true,实际实现需要修改
} catch (e) {
console.error('检查文件存在性失败:', e);
return false;
}
};
const handleDeleteTorrent = async (torrentId, e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发资源项的点击事件
try {
// 确认删除
if (!window.confirm('确定要删除这个种子吗?此操作不可撤销!')) {
return;
}
// 调用删除API
await deleteTorrent(torrentId);
// 删除成功后刷新列表
message.success('种子删除成功');
await fetchTorrentPosts(currentPage);
} catch (error) {
console.error('删除种子失败:', error);
message.error('删除种子失败: ' + (error.response?.data?.message || error.message));
}
};
const handleRequestPostSubmit = async (e) => {
e.preventDefault();
try {
const username = localStorage.getItem('username');
const response = await createRequestPost(
postTitle,
postContent,
username,
selectedImage
);
if (response.data.code === 200) {
// 刷新帖子列表
await fetchRequestPosts(currentPage);
// 重置表单
setShowPostModal(false);
setPostTitle('');
setPostContent('');
setSelectedImage(null);
} else {
setHelpError(response.data.message || '发帖失败');
}
} catch (err) {
setHelpError(err.message || '发帖失败');
}
};
const handleHelpPostSubmit = async (e) => {
e.preventDefault();
try {
const username = localStorage.getItem('username');
const response = await createHelpPost(
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 || '发帖失败');
}
};
const fetchTorrentPosts = async (page = 1, isReset = false) => {
setTorrentLoading(true);
try {
const params = {
page,
size: 5
};
// 如果有筛选条件且不是重置操作
if (!isReset && Object.values(selectedFilters).some(v => v !== 'all')) {
if (selectedFilters.type !== 'all') params.category = selectedFilters.type;
if (selectedFilters.subtitle !== 'all') params.subtitle = selectedFilters.subtitle;
if (selectedFilters.region !== 'all') params.region = selectedFilters.region;
if (selectedFilters.resolution !== 'all') params.resolution = selectedFilters.resolution;
}
const response = (shareSearch && !isReset)
? await searchTorrents(shareSearch, page)
: await api.get('/torrent', { params });
if (response.data.code === 200) {
setTorrentPosts(response.data.data.records);
const total = response.data.data.total;
setTotalPages(Math.ceil(total / 5));
setCurrentPage(page);
} else {
setTorrentError(response.data.message);
}
} catch (err) {
setTorrentError(err.message);
} finally {
setTorrentLoading(false);
}
};
// 在useEffect中调用
useEffect(() => {
if (activeTab === 'share') {
fetchTorrentPosts();
}
}, [activeTab]);
const fetchRequestPosts = async (page = 1) => {
setRequestLoading(true);
try {
const response = await getRequestPosts(page);
if (response.data.code === 200) {
const postsWithCounts = await Promise.all(
response.data.data.records.map(async (post) => {
try {
const detailResponse = await getRequestPostDetail(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;
}
})
);
setRequestPosts(postsWithCounts);
setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
setCurrentPage(page);
} else {
setRequestError(response.data.message || '获取求助帖失败');
}
} catch (err) {
setRequestError(err.message || '获取求助帖失败');
} finally {
setRequestLoading(false);
}
};
const handleImageUpload = (e) => {
setSelectedImage(e.target.files[0]);
};
const fetchHelpPosts = async (page = 1) => {
setHelpLoading(true);
try {
const response = await getHelpPosts(page);
if (response.data.code === 200) {
const postsWithCounts = await Promise.all(
response.data.data.records.map(async (post) => {
try {
const detailResponse = await getHelpPostDetail(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);
}
};
const fetchRecommendations = async () => {
try {
setRecommendLoading(true);
const response = await getRecommendations(5);
if (response.code === 200) {
setRecommendations(response.data || []);
// 标记这些推荐为已显示
response.data.forEach(torrent => {
markRecommendationShown(torrent.id);
});
}
} catch (error) {
setRecommendError(error.message || '获取推荐失败');
} finally {
setRecommendLoading(false);
}
};
useEffect(() => {
if (activeTab === 'request') {
fetchRequestPosts(currentPage);
}
}, [activeTab, currentPage]); // 添加 currentPage 作为依赖
// 分类维度配置
const filterCategories = {
type: {
label: '类型',
options: {
'all': '全部',
'电影': '电影',
'电视剧': '电视剧',
'动漫': '动漫',
'综艺': '综艺',
'音乐': '音乐',
'其他': '其他'
}
},
subtitle: {
label: '字幕',
options: {
'all': '全部',
'无需字幕': '无需字幕',
'暂无字幕': '暂无字幕',
'自带中文字幕': '自带中文字幕',
'自带双语字幕(含中文)': '自带双语字幕(含中文)',
'附件中文字幕': '附件中文字幕',
'附件双语字幕': '附件双语字幕'
}
},
region: {
label: '地区',
options: {
'all': '全部',
'中国': '中国',
'英国': '英国',
'美国': '美国',
'日本': '日本',
'韩国': '韩国',
'其他': '其他'
}
},
resolution: {
label: '分辨率',
options: {
'all': '全部',
'4K': '4K',
'2K': '2K',
'1080P': '1080P',
'720P': '720P',
'SD': 'SD',
'无损音源': '无损音源',
'杜比全景声': '杜比全景声',
'其他': '其他'
}
}
};
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 ? 'all' : value
}));
};
// 应用筛选条件
const applyFilters = async () => {
try {
setTorrentLoading(true);
// 构建查询参数
const params = {
page: 1, // 从第一页开始
size: 5
};
// 添加筛选条件
if (selectedFilters.type !== 'all') {
params.category = selectedFilters.type;
}
if (selectedFilters.subtitle !== 'all') {
params.subtitle = selectedFilters.subtitle;
}
if (selectedFilters.region !== 'all') {
params.region = selectedFilters.region;
}
if (selectedFilters.resolution !== 'all') {
params.resolution = selectedFilters.resolution;
}
// 调用API获取筛选结果
const response = await api.get('/torrent', { params });
if (response.data.code === 200) {
setTorrentPosts(response.data.data.records);
setTotalPages(Math.ceil(response.data.data.total / 5));
setCurrentPage(1);
} else {
setTorrentError(response.data.message || '筛选失败');
}
} catch (err) {
setTorrentError(err.message || '筛选失败');
} finally {
setTorrentLoading(false);
}
};
// 恢复滚动位置
useEffect(() => {
if (location.state?.scrollPosition) {
window.scrollTo(0, location.state.scrollPosition);
}
}, [location.state]);
// 在Dashboard.jsx中修改useEffect
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/login');
return;
}
const fetchUserInfo = async () => {
try {
setLoading(true);
const backendData = await getUserInfo(); // 调用修改后的方法
console.log('后端返回的用户数据:', backendData); // 调试用
setUserInfo({
name: backendData.username || '演示用户',
avatar: 'https://via.placeholder.com/40',
isAdmin: backendData.authority === 'ADMIN' // 检查 authority 是否为 "ADMIN"
});
} catch (error) {
console.error('获取用户信息失败:', error);
setError('获取用户信息失败');
} finally {
setLoading(false);
}
};
fetchUserInfo();
}, [navigate]);
useEffect(() => {
if (activeTab === 'announcement') {
const timer = setInterval(() => {
setCurrentSlide(prev => {
const count = carouselDiscounts.length || 1;
return (prev + 1) % count;
});
}, 3000);
return () => clearInterval(timer);
}
}, [activeTab]);
useEffect(() => {
if (activeTab === 'help') {
fetchHelpPosts();
}
}, [activeTab, currentPage]); // 添加 currentPage 作为依赖
const renderContent = () => {
switch (activeTab) {
case 'announcement':
return (
<div className="content-area" data-testid="announcement-section">
<div className="section-search-container">
<input
type="text"
placeholder="搜索公告..."
value={announcementSearch}
onChange={(e) => setAnnouncementSearch(e.target.value)}
className="section-search-input"
onKeyPress={(e) => e.key === 'Enter' && handleSearchAnnouncement()}
/>
<button
className="search-button"
onClick={handleSearchAnnouncement}
>
搜索
</button>
</div>
{/* 轮播图区域 */}
<div className="carousel-container">
{carouselDiscounts.length === 0 ? (
<div className="carousel-slide active">
<div className="carousel-image gray-bg">暂无折扣活动</div>
</div>
) : (
carouselDiscounts.map((discount, index) => (
<div key={index} className={`carousel-slide ${currentSlide === index ? 'active' : ''}`}>
<div className="carousel-image gray-bg">
<h3>{discount.type}</h3>
<p>{discount.name}</p>
<p>{new Date(discount.startTime).toLocaleDateString()} ~ {new Date(discount.endTime).toLocaleDateString()}</p>
</div>
</div>
))
)}
<div className="carousel-dots">
{carouselDiscounts.map((_, index) => (
<span key={index} className={`dot ${currentSlide === index ? '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.content.substring(0, 100)}...</p>
<div className="announcement-footer exclude-click">
<span>{announcement.date}</span>
</div>
</div>
))}
</div>
{/* 新增的为你推荐区域 */}
<div className="recommend-section">
<h2 className="section-title">为你推荐</h2>
{recommendLoading && <div className="loading">加载推荐中...</div>}
{recommendError && <div className="error">{recommendError}</div>}
<div className="recommend-grid">
{recommendations.map(torrent => (
<div
key={torrent.id}
className="recommend-card"
onClick={() => handleRecommendClick(torrent)}
>
<div className="card-poster">
<div className="poster-image gray-bg">
{torrent.torrentName.charAt(0)}
</div>
</div>
<div className="card-info">
<h3 className="card-title">{torrent.torrentName}</h3>
<p className="card-meta">
{torrent.resolution} | {torrent.region} | {torrent.category}
</p>
<p className="card-subtitle">字幕: {torrent.subtitle}</p>
</div>
<div className="card-stats">
<span className="stat">👍 {torrent.likeCount || 0}</span>
</div>
</div>
))}
{!recommendLoading && recommendations.length === 0 && (
<div className="no-recommendations">
暂无推荐,请先下载一些资源以获取个性化推荐
</div>
)}
</div>
</div>
</div>
);
case 'share':
return (
<div className="content-area" data-testid="share-section">
{/* 分享区搜索框 */}
<div className="section-search-container">
<input
type="text"
placeholder="搜索资源..."
value={shareSearch}
onChange={(e) => setShareSearch(e.target.value)}
className="section-search-input"
onKeyPress={(e) => e.key === 'Enter' && handleSearchShare()}
/>
<button
className="search-button"
onClick={handleSearchShare}
>
搜索
</button>
<button
className="reset-button"
onClick={handleResetShareSearch} // 使用新的重置函数
style={{marginLeft: '10px'}}
>
重置
</button>
</div>
{/* 上传按钮 - 添加在筛选区上方 */}
<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>
<option value="其他">其他</option>
</select>
</div>
{/* 修改后的地区下拉框 */}
<div className="form-group">
<label>地区</label>
<select
value={uploadData.region || ''}
onChange={(e) => setUploadData({...uploadData, region: 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>
<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) => handleDownloadClick(torrent, e)}
>
立即下载
</button>
{/* 添加删除按钮 - 只有管理员或发布者可见 */}
{(userInfo?.isAdmin || userInfo?.name === torrent.username) && (
<button
className="delete-btn"
onClick={(e) => handleDeleteTorrent(torrent.id, e)}
>
删除
</button>
)}
</div>
))}
</div>
{totalPages > 1 && (
<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="section-search-container">
<input
type="text"
placeholder="搜索求助..."
value={requestSearch}
onChange={(e) => setRequestSearch(e.target.value)}
className="section-search-input"
onKeyPress={(e) => e.key === 'Enter' && handleSearchRequest()}
/>
<button
className="search-button"
onClick={handleSearchRequest}
>
搜索
</button>
<button
className="reset-button"
onClick={handleResetRequestSearch}
style={{marginLeft: '10px'}}
>
重置
</button>
</div>
{/* 新增发帖按钮 */}
<div className="post-header">
<button
className="create-post-btn"
onClick={() => setShowPostModal(true)}
>
发帖求助
</button>
</div>
{/* 加载状态和错误提示 */}
{requestLoading && <div className="loading">加载中...</div>}
{requestError && <div className="error">{helpError}</div>}
{/* 求种区帖子列表 */}
<div className="request-list">
{requestPosts.map(post => (
<div
key={post.id}
className={`request-post ${post.isSolved ? 'solved' : ''}`}
onClick={() => navigate(`/request/${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={() => fetchRequestPosts(currentPage - 1)}
disabled={currentPage === 1}
>
上一页
</button>
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => fetchRequestPosts(page)}
className={currentPage === page ? 'active' : ''}
>
{page}
</button>
))}
<button
onClick={() => fetchRequestPosts(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={handleRequestPostSubmit}>
<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>
);
// 在Dashboard.jsx的renderContent函数中修改case 'help'部分
case 'help':
return (
<div className="content-area" data-testid="help-section">
{/* 求助区搜索框 */}
<div className="section-search-container">
<input
type="text"
placeholder="搜索求助..."
value={helpSearch}
onChange={(e) => setHelpSearch(e.target.value)}
className="section-search-input"
onKeyPress={(e) => e.key === 'Enter' && handleSearchHelp()}
/>
<button
className="search-button"
onClick={handleSearchHelp}
>
搜索
</button>
<button
className="reset-button"
onClick={handleResetHelpSearch}
style={{marginLeft: '10px'}}
>
重置
</button>
</div>
{/* 新增发帖按钮 */}
<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={handleHelpPostSubmit}>
<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="platform-name">
<h2>PT资源站</h2>
</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()}
{/* 下载模态框 - 添加在这里 */}
{showDownloadModal && selectedTorrent && (
<div className="modal-overlay">
<div className="download-modal">
<h3>下载 {selectedTorrent.torrentName}</h3>
<button
className="close-btn"
onClick={() => !isDownloading && setShowDownloadModal(false)}
disabled={isDownloading}
>
×
</button>
<div className="form-group">
<label>下载路径:</label>
<input
type="text"
value={downloadPath}
onChange={(e) => {
// 实时格式化显示
let path = e.target.value
.replace(/\t/g, '')
.replace(/\\/g, '/')
.replace(/\s+/g, ' ');
setDownloadPath(path);
}}
disabled={isDownloading}
placeholder="例如: D:/downloads/"
/>
</div>
{isDownloading && (
<div className="progress-container">
<div className="progress-bar" style={{ width: `${downloadProgress}%` }}>
{downloadProgress}%
</div>
</div>
)}
<div className="modal-actions">
<button
onClick={() => !isDownloading && setShowDownloadModal(false)}
disabled={isDownloading}
>
取消
</button>
<button
onClick={handleDownload}
disabled={isDownloading || !downloadPath}
>
{isDownloading ? '下载中...' : '开始下载'}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Dashboard;