查看种子列表用户端和管理员端界面

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();
+  });
+});