feat(torrents): 实现种子上传和列表展示功能

- 新增种子上传功能,包括用户认证和表单处理
- 实现种子列表展示,含搜索和筛选功能
- 调整布局和样式,优化用户体验
- 移除不必要的分类和筛选条件
- 重构部分代码以适应新功能

Change-Id: I86229123ba1e1a70e632042b2a9544e08c3c6876
diff --git a/src/api/torrents.js b/src/api/torrents.js
new file mode 100644
index 0000000..8dd603d
--- /dev/null
+++ b/src/api/torrents.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request";
+
+export const uploadTorrent = (formData) => {
+  return request.post("/resource/publish", formData);
+};
+
+export const getTorrentList = (params) => {
+  return request.get("/list/all", { params });
+};
\ No newline at end of file
diff --git a/src/features/admin/pages/AdminPanel.jsx b/src/features/admin/pages/AdminPanel.jsx
index bffa5bf..5296647 100644
--- a/src/features/admin/pages/AdminPanel.jsx
+++ b/src/features/admin/pages/AdminPanel.jsx
@@ -73,7 +73,6 @@
       key: 'action',
       render: (_, record) => (
         <Space size="middle">
-          <Button type="link">编辑</Button>
           {record.userType !== 1 && (
             <Popconfirm
               title="确定要删除该用户吗?"
diff --git a/src/features/torrents/pages/TorrentListPage.jsx b/src/features/torrents/pages/TorrentListPage.jsx
index 9e1fd49..c0bcc7a 100644
--- a/src/features/torrents/pages/TorrentListPage.jsx
+++ b/src/features/torrents/pages/TorrentListPage.jsx
@@ -29,6 +29,8 @@
   ReloadOutlined,
 } from "@ant-design/icons";
 import { Link } from "react-router-dom";
+import { getTorrentList } from "../../../api/torrents";
+import { useAuth } from "../../auth/contexts/AuthContext";
 
 const { Title, Text } = Typography;
 const { TabPane } = Tabs;
@@ -245,32 +247,66 @@
 
 const TorrentListPage = () => {
   // 状态管理
-  const [activeCategory, setActiveCategory] = useState("all");
-  const [activeSubcategories, setActiveSubcategories] = useState([]);
-  const [statusFilter, setStatusFilter] = useState("all"); // 'all', 'active', 'dead'
   const [ownTorrentsOnly, setOwnTorrentsOnly] = useState(false);
-  const [subtitledOnly, setSubtitledOnly] = useState(false);
-  const [collectionsOnly, setCollectionsOnly] = useState(false);
   const [searchText, setSearchText] = useState("");
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize] = useState(50);
   
   // 添加加载状态
   const [loading, setLoading] = useState(true);
-  const [categoryLoading, setCategoryLoading] = useState(false);
   const [error, setError] = useState(null);
   const [torrents, setTorrents] = useState([]);
+  
+  // 获取用户信息
+  const { user } = useAuth();
 
-  // 模拟数据加载
+  // 加载种子数据
   useEffect(() => {
     const loadData = async () => {
+      if (!user?.username && !user?.uid) {
+        setError("用户信息不完整,请重新登录");
+        setLoading(false);
+        return;
+      }
+
       try {
         setLoading(true);
-        // 模拟网络延迟
-        await new Promise((resolve) => setTimeout(resolve, 1000));
-        setTorrents(torrentData);
+        const username = user?.username || user?.uid;
+        const response = await getTorrentList({ username });
+        
+        if (response && Array.isArray(response)) {
+          // 转换数据格式,添加缺失字段的默认值
+          const transformedData = response.map((item, index) => ({
+            key: item.resourceId || index,
+            id: item.resourceId,
+            resourceId: item.resourceId,
+            title: item.name || "未知标题",
+            name: item.name,
+            size: item.size || "0 MB",
+            publishTime: item.publishTime || "",
+            uploader: item.author || "未知作者",
+            author: item.author,
+            description: item.description || "",
+            // 设置默认值
+            category: "其他",
+            subcategory: "其他",
+            fileCount: 1,
+            views: 0,
+            seeders: 0,
+            leechers: 0,
+            completed: 0,
+            isOwnTorrent: (item.author === username),
+            hasSubtitle: false,
+            isCollection: false,
+            isActive: true,
+          }));
+          setTorrents(transformedData);
+        } else {
+          setTorrents([]);
+        }
         setError(null);
       } catch (err) {
+        console.error("加载种子数据失败:", err);
         setError("加载种子数据失败,请稍后再试");
         setTorrents([]);
       } finally {
@@ -279,73 +315,23 @@
     };
 
     loadData();
-  }, []);
+  }, [user]);
 
-  // 模拟分类切换加载
-  const handleCategoryChange = (key) => {
-    setCategoryLoading(true);
-    setActiveCategory(key);
-    setActiveSubcategories([]);
-    setCurrentPage(1);
-    
-    // 模拟切换延迟
-    setTimeout(() => {
-      setCategoryLoading(false);
-    }, 500);
-  };
 
-  // 处理二级分类变化
-  const handleSubcategoryChange = (subcategory) => {
-    const newSubcategories = [...activeSubcategories];
-    const index = newSubcategories.indexOf(subcategory);
-
-    if (index > -1) {
-      newSubcategories.splice(index, 1);
-    } else {
-      newSubcategories.push(subcategory);
-    }
-
-    setActiveSubcategories(newSubcategories);
-    setCurrentPage(1);
-  };
 
   // 过滤种子数据
   const filteredTorrents = torrents.filter((torrent) => {
-    // 一级分类筛选
-    if (activeCategory !== "all") {
-      const categoryObj = categories.find((cat) => cat.key === activeCategory);
-      if (categoryObj && torrent.category !== categoryObj.name) {
-        return false;
-      }
-    }
-
-    // 二级分类筛选
-    if (
-      activeSubcategories.length > 0 &&
-      !activeSubcategories.includes(torrent.subcategory)
-    ) {
-      return false;
-    }
-
-    // 状态筛选
-    if (statusFilter === "active" && !torrent.isActive) return false;
-    if (statusFilter === "dead" && torrent.isActive) return false;
-
     // 我的种子筛选
     if (ownTorrentsOnly && !torrent.isOwnTorrent) return false;
 
-    // 有字幕筛选
-    if (subtitledOnly && !torrent.hasSubtitle) return false;
-
-    // 合集筛选
-    if (collectionsOnly && !torrent.isCollection) return false;
-
     // 搜索文本筛选
-    if (
-      searchText &&
-      !torrent.title.toLowerCase().includes(searchText.toLowerCase())
-    ) {
-      return false;
+    if (searchText) {
+      const searchLower = searchText.toLowerCase();
+      return (
+        torrent.title.toLowerCase().includes(searchLower) ||
+        torrent.description.toLowerCase().includes(searchLower) ||
+        torrent.uploader.toLowerCase().includes(searchLower)
+      );
     }
 
     return true;
@@ -363,28 +349,21 @@
   // 表格列定义
   const columns = [
     {
-      title: "类别",
-      dataIndex: "category",
-      key: "category",
-      width: 100,
-      render: (text, record) => (
-        <Space direction="vertical" size={0}>
-          <Tag color="blue">{text}</Tag>
-          <Text type="secondary" style={{ fontSize: "12px" }}>
-            {record.subcategory}
-          </Text>
-        </Space>
-      ),
-    },
-    {
-      title: "名称",
+      title: "资源名称",
       dataIndex: "title",
       key: "title",
       render: (text, record) => (
-        <Space>
-          <Link to={`/torrent/${record.id}`}>{text}</Link>
-          {record.hasSubtitle && <Tag color="green">中字</Tag>}
-          {record.isCollection && <Tag color="purple">合集</Tag>}
+        <Space direction="vertical" size={2}>
+          <Link to={`/torrent/${record.id}`} style={{ fontWeight: 'bold' }}>
+            {text}
+          </Link>
+          {record.description && (
+            <Text type="secondary" style={{ fontSize: "12px" }}>
+              {record.description.length > 50 
+                ? `${record.description.substring(0, 50)}...` 
+                : record.description}
+            </Text>
+          )}
         </Space>
       ),
     },
@@ -410,80 +389,39 @@
       title: "大小",
       dataIndex: "size",
       key: "size",
-      width: 90,
-      sorter: (a, b) => parseFloat(a.size) - parseFloat(b.size),
-    },
-    {
-      title: "文件",
-      dataIndex: "fileCount",
-      key: "fileCount",
-      width: 70,
-      render: (count) => (
-        <Tooltip title={`${count} 个文件`}>
-          <span>
-            <FileTextOutlined /> {count}
-          </span>
-        </Tooltip>
-      ),
-    },
-    {
-      title: "点击",
-      dataIndex: "views",
-      key: "views",
-      width: 70,
-      sorter: (a, b) => a.views - b.views,
+      width: 100,
+      sorter: (a, b) => {
+        // 简单的大小排序,假设格式为 "数字 单位"
+        const parseSize = (sizeStr) => {
+          const match = sizeStr.match(/^([\d.]+)\s*(\w+)/);
+          if (!match) return 0;
+          const [, num, unit] = match;
+          const multipliers = { 'KB': 1, 'MB': 1024, 'GB': 1024 * 1024, 'TB': 1024 * 1024 * 1024 };
+          return parseFloat(num) * (multipliers[unit.toUpperCase()] || 1);
+        };
+        return parseSize(a.size) - parseSize(b.size);
+      },
     },
     {
       title: "发布时间",
       dataIndex: "publishTime",
       key: "publishTime",
-      width: 100,
+      width: 120,
       sorter: (a, b) => new Date(a.publishTime) - new Date(b.publishTime),
-    },
-    {
-      title: "种子数",
-      dataIndex: "seeders",
-      key: "seeders",
-      width: 80,
-      sorter: (a, b) => a.seeders - b.seeders,
-      render: (seeders, record) => (
-        <Badge
-          count={seeders}
-          style={{
-            backgroundColor: seeders > 0 ? "#52c41a" : "#f5222d",
-            fontSize: "12px",
-          }}
-        />
-      ),
-    },
-    {
-      title: "下载量",
-      dataIndex: "leechers",
-      key: "leechers",
-      width: 80,
-      sorter: (a, b) => a.leechers - b.leechers,
-      render: (leechers) => (
-        <Badge
-          count={leechers}
-          style={{
-            backgroundColor: "#faad14",
-            fontSize: "12px",
-          }}
-        />
-      ),
-    },
-    {
-      title: "完成",
-      dataIndex: "completed",
-      key: "completed",
-      width: 70,
-      sorter: (a, b) => a.completed - b.completed,
+      render: (time) => {
+        if (!time) return '-';
+        try {
+          return new Date(time).toLocaleDateString('zh-CN');
+        } catch {
+          return time;
+        }
+      },
     },
     {
       title: "发布者",
       dataIndex: "uploader",
       key: "uploader",
-      width: 100,
+      width: 120,
       render: (text, record) => (
         <Space>
           <Link to={`/user/${text}`}>{text}</Link>
@@ -530,44 +468,7 @@
         </Button>
       </div>
 
-      {/* 一级分类筛选 */}
-      <Spin spinning={categoryLoading} indicator={antIcon}>
-        <Tabs
-          activeKey={activeCategory}
-          onChange={handleCategoryChange}
-          type="card"
-        >
-          {categories.map((category) => (
-            <TabPane
-              tab={
-                <span>
-                  {category.name} <Tag>{category.count}</Tag>
-                </span>
-              }
-              key={category.key}
-            >
-              {/* 二级分类筛选 */}
-              {category.subcategories.length > 0 && (
-                <div className="flex flex-wrap gap-2 p-3 bg-gray-50 rounded">
-                  {category.subcategories.map((subcategory) => (
-                    <Checkbox
-                      key={subcategory}
-                      checked={activeSubcategories.includes(subcategory)}
-                      onChange={() => handleSubcategoryChange(subcategory)}
-                    >
-                      {subcategory}
-                    </Checkbox>
-                  ))}
-                </div>
-              )}
-            </TabPane>
-          ))}
-        </Tabs>
-      </Spin>
-
-      <Divider style={{ margin: "12px 0" }} />
-
-      {/* 额外筛选条件 */}
+      {/* 筛选条件 */}
       <div className="flex justify-between items-center flex-wrap gap-4">
         <div className="flex items-center gap-4 flex-wrap">
           <Skeleton loading={loading} active paragraph={false} title={{ width: '100%' }} className="inline-block">
@@ -577,56 +478,17 @@
             >
               我的种子
             </Checkbox>
-
-            <Checkbox
-              checked={subtitledOnly}
-              onChange={(e) => setSubtitledOnly(e.target.checked)}
-            >
-              有字幕
-            </Checkbox>
-
-            <Checkbox
-              checked={collectionsOnly}
-              onChange={(e) => setCollectionsOnly(e.target.checked)}
-            >
-              合集
-            </Checkbox>
-
-            <Radio.Group
-              value={statusFilter}
-              onChange={(e) => setStatusFilter(e.target.value)}
-              optionType="button"
-              buttonStyle="solid"
-            >
-              <Radio.Button value="all">全部</Radio.Button>
-              <Radio.Button value="active">
-                <Tooltip title="有做种的资源">
-                  <Space>
-                    <Badge status="success" />
-                    仅活种
-                  </Space>
-                </Tooltip>
-              </Radio.Button>
-              <Radio.Button value="dead">
-                <Tooltip title="无人做种的资源">
-                  <Space>
-                    <Badge status="error" />
-                    仅死种
-                  </Space>
-                </Tooltip>
-              </Radio.Button>
-            </Radio.Group>
           </Skeleton>
         </div>
 
         <div>
           <Search
-            placeholder="搜索种子"
+            placeholder="搜索种子名称、描述或发布者"
             allowClear
             enterButton={<SearchOutlined />}
             size="middle"
             onSearch={handleSearch}
-            style={{ width: 300 }}
+            style={{ width: 350 }}
             loading={loading}
           />
         </div>
@@ -653,7 +515,7 @@
           }}
           size="middle"
           bordered
-          scroll={{ x: 1100 }}
+          scroll={{ x: 800 }}
           loading={{
             spinning: loading,
             indicator: <Spin indicator={antIcon} />,
diff --git a/src/features/torrents/pages/UploadTorrentPage.jsx b/src/features/torrents/pages/UploadTorrentPage.jsx
index 8e17a9e..6b94524 100644
--- a/src/features/torrents/pages/UploadTorrentPage.jsx
+++ b/src/features/torrents/pages/UploadTorrentPage.jsx
@@ -22,6 +22,8 @@
   InboxOutlined,
   InfoCircleOutlined,
 } from "@ant-design/icons";
+import { uploadTorrent } from "../../../api/torrents";
+import { useAuth } from "../../auth/contexts/AuthContext";
 
 const { Title, Paragraph, Text } = Typography;
 const { TabPane } = Tabs;
@@ -34,23 +36,16 @@
 const uploadProps = {
   name: "torrent",
   multiple: false,
-  action: "/api/upload-torrent",
   accept: ".torrent",
   maxCount: 1,
-  onChange(info) {
-    const { status } = info.file;
-    if (status === "done") {
-      message.success(`${info.file.name} 文件上传成功`);
-    } else if (status === "error") {
-      message.error(`${info.file.name} 文件上传失败`);
-    }
-  },
   beforeUpload(file) {
     const isTorrent = file.type === "application/x-bittorrent" || file.name.endsWith(".torrent");
     if (!isTorrent) {
       message.error("您只能上传 .torrent 文件!");
+      return Upload.LIST_IGNORE;
     }
-    return isTorrent || Upload.LIST_IGNORE;
+    // 阻止自动上传,我们将在表单提交时手动处理
+    return false;
   },
 };
 
@@ -142,7 +137,6 @@
     <Form.Item
       name="title"
       label="标题"
-      rules={[{ required: true, message: "请输入资源标题" }]}
     >
       <Input placeholder="请输入完整、准确的资源标题" />
     </Form.Item>
@@ -150,7 +144,6 @@
     <Form.Item
       name="chineseName"
       label="中文名"
-      rules={[{ required: true, message: "请输入资源中文名" }]}
     >
       <Input placeholder="请输入资源中文名称" />
     </Form.Item>
@@ -165,7 +158,6 @@
     <Form.Item
       name="year"
       label="年份"
-      rules={[{ required: true, message: "请选择年份" }]}
     >
       <DatePicker picker="year" placeholder="选择年份" />
     </Form.Item>
@@ -173,7 +165,6 @@
     <Form.Item
       name="region"
       label="地区"
-      rules={[{ required: true, message: "请选择地区" }]}
     >
       <Select placeholder="请选择地区">
         <Option value="china">中国大陆</Option>
@@ -194,7 +185,6 @@
     <Form.Item
       name="language"
       label="语言"
-      rules={[{ required: true, message: "请选择语言" }]}
     >
       <Select placeholder="请选择语言" mode="multiple">
         <Option value="chinese">中文</Option>
@@ -271,7 +261,6 @@
     <Form.Item
       name="movieType"
       label="电影类型"
-      rules={[{ required: true, message: "请选择电影类型" }]}
     >
       <Select placeholder="请选择电影类型" mode="multiple">
         <Option value="action">动作</Option>
@@ -295,7 +284,6 @@
     <Form.Item
       name="resolution"
       label="分辨率"
-      rules={[{ required: true, message: "请选择分辨率" }]}
     >
       <Select placeholder="请选择分辨率">
         <Option value="4K">4K</Option>
@@ -310,7 +298,6 @@
     <Form.Item
       name="source"
       label="片源"
-      rules={[{ required: true, message: "请选择片源" }]}
     >
       <Select placeholder="请选择片源">
         <Option value="bluray">蓝光原盘</Option>
@@ -784,10 +771,49 @@
 const UploadTorrentPage = () => {
   const [form] = Form.useForm();
   const [activeCategory, setActiveCategory] = useState("notice");
+  const { user } = useAuth();
 
-  const onFinish = (values) => {
-    console.log("提交的表单数据:", values);
-    message.success("种子上传成功!");
+  const onFinish = async (values) => {
+    try {
+      // 创建 FormData 对象
+      const formData = new FormData();
+      
+      // * 添加用户名
+      const username = user?.username || user?.uid;
+      if (!username) {
+        message.error('用户信息不完整,请重新登录');
+        return;
+      }
+      formData.append('username', username);
+      
+      // 添加描述信息
+      formData.append('description', values.description || '');
+      
+      // 添加种子文件
+      if (values.torrentFile && values.torrentFile.fileList && values.torrentFile.fileList.length > 0) {
+        const torrentFile = values.torrentFile.fileList[0].originFileObj;
+        formData.append('torrent', torrentFile);
+      } else {
+        message.error('请选择种子文件');
+        return;
+      }
+      
+      console.log('username', formData.get('username'));
+      console.log('description', formData.get('description'));
+      console.log('torrent', formData.get('torrent'));
+      // 调用上传接口
+      const response = await uploadTorrent(formData);
+      
+      if (response && response.message === "Resource published successfully") {
+        message.success('种子上传成功!');
+        form.resetFields(); // 重置表单
+      } else {
+        message.error('上传失败,请重试');
+      }
+    } catch (error) {
+      console.error('上传种子时出错:', error);
+      message.error('上传失败:' + (error.message || '网络错误,请重试'));
+    }
   };
 
   const renderFormByCategory = (category) => {
diff --git a/src/layouts/MainLayout.jsx b/src/layouts/MainLayout.jsx
index fc9a1d0..d63246f 100644
--- a/src/layouts/MainLayout.jsx
+++ b/src/layouts/MainLayout.jsx
@@ -28,7 +28,7 @@
 };
 
 const MainLayout = ({ children }) => {
-  const { user, logout } = useAuth();
+  const { user, logout, hasRole } = useAuth();
   const location = useLocation();
 
   // 根据当前路径获取对应的菜单key
@@ -89,7 +89,7 @@
   ];
 
   // 如果用户是管理员,添加管理面板菜单项
-  if (user?.role === "admin") {
+  if (hasRole("admin")) {
     menuItems.push({
       key: "7",
       icon: <SettingOutlined />,
diff --git a/src/services/api.js b/src/services/api.js
deleted file mode 100644
index 36ccab4..0000000
--- a/src/services/api.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import axios from 'axios';
-
-// 创建axios实例
-const api = axios.create({
-    baseURL: '/api',  // 使用相对路径,让Vite代理处理
-    timeout: 10000,
-    headers: {
-        'Content-Type': 'application/json',
-    }
-});
-
-// 请求拦截器
-api.interceptors.request.use(
-    config => {
-        // 从localStorage获取token
-        const token = localStorage.getItem('token');
-        if (token) {
-            config.headers.Authorization = `Bearer ${token}`;
-        }
-        return config;
-    },
-    error => {
-        return Promise.reject(error);
-    }
-);
-
-// 响应拦截器
-api.interceptors.response.use(
-    response => {
-        return response.data;
-    },
-    error => {
-        if (error.response) {
-            // 处理401未授权错误
-            if (error.response.status === 401) {
-                localStorage.removeItem('token');
-                window.location.href = '/login';
-            }
-        }
-        return Promise.reject(error);
-    }
-);
-
-export default api; 
\ No newline at end of file
diff --git a/src/utils/request.js b/src/utils/request.js
index a82602a..478b9fb 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -3,7 +3,7 @@
 
 // 创建 axios 实例
 const request = axios.create({
-  baseURL: "/api",
+  baseURL: "http://192.168.10.174:8080/api",
   timeout: 10000,
 });
 
diff --git a/vite.config.js b/vite.config.js
index 47bc9fd..6c73a38 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -24,15 +24,15 @@
       '@': resolve(__dirname, './src')
     }
   },
-  server: {
-    host: true,
-    port: 3000,
-    proxy: {
-      '/api': {
-        target: 'http://localhost:8080',  // 修改为本地后端地址
-        changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/api/, '/api')
-      }
-    }
-  }
+  // server: {
+  //   host: true,
+  //   port: 3000,
+  //   proxy: {
+  //     '/api': {
+  //       target: 'http://localhost:8080',  // 修改为本地后端地址
+  //       changeOrigin: true,
+  //       rewrite: (path) => path.replace(/^\/api/, '/api')
+  //     }
+  //   }
+  // }
 })