feat: 完成 Tracker 项目与 Torrent 种子模块的前后端接口与页面开发

- 实现 Tracker 项目、任务、任务日志、项目用户关联等模块的接口封装与 ProTable 页面
- 实现 Torrent 种子主表、文件列表、Tracker 服务器、标签模块的前后端接口封装
- 支持新增、编辑、删除、详情查看等完整 CRUD 功能
- 页面基于 Ant Design Pro,支持分页、筛选、Drawer + Modal 表单展示

Change-Id: If8ead64a0bf6c177545f1c3c348ee09cad221a85
diff --git a/react-ui/src/pages/Torrent/data.d.ts b/react-ui/src/pages/Torrent/data.d.ts
new file mode 100644
index 0000000..e16c819
--- /dev/null
+++ b/react-ui/src/pages/Torrent/data.d.ts
@@ -0,0 +1,59 @@
+
+
+/** 种子主表 */
+export interface BtTorrent {
+  /** 种子ID */
+  torrentId: number;
+  /** InfoHash */
+  infoHash: string;
+  /** 种子名称 */
+  name: string;
+  /** 总大小(字节) */
+  length: number;
+  /** 每个 piece 的长度 */
+  pieceLength: number;
+  /** piece 总数 */
+  piecesCount: number;
+  /** 创建工具(如 qBittorrent) */
+  createdBy: string;
+  /** .torrent 内的创建时间 */
+  torrentCreateTime: Date | null;
+  /** 上传用户ID(sys_user.userId) */
+  uploaderId: number;
+  /** 上传时间 */
+  uploadTime: Date;
+  /** 种子文件存储路径(服务器端路径或 URL) */
+  filePath: string;
+}
+
+/** 种子文件列表 */
+export interface BtTorrentFile {
+  /** 文件记录ID */
+  id: number;
+  /** 种子ID */
+  torrentId: number;
+  /** 文件路径 */
+  filePath: string;
+  /** 文件大小 */
+  fileSize: number;
+}
+
+/** announce 列表 */
+export interface BtTorrentAnnounce {
+  /** ID */
+  id: number;
+  /** 种子ID */
+  torrentId: number;
+  /** Tracker URL */
+  announceUrl: string;
+}
+
+/** 标签表(可选) */
+export interface BtTorrentTag {
+  /** ID */
+  id: number;
+  /** 种子ID */
+  torrentId: number;
+  /** 标签内容 */
+  tag: string;
+}
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;
diff --git a/react-ui/src/pages/Torrent/service.ts b/react-ui/src/pages/Torrent/service.ts
new file mode 100644
index 0000000..c1d8e26
--- /dev/null
+++ b/react-ui/src/pages/Torrent/service.ts
@@ -0,0 +1,175 @@
+import { request } from '@umijs/max';
+import type {
+  BtTorrent,
+  BtTorrentFile,
+  BtTorrentAnnounce,
+  BtTorrentTag,
+} from '@/pages/Torrent/data'; // 假设你把 data.d.ts 放这里
+
+// ================================
+// 种子主表(bt_torrent)接口
+// ================================
+
+/** 查询种子列表 */
+export async function listBtTorrent(params?: Partial<BtTorrent>) {
+  const queryString = params ? `?${new URLSearchParams(params as any)}` : '';
+  return request(`/api/system/torrent/list${queryString}`, {
+    method: 'get',
+  });
+}
+
+/** 获取种子详情 */
+export async function getBtTorrent(torrentId: number) {
+  return request<BtTorrent>(`/api/system/torrent/${torrentId}`, {
+    method: 'get',
+  });
+}
+
+/** 新增种子 */
+export async function addBtTorrent(data: BtTorrent) {
+  return request('/api/system/torrent', {
+    method: 'post',
+    data,
+  });
+}
+
+/** 修改种子 */
+export async function updateBtTorrent(data: BtTorrent) {
+  return request('/api/system/torrent', {
+    method: 'put',
+    data,
+  });
+}
+
+/** 删除种子(支持批量) */
+export async function removeBtTorrent(torrentIds: number[]) {
+  return request(`/api/system/torrent/${torrentIds.join(',')}`, {
+    method: 'delete',
+  });
+}
+
+// ================================
+// 种子文件(bt_torrent_file)接口
+// ================================
+
+/** 查询种子文件列表 */
+export async function listBtTorrentFile(params?: Partial<BtTorrentFile>) {
+  const queryString = params ? `?${new URLSearchParams(params as any)}` : '';
+  return request(`/api/system/file/list${queryString}`, {
+    method: 'get',
+  });
+}
+
+/** 获取文件详情 */
+export async function getBtTorrentFile(id: number) {
+  return request<BtTorrentFile>(`/api/system/file/${id}`, {
+    method: 'get',
+  });
+}
+
+/** 新增文件 */
+export async function addBtTorrentFile(data: BtTorrentFile) {
+  return request('/api/system/file', {
+    method: 'post',
+    data,
+  });
+}
+
+/** 修改文件 */
+export async function updateBtTorrentFile(data: BtTorrentFile) {
+  return request('/api/system/file', {
+    method: 'put',
+    data,
+  });
+}
+
+/** 删除文件(支持批量) */
+export async function removeBtTorrentFile(ids: number[]) {
+  return request(`/api/system/file/${ids.join(',')}`, {
+    method: 'delete',
+  });
+}
+
+// ================================
+// Tracker 列表(bt_torrent_announce)接口
+// ================================
+
+/** 查询 Tracker 列表 */
+export async function listBtTorrentAnnounce(params?: Partial<BtTorrentAnnounce>) {
+  const queryString = params ? `?${new URLSearchParams(params as any)}` : '';
+  return request(`/api/system/announce/list${queryString}`, {
+    method: 'get',
+  });
+}
+
+/** 获取单个 Tracker */
+export async function getBtTorrentAnnounce(id: number) {
+  return request<BtTorrentAnnounce>(`/api/system/announce/${id}`, {
+    method: 'get',
+  });
+}
+
+/** 新增 Tracker */
+export async function addBtTorrentAnnounce(data: BtTorrentAnnounce) {
+  return request('/api/system/announce', {
+    method: 'post',
+    data,
+  });
+}
+
+/** 修改 Tracker */
+export async function updateBtTorrentAnnounce(data: BtTorrentAnnounce) {
+  return request('/api/system/announce', {
+    method: 'put',
+    data,
+  });
+}
+
+/** 删除 Tracker(支持批量) */
+export async function removeBtTorrentAnnounce(ids: number[]) {
+  return request(`/api/system/announce/${ids.join(',')}`, {
+    method: 'delete',
+  });
+}
+
+// ================================
+// 种子标签(bt_torrent_tags)接口
+// ================================
+
+/** 查询标签列表 */
+export async function listBtTorrentTags(params?: Partial<BtTorrentTag>) {
+  const queryString = params ? `?${new URLSearchParams(params as any)}` : '';
+  return request(`/api/system/tags/list${queryString}`, {
+    method: 'get',
+  });
+}
+
+/** 获取标签详情 */
+export async function getBtTorrentTag(id: number) {
+  return request<BtTorrentTag>(`/api/system/tags/${id}`, {
+    method: 'get',
+  });
+}
+
+/** 新增标签 */
+export async function addBtTorrentTag(data: BtTorrentTag) {
+  return request('/api/system/tags', {
+    method: 'post',
+    data,
+  });
+}
+
+/** 修改标签 */
+export async function updateBtTorrentTag(data: BtTorrentTag) {
+  return request('/api/system/tags', {
+    method: 'put',
+    data,
+  });
+}
+
+/** 删除标签(支持批量) */
+export async function removeBtTorrentTag(ids: number[]) {
+  return request(`/api/system/tags/${ids.join(',')}`, {
+    method: 'delete',
+  });
+}