查看种子列表用户端和管理员端界面
Change-Id: Iaa99a85c824730d993687c3af6daaeb868b220b8
diff --git a/src/components/torrentlist.jsx b/src/components/torrentlist.jsx
new file mode 100644
index 0000000..c760999
--- /dev/null
+++ b/src/components/torrentlist.jsx
@@ -0,0 +1,902 @@
+import { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+//import { Layout, Menu, Button, Radio,Input, } from 'antd';
+import { Layout, Input, Button, Radio, Spin, message, Modal, Pagination } from 'antd';
+const { Header, Content, Sider } = Layout;
+import '../filter.css';
+import '../torrentlist.css';
+import '../complain.css';
+import axios from 'axios';
+import Navbar from './Navbar';
+import {createComplain} from '../api/complain'; // 假设举报API在这个路径
+
+// 常量配置集中管理
+const FILTER_OPTIONS = {
+ // 通用选项
+ common: {
+ resolution: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ],
+ region: {
+ movie: [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ],
+ variety: [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ],
+ sports: [
+ { value: '亚洲', label: '亚洲' },
+ { value: '欧洲', label: '欧洲' },
+ { value: '美洲', label: '美洲' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ genre: {
+ movie: [
+ { value: '动作', label: '动作' },
+ { value: '喜剧', label: '喜剧' },
+ { value: '爱情', label: '爱情' },
+ { value: '科幻', label: '科幻' },
+ { value: '恐怖', label: '恐怖' },
+ { value: '冒险', label: '冒险' },
+ { value: '历史', label: '历史' },
+ { value: '悬疑', label: '悬疑' },
+ { value: '其他', label: '其他' },
+ ],
+ music: [
+ { value: '流行', label: '流行' },
+ { value: '摇滚', label: '摇滚' },
+ { value: '电子', label: '电子' },
+ { value: '古典', label: '古典' },
+ { value: '爵士', label: '爵士' },
+ { value: '民谣', label: '民谣' },
+ { value: '说唱', label: '说唱' },
+ { value: '其他', label: '其他' },
+ ],
+ anime: [
+ { value: '新番连载', label: '新番连载' },
+ { value: '剧场版', label: '剧场版' },
+ { value: 'OVA', label: 'OVA' },
+ { value: '完结动漫', label: '完结动漫' },
+ { value: '其他', label: '其他' },
+ ],
+ game: [
+ { value: '角色扮演', label: '角色扮演' },
+ { value: '射击', label: '射击' },
+ { value: '冒险', label: '冒险' },
+ { value: '策略', label: '策略' },
+ { value: '体育', label: '体育' },
+ { value: '桌面游戏', label: '桌面游戏' },
+ { value: '其他', label: '其他' },
+ ],
+ variety: [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '音乐', label: '音乐' },
+ { value: '游戏', label: '游戏' },
+ { value: '其他', label: '其他' },
+ ],
+ learning: [
+ { value: '计算机', label: '计算机' },
+ { value: '软件', label: '软件' },
+ { value: '人文', label: '人文' },
+ { value: '外语', label: '外语' },
+ { value: '理工科', label: '理工科' },
+ { value: '其他', label: '其他' },
+ ],
+ sports: [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ],
+ // 其他类型...
+ }
+ },
+
+ // 分类特定选项
+ categories: {
+ 1: { // 电影
+ name: '电影',
+ filters: [
+ { id: 'resolution', label: '分辨率', type: 'select' },
+ {
+ id: 'codec_format', label: '编码格式', type: 'select',
+ options: [
+ { value: 'H.264', label: 'H.264' },
+ { value: 'H.265', label: 'H.265' },
+ { value: 'AV1', label: 'AV1' },
+ { value: 'VC1', label: 'VC1' },
+ { value: 'X264', label: 'X264' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ { id: 'region', label: '地区', type: 'select' },
+ { id: 'genre', label: '类型', type: 'select' }
+ ]
+ },
+ 2: { // 电视剧
+ name: '剧集',
+ filters: [
+ {
+ id: 'region', label: '地区', type: 'select',
+ options: [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '游戏', label: '游戏' },
+ { value: '音乐', label: '音乐' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 3: { // 音乐
+ name: '音乐',
+ filters: [
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '专辑', label: '专辑' },
+ { value: '单曲', label: '单曲' },
+ { value: 'EP', label: 'EP' },
+ { value: '现场', label: '现场' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'style', label: '风格', type: 'select',
+ options: [
+ { value: '流行', label: '流行' },
+ { value: '摇滚', label: '摇滚' },
+ { value: '电子', label: '电子' },
+ { value: '古典', label: '古典' },
+ { value: '爵士', label: '爵士' },
+ { value: '民谣', label: '民谣' },
+ { value: '说唱', label: '说唱' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'MP3', label: 'MP3' },
+ { value: 'FLAC', label: 'FLAC' },
+ { value: 'WAV', label: 'WAV' },
+ { value: 'AAC', label: 'AAC' },
+ { value: 'OGG', label: 'OGG' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 4: { // 动漫
+ name: '动漫',
+ filters: [
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '新番连载', label: '新番连载' },
+ { value: '剧场版', label: '剧场版' },
+ { value: 'OVA', label: 'OVA' },
+ { value: '完结动漫', label: '完结动漫' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'ZIP', label: 'ZIP' },
+ { value: 'RAR', label: 'RAR' },
+ { value: '7Z', label: '7Z' },
+ { value: 'MKV', label: 'MKV' },
+ { value: 'MP4', label: 'MP4' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'resolution', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 5: { // 游戏
+ name: '游戏',
+ filters: [
+ {
+ id: 'platform', label: '平台', type: 'select',
+ options: [
+ { value: 'PC', label: 'PC' },
+ { value: 'PS5', label: 'PS5' },
+ { value: 'Xbox', label: 'Xbox' },
+ { value: 'Switch', label: 'Switch' },
+ { value: '手机', label: '手机' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '角色扮演', label: '角色扮演' },
+ { value: '射击', label: '射击' },
+ { value: '冒险', label: '冒险' },
+ { value: '策略', label: '策略' },
+ { value: '体育', label: '体育' },
+ { value: '桌面游戏', label: '桌面游戏' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'data_format', label: '数据类型', type: 'select',
+ options: [
+ { value: '压缩包', label: '压缩包' },
+ { value: '补丁', label: '补丁' },
+ { value: '安装包', label: '安装包' },
+ { value: 'nds', label: 'nds' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'language', label: '语言', type: 'select',
+ options: [
+ { value: '中文', label: '中文' },
+ { value: '英文', label: '英文' },
+ { value: '日文', label: '日文' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 6: { // 综艺
+ name: '综艺',
+ filters: [
+ {
+ id: 'is_mainland', label: '是否大陆综艺', type: 'select',
+ options: [
+ { value: 'true', label: '是' },
+ { value: 'false', label: ' 不是' },
+ ]
+ },
+ {
+ id: 'format', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '游戏', label: '游戏' },
+ { value: '音乐', label: '音乐' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 7: { // 体育
+ name: '体育',
+ filters: [
+ {
+ id: 'genre', label: '体育类型', type: 'select',
+ options: [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'event_type', label: '赛事类型', type: 'select',
+ options: [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 8: { // 软件
+ name: '软件',
+ filters: [
+ {
+ id: 'platform', label: '平台', type: 'select',
+ options: [
+ { value: 'Windows', label: 'Windows' },
+ { value: 'Mac', label: 'Mac' },
+ { value: 'Linux', label: 'Linux' },
+ { value: 'Android', label: 'Android' },
+ { value: 'iOS', label: 'iOS' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'EXE', label: 'EXE' },
+ { value: 'DMG', label: 'DMG' },
+ { value: '光盘镜像', label: '光盘镜像' },
+ { value: 'APK', label: 'APK' },
+ { value: 'IPA', label: 'IPA' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '系统软件', label: '系统软件' },
+ { value: '应用软件', label: '应用软件' },
+ { value: '游戏软件', label: '游戏软件' },
+ { value: '驱动程序', label: '驱动程序' },
+ { value: '办公软件', label: '办公软件' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ ]
+ },
+ 9: { // 学习
+ name: '学习',
+ filters: [
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '计算机', label: '计算机' },
+ { value: '软件', label: '软件' },
+ { value: '人文', label: '人文' },
+ { value: '外语', label: '外语' },
+ { value: '理工科', label: '理工科' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'PDF', label: 'PDF' },
+ { value: 'EPUB', label: 'EPUB' },
+ { value: '视频', label: '视频' },
+ { value: '音频', label: '音频' },
+ { value: 'PPT', label: 'PPT' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 10: { // 纪录片
+ name: '纪录片',
+ filters: [
+ {
+ id: 'source', label: '视频源', type: 'select',
+ options: [
+ { value: 'CCTV', label: 'CCTV' },
+ { value: '卫视', label: '卫视' },
+ { value: '国家地理', label: '国家地理' },
+ { value: 'BBC', label: 'BBC' },
+ { value: 'Discovery', label: 'Discovery' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 11: { // 其他
+ name: '其他',
+ filters: [
+ {
+ id: 'gener', label: '类型', type: 'select',
+ options: [
+ { value: '电子书', label: '电子书' },
+ { value: '视频', label: '视频' },
+ { value: 'MP3', label: 'MP3' },
+ { value: '图片', label: '图片' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ }
+ // 其他分类配置...
+ }
+};
+
+
+
+function TorrentList() {
+ const [torrents, setTorrents] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState("");
+ const [filters, setFilters] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ //const userId = localStorage.getItem('userId'); // 假设用户ID存储在localStorage中
+ const userId = 1; // 确保是数字类型
+ const [usernames, setUsernames] = useState({});
+ const [searchKeyword, setSearchKeyword] = useState('');
+ // [新增] 分页相关状态
+ const [currentPage, setCurrentPage] = useState(1); // 当前页码
+ const [itemsPerPage, setItemsPerPage] = useState(10); // 每页显示的项目数
+ const [totalItems, setTotalItems] = useState(0); // 确保这行存在
+const [isReportModalVisible, setIsReportModalVisible] = useState(false);
+const [currentTorrentId, setCurrentTorrentId] = useState(null);
+const [reportContent, setReportContent] = useState('');
+const [currentTorrent, setCurrentTorrent] = useState({});
+const[currentTorrentUploaderId, setCurrentTorrentUploaderId] = useState(null);
+ // 获取所有分类
+ useEffect(() => {
+ const fetchCategories = async () => {
+ try {
+ const res = await axios.get('http://localhost:8080/categories');
+ setCategories(res.data);
+ } catch (err) {
+ console.error('加载分类失败', err);
+ setError('加载分类失败,请稍后重试');
+ }
+ };
+ fetchCategories();
+ }, []);
+
+
+// 获取分类筛选配置
+const getCategoryFilters = (categoryId) => {
+
+ const category = FILTER_OPTIONS.categories[categoryId];
+ if (!category) return [];
+
+ return category.filters.map(filter => {
+ // 自动填充通用选项
+ if (filter.id === 'resolution' && !filter.options) {
+ return { ...filter, options: FILTER_OPTIONS.common.resolution };
+ }
+ if (filter.id === 'region' && !filter.options) {
+ const regionType = categoryId === 8 ? 'sports' : 'movie';
+ return { ...filter, options: FILTER_OPTIONS.common.region[regionType] };
+ }
+ if (filter.id === 'genre' && !filter.options) {
+ const genreType = categoryId === 3 ? 'music' : 'movie';
+ return { ...filter, options: FILTER_OPTIONS.common.genre[genreType] };
+ }
+ return filter;
+ });
+};
+
+// 显示举报模态框
+const showReportModal = (torrentId) => {
+ setCurrentTorrentId(torrentId);
+ setReportContent('');
+ setIsReportModalVisible(true);
+};
+
+// 处理举报提交
+const handleReportSubmit = async () => {
+ if (!reportContent.trim()) {
+ message.error('请输入举报内容');
+ return;
+ }
+
+ try {
+ const currentTorrentData = torrents.find(t => t.torrentid === currentTorrentId);
+ const duser = currentTorrentData ? currentTorrentData.uploader_id : null;
+ const complainData = {
+ puse: userId, // 举报人ID
+ duser: duser, // 被举报人ID
+ content: reportContent,
+ torrentid: currentTorrentId,
+
+ };
+ console.log('举报数据:', complainData)
+
+ await createComplain(complainData);
+ message.success('举报提交成功');
+ setIsReportModalVisible(false);
+ } catch (error) {
+ console.error('举报提交失败:', error);
+ message.error('举报提交失败,请稍后重试');
+ }
+ };
+
+
+
+// 格式化日期显示
+const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' };
+ return new Date(dateString).toLocaleString('zh-CN', options);
+};
+
+// 获取促销方式名称
+const getPromotionName = (promotionId) => {
+ switch (promotionId) {
+ case 1: return '上传加倍';
+ case 2: return '下载免费';
+ case 3: return '下载减半';
+ default: return '没有促销';
+ }
+};
+
+ // 搜索种子
+ const handleSearch = async () => {
+ if (!searchKeyword.trim()) {
+ fetchAllTorrents();
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ try {
+ const res = await axios.get(`http://localhost:8080/torrent/search`, {
+ params: { keyword: searchKeyword },
+ });
+ setTorrents(res.data);
+ //setCurrentPage(1); // 搜索后重置为第一页
+ } catch (err) {
+ console.error('搜索失败', err);
+ setError('搜索失败,请稍后重试');
+ message.error('搜索失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 获取种子数据
+ // [修改] 获取种子数据
+ useEffect(() => {
+ const fetchTorrents = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ let url = selectedCategory
+ ? `http://localhost:8080/torrent/listByCategorywithfilter?categoryid=${selectedCategory}`
+ : `http://localhost:8080/torrent/list`;
+
+ // [修改] 添加筛选参数(不再需要page和limit参数)
+ const params = new URLSearchParams();
+ Object.entries(filters).forEach(([key, value]) => {
+ if (value) params.append(key, value);
+ });
+
+ // [修改] 只有当有筛选参数时才添加
+ if (params.toString()) {
+ const separator = selectedCategory ? '&' : '?';
+ url += separator + params.toString();
+ }
+
+ const res = await axios.get(url);
+ setTorrents(res.data); // [修改] 存储所有数据,不再分页
+ setTotalItems(res.data.length); // [新增] 设置总数据量
+ } catch (err) {
+ console.error('获取种子失败', err);
+ setError('获取种子列表失败,请稍后重试');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const timer = setTimeout(fetchTorrents, 300);
+ return () => clearTimeout(timer);
+ }, [selectedCategory, filters]); // [注意] 依赖项不变
+
+ // [新增] 前端分页处理函数
+ const paginateData = (data, currentPage, itemsPerPage) => {
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ return data.slice(startIndex, endIndex);
+ };
+
+ const currentTorrents = paginateData(torrents, currentPage, itemsPerPage);
+ useEffect(() => {
+ const fetchUsernames = async () => {
+ if (torrents.length === 0) return;
+
+ const usernamePromises = torrents.map(async (torrent) => {
+ if (torrent.uploader_id && !usernames[torrent.uploader_id]) {
+ try {
+ const response = await fetch(`http://localhost:8080/torrent/${torrent.uploader_id}/username`);
+ if (response.ok) {
+ const username = await response.text();
+ return { [torrent.uploader_id]: username };
+ }
+ } catch (error) {
+ console.error(`Failed to fetch username for uploader_id ${torrent.uploader_id}:`, error);
+ }
+ }
+ return {};
+ });
+
+ const results = await Promise.all(usernamePromises);
+ const mergedUsernames = results.reduce((acc, curr) => ({ ...acc, ...curr }), {});
+ setUsernames((prev) => ({ ...prev, ...mergedUsernames }));
+ };
+
+ fetchUsernames();
+ }, [torrents]);
+
+
+ // 切换分类时重置筛选条件
+ const handleCategoryChange = (categoryId) => {
+ setSelectedCategory(categoryId);
+ setFilters({});
+ };
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 处理筛选条件变化
+ const handleFilterChange = (e) => {
+ const { name, value } = e.target;
+ setFilters(prev => ({ ...prev, [name]: value }));
+ };
+
+
+
+ // 下载种子
+ const handleDownload = (torrentId) => {
+ //window.open(`http://localhost:8080/torrent/download/${torrentId}`, '_blank');
+ window.open(`http://localhost:8080/torrent/download/${torrentId}?userId=${userId}`, '_blank');
+
+ };
+
+ // 获取当前分类的筛选配置
+ const currentFilters = getCategoryFilters(selectedCategory);
+
+ return (
+ <div className="p-4 max-w-7xl mx-auto">
+ <Navbar />
+ {/* <h1 className="text-2xl font-bold mb-6">种子列表</h1> */}
+
+ {/* 搜索框 */}
+ <div className="mb-4 flex items-center">
+ <Input
+ placeholder="搜索种子..."
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ style={{ width: 300 }}
+ onPressEnter={handleSearch}
+ />
+ <Button
+ type="primary"
+ onClick={handleSearch}
+ style={{ marginLeft: 8 }}
+ >
+ 搜索
+ </Button>
+ </div>
+
+ <div className="filter-container">
+ <div className="filter-container">
+ <div className="filter-row">
+ <label className="filter-label">选择分类:</label>
+ <Radio.Group
+ onChange={(e) => handleCategoryChange(e.target.value)}
+ value={selectedCategory}
+ className="flex space-x-2" // 添加 flex 布局
+ >
+ <Radio.Button className="custom-radio-btn" value="">
+ 全部分类
+ </Radio.Button>
+ {categories.map(cat => (
+ <Radio.Button key={cat.categoryid} className="custom-radio-btn" value={cat.categoryid}>
+ {cat.category_name}
+ </Radio.Button>
+ ))}
+ </Radio.Group>
+ </div>
+
+ </div>
+
+ {currentFilters.length > 0 &&
+ currentFilters.map(filter => (
+ <div key={filter.id} className="filter-row">
+ <label className="filter-label">{filter.label}:</label>
+ <Radio.Group
+ onChange={(e) => handleFilterChange({ target: { name: filter.id, value: e.target.value } })}
+ value={filters[filter.id] || ''}
+ >
+ <Radio.Button className="custom-radio-btn" value="">全部</Radio.Button>
+ {filter.options.map(option => (
+ <Radio.Button key={option.value} className="custom-radio-btn" value={option.value}>
+ {option.label}
+ </Radio.Button>
+ ))}
+ </Radio.Group>
+ </div>
+ ))}
+ </div>
+
+
+ {/* 错误提示 */}
+ {error && (
+ <div className="mb-4 p-3 bg-red-100 text-red-700 rounded border border-red-200">
+ {error}
+ </div>
+ )}
+
+
+ {isLoading ? (
+ <div className="loading-container">
+ <div className="spinner"></div>
+ </div>
+ ) : (
+ <div className="torrents-container">
+ {torrents.length > 0 ? (
+ <div className="torrents-grid">
+ {currentTorrents.map(torrent => (
+ <div key={torrent.torrentid} className="torrent-card">
+ <div className="cover">
+ {torrent.coverImagePath ? (
+ <img
+ src={torrent.coverImagePath}
+ alt="封面"
+ className="cover-image"
+ />
+ ) : (
+ <div className="no-cover">无封面</div>
+ )}
+ </div>
+
+ <div className="info">
+ <h3 className="title" title={torrent.filename}>
+ {torrent.filename}
+ </h3>
+ <p className="description" title={torrent.description}>
+ {torrent.description || '暂无描述'}
+ </p>
+
+ <div className="details">
+ <span>大小: {formatFileSize(torrent.torrentSize)}</span>
+ <span>上传者: {usernames[torrent.uploader_id]}</span>
+ <span>上传时间: {new Date(torrent.uploadTime).toLocaleDateString()}</span>
+ <span>下载次数: {torrent.downloadCount}</span>
+ <span>促销: {getPromotionName(torrent.promotionid)}</span>
+ </div>
+
+ <div className="actions">
+ <button
+ onClick={() => handleDownload(torrent.torrentid)}
+ className="btn btn-download"
+ >
+ 下载
+ </button>
+ <Link
+ to={`/torrent/${torrent.torrentid}`}
+ className="btn btn-detail"
+ >
+ 详情
+ </Link>
+ </div>
+
+ {/* 新增举报按钮 */}
+
+<button
+ className="report-btn"
+ onClick={() => showReportModal(torrent.torrentid)}
+>
+ 举报
+</button>
+
+ {/* 添加举报模态框 */}
+
+
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className="no-data">没有找到符合条件的种子</div>
+ )}
+ </div>
+ )}
+ <Modal
+ title="举报内容"
+ open={isReportModalVisible}
+ onOk={handleReportSubmit}
+ onCancel={() => setIsReportModalVisible(false)}
+ okText="提交"
+ cancelText="取消"
+ >
+ <p>您正在举报种子ID: {currentTorrentId}</p>
+ <Input.TextArea
+ rows={4}
+ value={reportContent}
+ onChange={(e) => setReportContent(e.target.value)}
+ placeholder="请输入举报原因..."
+ />
+ </Modal>
+ {/* // [新增] 分页组件 */}
+ {totalItems > 0 && (
+ <div className="pagination-container mt-6 flex justify-center">
+ <Pagination
+ current={currentPage}
+ pageSize={itemsPerPage}
+ total={totalItems}
+ onChange={(page) => setCurrentPage(page)} // [新增] 页码变化处理
+ showSizeChanger={false} // [可选] 是否显示每页条数选择器
+ showTotal={(total) => `共 ${total} 条记录`} // [可选] 显示总条数
+ />
+ </div>
+ )}
+
+ </div>
+ );
+}
+
+export default TorrentList;
\ No newline at end of file
diff --git a/src/components/torrentmanage.jsx b/src/components/torrentmanage.jsx
new file mode 100644
index 0000000..4756b35
--- /dev/null
+++ b/src/components/torrentmanage.jsx
@@ -0,0 +1,443 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Table,
+ Button,
+ Modal,
+ Image,
+ message,
+ Spin,
+ Input,
+ Select,
+ Pagination,
+ Space
+} from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import axios from 'axios';
+
+const { confirm } = Modal;
+const { Option } = Select;
+
+const TorrentManagement = () => {
+ // 状态管理
+ const [torrents, setTorrents] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [selectedTorrentId, setSelectedTorrentId] = useState(null);
+ const [promotionOptions, setPromotionOptions] = useState([
+ { value: 1, label: '上传加倍' },
+ { value: 2, label: '下载减半' },
+ { value: 3, label: '免费下载' },
+ { value: 0, label: '无促销' }
+ ]);
+ const [selectedPromotion, setSelectedPromotion] = useState(null);
+ const [showPromotionWarning, setShowPromotionWarning] = useState(false);
+ const [currentUserId, setCurrentUserId] = useState(null);
+ const [applyPromotionsLoading, setApplyPromotionsLoading] = useState(false);
+ const [usernames, setUsernames] = useState({});
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const navigate = useNavigate(); // 用于导航到详情页
+
+ // 获取当前用户ID
+ useEffect(() => {
+ const userId = 1; // 示例,实际从认证系统获取
+ setCurrentUserId(userId ? parseInt(userId) : null);
+ }, []);
+
+ // 获取所有种子数据
+ useEffect(() => {
+ fetchAllTorrents();
+ }, [searchKeyword]);
+
+ // 获取所有种子数据的函数
+ const fetchAllTorrents = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const res = await axios.get('http://localhost:8080/torrent/list');
+ setTorrents(res.data);
+ setCurrentPage(1); // 重置为第一页
+ } catch (err) {
+ console.error('获取种子失败', err);
+ setError('获取种子列表失败,请稍后重试');
+ message.error('获取种子列表失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ console.log('当前种子列表:', torrents);
+
+ // 在组件加载时,批量获取所有 uploader_id 对应的 username
+ useEffect(() => {
+ const fetchUsernames = async () => {
+ if (torrents.length === 0) return;
+
+ const usernamePromises = torrents.map(async (torrent) => {
+ if (torrent.uploader_id && !usernames[torrent.uploader_id]) {
+ try {
+ const response = await fetch(`http://localhost:8080/torrent/${torrent.uploader_id}/username`);
+ if (response.ok) {
+ const username = await response.text();
+ return { [torrent.uploader_id]: username };
+ }
+ } catch (error) {
+ console.error(`Failed to fetch username for uploader_id ${torrent.uploader_id}:`, error);
+ }
+ }
+ return {};
+ });
+
+ const results = await Promise.all(usernamePromises);
+ const mergedUsernames = results.reduce((acc, curr) => ({ ...acc, ...curr }), {});
+ setUsernames((prev) => ({ ...prev, ...mergedUsernames }));
+ };
+
+ fetchUsernames();
+ }, [torrents]);
+
+ // 处理删除种子
+ const handleDeleteTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ confirm({
+ title: '确认删除',
+ icon: <ExclamationCircleOutlined />,
+ content: '确定要删除这个种子吗?此操作不可恢复!',
+ onOk: async () => {
+ try {
+ await axios.delete(`http://localhost:8080/torrent/delete/${torrentId}`, {
+ params: { userid: currentUserId }
+ });
+ setTorrents(torrents.filter(torrent => torrent.torrentid !== torrentId));
+ message.success('种子删除成功');
+ } catch (err) {
+ console.error('删除种子失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权删除此种子');
+ } else {
+ message.error('删除种子失败');
+ }
+ }
+ }
+ });
+ };
+
+ // 搜索种子
+ const handleSearch = async () => {
+ if (!searchKeyword.trim()) {
+ fetchAllTorrents();
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ try {
+ const res = await axios.get(`http://localhost:8080/torrent/search`, {
+ params: { keyword: searchKeyword },
+ });
+ setTorrents(res.data);
+ setCurrentPage(1); // 搜索后重置为第一页
+ } catch (err) {
+ console.error('搜索失败', err);
+ setError('搜索失败,请稍后重试');
+ message.error('搜索失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 处理修改促销方式
+ const handlePromotionChange = (torrentId, newPromotion) => {
+ setSelectedTorrentId(torrentId);
+ setSelectedPromotion(newPromotion);
+ setShowPromotionWarning(true);
+ };
+
+ // 确认修改促销方式
+ const confirmPromotionChange = async () => {
+ if (selectedTorrentId && selectedPromotion !== null) {
+ try {
+ await axios.post('http://localhost:8080/torrent/setPromotion', null, {
+ params: {
+ userid: currentUserId,
+ torrentId: selectedTorrentId,
+ promotionId: selectedPromotion
+ }
+ });
+ setTorrents(torrents.map(torrent =>
+ torrent.torrentid === selectedTorrentId
+ ? { ...torrent, promotionid: selectedPromotion }
+ : torrent
+ ));
+ setShowPromotionWarning(false);
+ message.success('促销方式修改成功');
+ } catch (err) {
+ console.error('修改促销方式失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权修改此种子的促销方式');
+ } else {
+ message.error('修改促销方式失败');
+ }
+ }
+ }
+ };
+
+ // 取消修改促销方式
+ const cancelPromotionChange = () => {
+ setShowPromotionWarning(false);
+ setSelectedTorrentId(null);
+ setSelectedPromotion(null);
+ };
+
+ // 触发检查(应用促销规则)
+ const handleApplyPromotions = async () => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ setApplyPromotionsLoading(true);
+ try {
+ const res = await axios.post('http://localhost:8080/torrent/applyPromotions', null, {
+ params: { userid: currentUserId }
+ });
+
+ if (res.data.success) {
+ message.success(res.data.message);
+ fetchAllTorrents(); // 刷新种子列表
+ }
+ } catch (err) {
+ console.error('应用促销规则失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权执行此操作');
+ } else {
+ message.error('应用促销规则失败');
+ }
+ } finally {
+ setApplyPromotionsLoading(false);
+ }
+ };
+
+ // 分页数据计算
+ const getCurrentPageData = () => {
+ const start = (currentPage - 1) * pageSize;
+ const end = start + pageSize;
+ return torrents.slice(start, end);
+ };
+
+ // 页码变化处理
+ const handlePageChange = (page) => {
+ setCurrentPage(page);
+ };
+
+ // 每页条数变化处理
+ const handlePageSizeChange = (current, size) => {
+ setPageSize(size);
+ setCurrentPage(1); // 重置为第一页
+ };
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' };
+ return new Date(dateString).toLocaleString('zh-CN', options);
+ };
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 获取促销方式名称
+ const getPromotionName = (promotionId) => {
+ if (promotionId === null) return '无促销';
+ const option = promotionOptions.find(opt => opt.value === promotionId);
+ return option ? option.label : '未知促销';
+ };
+
+ const handleViewDetails = (torrentId) => {
+ navigate(`/admin/${torrentId}`); // 使用已定义的 navigate 变量
+ };
+
+ return (
+ <div className="p-4 max-w-7xl mx-auto">
+ <h1 className="text-2xl font-bold mb-6">种子管理</h1>
+
+ {/* 搜索框 */}
+ <div className="mb-4 flex items-center">
+ <Input
+ placeholder="搜索种子..."
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ style={{ width: 300 }}
+ onPressEnter={handleSearch}
+ />
+ <Button
+ type="primary"
+ onClick={handleSearch}
+ style={{ marginLeft: 8 }}
+ >
+ 搜索
+ </Button>
+ </div>
+
+ {/* 右上角按钮 */}
+ <Button
+ type="primary"
+ loading={applyPromotionsLoading}
+ onClick={handleApplyPromotions}
+ style={{ marginBottom: 16 }}
+ >
+ 触发检查
+ </Button>
+
+ {/* 加载状态 */}
+ {isLoading && <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />}
+
+ {/* 错误提示 */}
+ {error && <div className="mb-4 p-3 bg-red-100 text-red-700 rounded border border-red-200">{error}</div>}
+
+ {/* 种子列表表格 */}
+ {!isLoading && !error && (
+ <>
+ <Table
+ columns={[
+ {
+ title: '封面',
+ dataIndex: 'coverImagePath',
+ key: 'coverImagePath',
+ render: (text) => text ? (
+ <Image
+ src={text}
+ width={50}
+ height={50}
+ preview={{ maskClosable: true }}
+ />
+ ) : (
+ <div className="w-16 h-16 bg-gray-200 flex items-center justify-center">无封面</div>
+ )
+ },
+ {
+ title: '名称',
+ dataIndex: 'filename',
+ key: 'filename'
+ },
+ {
+ title: '描述',
+ dataIndex: 'description',
+ key: 'description'
+ },
+ {
+ title: '大小',
+ dataIndex: 'torrentSize',
+ key: 'torrentSize',
+ render: (size) => formatFileSize(size)
+ },
+ {
+ title: '上传者',
+ dataIndex: 'uploader_id',
+ key: 'uploader_id',
+ render: (id) => usernames[id] || id
+ },
+ {
+ title: '上传时间',
+ dataIndex: 'uploadTime',
+ key: 'uploadTime',
+ render: (time) => formatDate(time)
+ },
+ {
+ title: '下载次数',
+ dataIndex: 'downloadCount',
+ key: 'downloadCount'
+ },
+ {
+ title: '促销',
+ dataIndex: 'promotionid',
+ key: 'promotionid',
+ render: (id) => getPromotionName(id)
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_, record) => (
+ <Space>
+ <Button
+ danger
+ onClick={() => handleDeleteTorrent(record.torrentid)}
+ loading={isLoading}
+ >
+ 删除
+ </Button>
+ <Select
+ value={record.promotionid}
+ onChange={(value) => handlePromotionChange(record.torrentid, value)}
+ style={{ width: 120 }}
+ disabled={isLoading}
+ >
+ {promotionOptions.map(option => (
+ <Option key={option.value} value={option.value}>{option.label}</Option>
+ ))}
+ </Select>
+ <Button
+ type="primary"
+ size="small"
+ onClick={() => handleViewDetails(record.torrentid)} // 使用处理函数
+ >
+ 查看详情
+ </Button>
+ </Space>
+ )
+ }
+ ]}
+ dataSource={getCurrentPageData()}
+ rowKey="torrentid"
+ pagination={false}
+ loading={isLoading}
+ />
+
+ {/* 分页控件 */}
+ {torrents.length > 0 && (
+ <div style={{ marginTop: 16, textAlign: 'center' }}>
+ <Pagination
+ current={currentPage}
+ pageSize={pageSize}
+ total={torrents.length}
+ onChange={handlePageChange}
+ onShowSizeChange={handlePageSizeChange}
+ showSizeChanger
+ showTotal={(total) => `共 ${total} 条记录`}
+ pageSizeOptions={['10', '20', '50']}
+ />
+ </div>
+ )}
+ </>
+ )}
+
+ {/* 促销方式修改确认弹窗 */}
+ <Modal
+ title="确认修改促销方式"
+ open={showPromotionWarning}
+ onOk={confirmPromotionChange}
+ onCancel={cancelPromotionChange}
+ okText="确认"
+ cancelText="取消"
+ >
+ <p>
+ 您确定要将种子 ID 为
+ <span className="font-bold">{selectedTorrentId}</span> 的促销方式修改为
+ <span className="font-bold">「{getPromotionName(selectedPromotion)}」</span> 吗?
+ </p>
+ </Modal>
+ </div>
+ );
+};
+
+export default TorrentManagement;
\ No newline at end of file
diff --git a/src/test/TorrentList.search.test.jsx b/src/test/TorrentList.search.test.jsx
new file mode 100644
index 0000000..6c588fd
--- /dev/null
+++ b/src/test/TorrentList.search.test.jsx
@@ -0,0 +1,65 @@
+// TorrentList.test.jsx
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import TorrentList from '../components/TorrentList';
+import axios from 'axios';
+import { MemoryRouter } from 'react-router-dom'; // ✅ 引入 MemoryRouter
+
+
+import { vi } from 'vitest';
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+vi.mock('axios');
+
+
+
+describe('TorrentList - 搜索功能', () => {
+ test('搜索关键词后应正确调用接口并显示结果', async () => {
+ const mockTorrents = [
+ { id: 1, title: '测试种子1', uploader_id: 123 },
+ { id: 2, title: '测试种子2', uploader_id: 456 },
+ ];
+
+ axios.get.mockResolvedValueOnce({ data: mockTorrents });
+
+ render(
+ <MemoryRouter>
+ <TorrentList />
+ </MemoryRouter>
+ );
+
+ // 输入关键词
+ const input = screen.getByPlaceholderText(/搜索种子/i);
+ fireEvent.change(input, { target: { value: '测试' } });
+
+ // 模拟点击搜索按钮
+ const button = screen.getByRole('button', { name: /搜\s*索/i });
+ fireEvent.click(button);
+
+
+ // 等待并断言结果被渲染
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith(
+ 'http://localhost:8080/torrent/search',
+ { params: { keyword: '测试' } }
+ );
+ });
+
+ // expect(await screen.findByText('测试种子1')).toBeInTheDocument();
+ // expect(await screen.findByText('测试种子2')).toBeInTheDocument();
+ });
+});