feat: 完成 Tracker 项目与 Torrent 种子模块的前后端接口与页面开发
- 实现 Tracker 项目、任务、任务日志、项目用户关联等模块的接口封装与 ProTable 页面
- 实现 Torrent 种子主表、文件列表、Tracker 服务器、标签模块的前后端接口封装
- 支持新增、编辑、删除、详情查看等完整 CRUD 功能
- 页面基于 Ant Design Pro,支持分页、筛选、Drawer + Modal 表单展示
Change-Id: If8ead64a0bf6c177545f1c3c348ee09cad221a85
diff --git a/react-ui/src/pages/Torrent/index.tsx b/react-ui/src/pages/Torrent/index.tsx
new file mode 100644
index 0000000..6990da7
--- /dev/null
+++ b/react-ui/src/pages/Torrent/index.tsx
@@ -0,0 +1,186 @@
+import React, { useRef, useState } from 'react';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
+import {
+ Button,
+ Modal,
+ message,
+ Drawer,
+ Form,
+ Input,
+ InputNumber,
+ DatePicker,
+ Card,
+ Layout,
+} from 'antd';
+import {
+ ProTable,
+ ActionType,
+ ProColumns,
+ ProDescriptions,
+ ProDescriptionsItemProps,
+} from '@ant-design/pro-components';
+import type { BtTorrent } from './data';
+import {
+ listBtTorrent,
+ getBtTorrent,
+ addBtTorrent,
+ updateBtTorrent,
+ removeBtTorrent,
+} from './service';
+
+const { Content } = Layout;
+
+const BtTorrentPage: React.FC = () => {
+ const actionRef = useRef<ActionType>();
+ const [form] = Form.useForm();
+ const [modalVisible, setModalVisible] = useState(false);
+ const [drawerVisible, setDrawerVisible] = useState(false);
+ const [current, setCurrent] = useState<Partial<BtTorrent>>({});
+
+ const columns: ProColumns<BtTorrent>[] = [
+ {
+ title: '种子ID',
+ dataIndex: 'torrentId',
+ hideInForm: true,
+ render: (dom, entity) => (
+ <a
+ onClick={async () => {
+ const res = await getBtTorrent(entity.torrentId!);
+ setCurrent(res);
+ setDrawerVisible(true);
+ }}
+ >
+ {dom}
+ </a>
+ ),
+ },
+ { title: '名称', dataIndex: 'name' },
+ { title: 'infoHash', dataIndex: 'infoHash' },
+ { title: '大小 (bytes)', dataIndex: 'length', valueType: 'digit' },
+ { title: '分片大小', dataIndex: 'pieceLength', valueType: 'digit' },
+ { title: '片段数', dataIndex: 'piecesCount', valueType: 'digit' },
+ { title: '创建工具', dataIndex: 'createdBy', hideInSearch: true },
+ { title: '上传时间', dataIndex: 'uploadTime', valueType: 'dateTime', hideInSearch: true },
+ {
+ title: '操作',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button key="view" type="link" icon={<EyeOutlined />} onClick={() => {
+ setCurrent(record);
+ setDrawerVisible(true);
+ }}>查看</Button>,
+ <Button key="edit" type="link" icon={<EditOutlined />} onClick={() => {
+ setCurrent(record);
+ form.setFieldsValue(record);
+ setModalVisible(true);
+ }}>编辑</Button>,
+ <Button key="delete" type="link" icon={<DeleteOutlined />} danger onClick={() => {
+ Modal.confirm({
+ title: '删除确认',
+ content: '确定删除该种子?',
+ onOk: async () => {
+ await removeBtTorrent([record.torrentId!]);
+ message.success('删除成功');
+ actionRef.current?.reload();
+ },
+ });
+ }}>删除</Button>,
+ ],
+ },
+ ];
+
+ const handleSubmit = async () => {
+ const values = await form.validateFields();
+ if (current?.torrentId) {
+ await updateBtTorrent({ ...current, ...values });
+ message.success('更新成功');
+ } else {
+ await addBtTorrent(values as BtTorrent);
+ message.success('新增成功');
+ }
+ setModalVisible(false);
+ form.resetFields();
+ actionRef.current?.reload();
+ };
+
+ return (
+ <Content>
+ <Card bordered={false}>
+ <ProTable<BtTorrent>
+ headerTitle="种子列表"
+ actionRef={actionRef}
+ rowKey="torrentId"
+ search={{ labelWidth: 100 }}
+ toolBarRender={() => [
+ <Button
+ key="add"
+ type="primary"
+ icon={<PlusOutlined />}
+ onClick={() => {
+ form.resetFields();
+ setCurrent({});
+ setModalVisible(true);
+ }}
+ >
+ 新增
+ </Button>,
+ ]}
+ request={async (params) => {
+ const res = await listBtTorrent(params);
+ return { data: res.rows || res.data || [], success: true };
+ }}
+ columns={columns}
+ />
+
+ {/* 编辑/新增弹窗 */}
+ <Modal
+ title={current?.torrentId ? '编辑种子' : '新增种子'}
+ open={modalVisible}
+ onOk={handleSubmit}
+ onCancel={() => setModalVisible(false)}
+ destroyOnClose
+ >
+ <Form form={form} layout="vertical">
+ <Form.Item name="name" label="名称" rules={[{ required: true }]}>
+ <Input />
+ </Form.Item>
+ <Form.Item name="infoHash" label="infoHash" rules={[{ required: true }]}>
+ <Input />
+ </Form.Item>
+ <Form.Item name="length" label="总大小 (bytes)" rules={[{ required: true }]}>
+ <InputNumber style={{ width: '100%' }} />
+ </Form.Item>
+ <Form.Item name="pieceLength" label="分片大小" rules={[{ required: true }]}>
+ <InputNumber style={{ width: '100%' }} />
+ </Form.Item>
+ <Form.Item name="piecesCount" label="片段数">
+ <InputNumber style={{ width: '100%' }} />
+ </Form.Item>
+ <Form.Item name="createdBy" label="创建工具">
+ <Input />
+ </Form.Item>
+ </Form>
+ </Modal>
+
+ {/* 详情抽屉 */}
+ <Drawer
+ width={500}
+ open={drawerVisible}
+ onClose={() => setDrawerVisible(false)}
+ title="种子详情"
+ >
+ {current && (
+ <ProDescriptions<BtTorrent>
+ column={1}
+ title={current.name}
+ request={async () => ({ data: current })}
+ columns={columns as ProDescriptionsItemProps<BtTorrent>[]}
+ />
+ )}
+ </Drawer>
+ </Card>
+ </Content>
+ );
+};
+
+export default BtTorrentPage;