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',
+ });
+}
diff --git a/react-ui/src/pages/Tracker/Projects/index.tsx b/react-ui/src/pages/Tracker/Projects/index.tsx
new file mode 100644
index 0000000..b5bf106
--- /dev/null
+++ b/react-ui/src/pages/Tracker/Projects/index.tsx
@@ -0,0 +1,203 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
+import {
+ Button,
+ message,
+ Drawer,
+ Modal,
+ Form,
+ Input,
+ Select,
+ DatePicker,
+ Card,
+ Layout,
+} from 'antd';
+import type { FormInstance } from 'antd';
+import {
+ ProTable,
+ ActionType,
+ ProColumns,
+ FooterToolbar,
+ ProDescriptions,
+ ProDescriptionsItemProps,
+} from '@ant-design/pro-components';
+import type { TrackerProject } from '../data.d.ts';
+import {
+ listTrackerProject,
+ getTrackerProject,
+ addTrackerProject,
+ updateTrackerProject,
+ removeTrackerProject,
+} from '../service';
+
+const { Content } = Layout;
+
+const TrackerProjectPage: React.FC = () => {
+ const actionRef = useRef<ActionType>();
+ const [detailVisible, setDetailVisible] = useState(false);
+ const [editVisible, setEditVisible] = useState(false);
+ const [current, setCurrent] = useState<TrackerProject>();
+ const [form] = Form.useForm();
+
+ const columns: ProColumns<TrackerProject>[] = [
+ {
+ title: '项目ID',
+ dataIndex: 'projectId',
+ render: (_, row) => (
+ <a
+ onClick={async () => {
+ const res = await getTrackerProject(row.projectId!);
+ setCurrent(res.data);
+ setDetailVisible(true);
+ }}
+ >
+ {row.projectId}
+ </a>
+ ),
+ },
+ { title: '项目名称', dataIndex: 'projectName' },
+ { title: '描述', dataIndex: 'description', hideInSearch: true },
+ { title: '状态', dataIndex: 'status', valueEnum: { active: { text: '激活' }, inactive: { text: '不活跃' } } },
+ { title: '创建者', dataIndex: 'createBy', hideInSearch: true },
+ { title: '创建时间', dataIndex: 'createTime', hideInSearch: true },
+ { title: '更新时间', dataIndex: 'updateTime', hideInSearch: true },
+ {
+ title: '操作',
+ valueType: 'option',
+ render: (_, row) => [
+ <Button
+ key="view"
+ type="link"
+ icon={<EyeOutlined />}
+ onClick={async () => {
+ const res = await getTrackerProject(row.projectId!);
+ setCurrent(res.data);
+ setDetailVisible(true);
+ }}
+ >
+ 查看
+ </Button>,
+ <Button
+ key="edit"
+ type="link"
+ icon={<EditOutlined />}
+ onClick={() => {
+ setCurrent(row);
+ form.setFieldsValue(row);
+ setEditVisible(true);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ key="delete"
+ type="link"
+ icon={<DeleteOutlined />}
+ danger
+ onClick={() => {
+ Modal.confirm({
+ title: '删除项目',
+ content: '确认删除该项目?',
+ onOk: async () => {
+ await removeTrackerProject([row.projectId!]);
+ message.success('删除成功');
+ actionRef.current?.reload();
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ /** 新增或更新 */
+ const handleSubmit = async (values: any) => {
+ if (current?.projectId) {
+ await updateTrackerProject({ ...current, ...values });
+ message.success('更新成功');
+ } else {
+ await addTrackerProject(values);
+ message.success('新增成功');
+ }
+ setEditVisible(false);
+ actionRef.current?.reload();
+ };
+
+ return (
+ <Content>
+ <Card bordered={false}>
+ <ProTable<TrackerProject>
+ headerTitle="项目列表"
+ actionRef={actionRef}
+ rowKey="projectId"
+ search={{ labelWidth: 100 }}
+ toolBarRender={() => [
+ <Button
+ key="add"
+ type="primary"
+ icon={<PlusOutlined />}
+ onClick={() => {
+ form.resetFields();
+ setCurrent(undefined);
+ setEditVisible(true);
+ }}
+ >
+ 新增
+ </Button>,
+ ]}
+ request={async (params) => {
+ const res = await listTrackerProject(params);
+ return { data: res.rows, success: true, total: res.total };
+ }}
+ columns={columns}
+ />
+
+ {/* 详情 Drawer */}
+ <Drawer
+ width={500}
+ open={detailVisible}
+ onClose={() => setDetailVisible(false)}
+ >
+ {current && (
+ <ProDescriptions<TrackerProject>
+ column={1}
+ title="项目详情"
+ dataSource={current}
+ columns={columns as ProDescriptionsItemProps<TrackerProject>[]}
+ />
+ )}
+ </Drawer>
+
+ {/* 新增/编辑 Modal */}
+ <Modal
+ title={current?.projectId ? '编辑项目' : '新增项目'}
+ open={editVisible}
+ onCancel={() => setEditVisible(false)}
+ onOk={() => {
+ form.validateFields().then(handleSubmit);
+ }}
+ >
+ <Form form={form} layout="vertical">
+ <Form.Item name="projectName" label="项目名称" rules={[{ required: true }]}>
+ <Input />
+ </Form.Item>
+ <Form.Item name="description" label="描述">
+ <Input.TextArea />
+ </Form.Item>
+ <Form.Item name="status" label="状态" initialValue="active">
+ <Select>
+ <Select.Option value="active">激活</Select.Option>
+ <Select.Option value="inactive">不活跃</Select.Option>
+ </Select>
+ </Form.Item>
+ {/* TODO: 补充其他字段 */}
+ </Form>
+ </Modal>
+ </Card>
+ </Content>
+ );
+};
+
+export default TrackerProjectPage;
diff --git a/react-ui/src/pages/Tracker/data.d.ts b/react-ui/src/pages/Tracker/data.d.ts
new file mode 100644
index 0000000..be71092
--- /dev/null
+++ b/react-ui/src/pages/Tracker/data.d.ts
@@ -0,0 +1,71 @@
+// data.d.ts
+
+/** 项目表 */
+export interface TrackerProject {
+ /** 项目ID */
+ projectId: number;
+ /** 项目名称 */
+ projectName: string;
+ /** 项目描述 */
+ description: string;
+ /** 项目状态(active: 激活, inactive: 不活跃) */
+ status: 'active' | 'inactive';
+ /** 创建者 */
+ createBy: string;
+ /** 创建时间 */
+ createTime: Date | null;
+ /** 更新者 */
+ updateBy: string;
+ /** 更新时间 */
+ updateTime: Date | null;
+}
+
+/** 项目与用户关联表 */
+export interface TrackerProjectUser {
+ /** 项目ID */
+ projectId: number;
+ /** 用户ID */
+ userId: number;
+ /** 角色(管理员、成员等) */
+ role: string;
+ /** 加入时间 */
+ createTime: Date;
+}
+
+/** 任务表 */
+export interface TrackerTask {
+ /** 任务ID */
+ taskId: number;
+ /** 所属项目ID */
+ projectId: number;
+ /** 任务名称 */
+ taskName: string;
+ /** 任务描述 */
+ description: string;
+ /** 分配给的用户ID */
+ assignedTo: number;
+ /** 任务状态(open: 待办, in_progress: 进行中, closed: 完成) */
+ status: 'open' | 'in_progress' | 'closed';
+ /** 任务优先级(low, medium, high) */
+ priority: 'low' | 'medium' | 'high';
+ /** 创建时间 */
+ createTime: Date | null;
+ /** 更新时间 */
+ updateTime: Date | null;
+}
+
+/** 任务日志表 */
+export interface TrackerTaskLog {
+ /** 日志ID */
+ logId: number;
+ /** 任务ID */
+ taskId: number;
+ /** 操作用户ID */
+ userId: number;
+ /** 操作类型(创建、更新、删除) */
+ action: '创建' | '更新' | '删除';
+ /** 操作描述 */
+ description: string;
+ /** 创建时间 */
+ createTime: Date | null;
+}
diff --git a/react-ui/src/pages/Tracker/service.ts b/react-ui/src/pages/Tracker/service.ts
new file mode 100644
index 0000000..c550cf4
--- /dev/null
+++ b/react-ui/src/pages/Tracker/service.ts
@@ -0,0 +1,199 @@
+import { request } from '@umijs/max';
+import type {
+ TrackerProject,
+ TrackerProjectUser,
+ TrackerTask,
+ TrackerTaskLog,
+} from '@/pages/Tracker/data';
+
+// ================================
+// 项目接口
+// ================================
+
+/** 查询项目分页列表 */
+export async function listTrackerProject(params?: TrackerProject) {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ return request(`/api/tracker/project/list${queryString}`, {
+ data: params,
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+/** 获取单个项目详情 */
+export async function getTrackerProject(projectId: number) {
+ return request<TrackerProject>(`/api/tracker/project/${projectId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增项目 */
+export async function addTrackerProject(data: TrackerProject) {
+ return request('/api/tracker/project', {
+ data,
+ method: 'post',
+ });
+}
+
+/** 更新项目 */
+export async function updateTrackerProject(data: TrackerProject) {
+ return request('/api/tracker/project', {
+ data,
+ method: 'put',
+ });
+}
+
+/** 删除项目(支持批量,传入 ID 数组) */
+export async function removeTrackerProject(projectIds: number[]) {
+ return request(`/api/tracker/project/${projectIds.join(',')}`, {
+ method: 'delete',
+ });
+}
+
+// ================================
+// 项目与用户关联接口
+// ================================
+
+/** 查询项目-用户关联分页列表 */
+export async function listTrackerProjectUser(params?: TrackerProjectUser) {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ return request(`/api/tracker/user/list${queryString}`, {
+ data: params,
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+/** 获取单个项目-用户关联详情 */
+export async function getTrackerProjectUser(projectId: number) {
+ return request<TrackerProjectUser>(`/api/tracker/user/${projectId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增项目-用户关联 */
+export async function addTrackerProjectUser(data: TrackerProjectUser) {
+ return request('/api/tracker/user', {
+ data,
+ method: 'post',
+ });
+}
+
+/** 更新项目-用户关联 */
+export async function updateTrackerProjectUser(data: TrackerProjectUser) {
+ return request('/api/tracker/user', {
+ data,
+ method: 'put',
+ });
+}
+
+/** 删除项目-用户关联(支持批量,传入项目 ID 数组) */
+export async function removeTrackerProjectUser(projectIds: number[]) {
+ return request(`/api/tracker/user/${projectIds.join(',')}`, {
+ method: 'delete',
+ });
+}
+
+// ================================
+// 任务接口
+// ================================
+
+/** 查询任务分页列表 */
+export async function listTrackerTask(params?: TrackerTask) {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ return request(`/api/tracker/task/list${queryString}`, {
+ data: params,
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+/** 获取单个任务详情 */
+export async function getTrackerTask(taskId: number) {
+ return request<TrackerTask>(`/api/tracker/task/${taskId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增任务 */
+export async function addTrackerTask(data: TrackerTask) {
+ return request('/api/tracker/task', {
+ data,
+ method: 'post',
+ });
+}
+
+/** 更新任务 */
+export async function updateTrackerTask(data: TrackerTask) {
+ return request('/api/tracker/task', {
+ data,
+ method: 'put',
+ });
+}
+
+/** 删除任务(支持批量,传入任务 ID 数组) */
+export async function removeTrackerTask(taskIds: number[]) {
+ return request(`/api/tracker/task/${taskIds.join(',')}`, {
+ method: 'delete',
+ });
+}
+
+// ================================
+// 任务日志接口
+// ================================
+
+/** 查询任务日志分页列表 */
+export async function listTrackerTaskLog(params?: TrackerTaskLog) {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ return request(`/api/tracker/log/list${queryString}`, {
+ data: params,
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+/** 获取单个任务日志详情 */
+export async function getTrackerTaskLog(logId: number) {
+ return request<TrackerTaskLog>(`/api/tracker/log/${logId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增任务日志 */
+export async function addTrackerTaskLog(data: TrackerTaskLog) {
+ return request('/api/tracker/log', {
+ data,
+ method: 'post',
+ });
+}
+
+/** 更新任务日志 */
+export async function updateTrackerTaskLog(data: TrackerTaskLog) {
+ return request('/api/tracker/log', {
+ data,
+ method: 'put',
+ });
+}
+
+/** 删除任务日志(支持批量,传入日志 ID 数组) */
+export async function removeTrackerTaskLog(logIds: number[]) {
+ return request(`/api/tracker/log/${logIds.join(',')}`, {
+ method: 'delete',
+ });
+}