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')
+ // }
+ // }
+ // }
})