You've already forked DataMate
refactor(annotation): 移除自动标注功能模块并简化创建对话框
- 删除 AutoAnnotation 相关的所有组件和页面文件 - 从 CreateAnnotationTaskDialog 中移除自动标注相关的表单和逻辑 - 简化 CreateAnnotationTaskDialog 为仅支持手动标注模式 - 移除 COCO_CLASSES 常量和相关依赖项 - 清理无用的导入和状态变量 - 更新对话框布局以适应单一标注模式
This commit is contained in:
@@ -1,302 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Table, message, Modal, Tag, Progress, Space, Tooltip } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnType } from "antd/es/table";
|
||||
import type { AutoAnnotationTask, AutoAnnotationStatus } from "../annotation.model";
|
||||
import {
|
||||
queryAutoAnnotationTasksUsingGet,
|
||||
deleteAutoAnnotationTaskByIdUsingDelete,
|
||||
downloadAutoAnnotationResultUsingGet,
|
||||
} from "../annotation.api";
|
||||
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
|
||||
|
||||
const STATUS_COLORS: Record<AutoAnnotationStatus, string> = {
|
||||
pending: "default",
|
||||
running: "processing",
|
||||
completed: "success",
|
||||
failed: "error",
|
||||
cancelled: "default",
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<AutoAnnotationStatus, string> = {
|
||||
pending: "等待中",
|
||||
running: "处理中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
|
||||
const MODEL_SIZE_LABELS: Record<string, string> = {
|
||||
n: "YOLOv8n (最快)",
|
||||
s: "YOLOv8s",
|
||||
m: "YOLOv8m",
|
||||
l: "YOLOv8l (推荐)",
|
||||
x: "YOLOv8x (最精确)",
|
||||
};
|
||||
|
||||
export default function AutoAnnotation() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tasks, setTasks] = useState<AutoAnnotationTask[]>([]);
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
const interval = setInterval(() => {
|
||||
fetchTasks(true);
|
||||
}, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const fetchTasks = async (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const response = await queryAutoAnnotationTasksUsingGet();
|
||||
setTasks(response.data || response || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch auto annotation tasks:", error);
|
||||
if (!silent) message.error("获取任务列表失败");
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (task: AutoAnnotationTask) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除自动标注任务「${task.name}」吗?`,
|
||||
content: "删除任务后,已生成的标注结果不会被删除。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAutoAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("任务删除成功");
|
||||
fetchTasks();
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async (task: AutoAnnotationTask) => {
|
||||
try {
|
||||
message.loading("正在准备下载...", 0);
|
||||
await downloadAutoAnnotationResultUsingGet(task.id);
|
||||
message.destroy();
|
||||
message.success("下载已开始");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.destroy();
|
||||
message.error("下载失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewResult = (task: AutoAnnotationTask) => {
|
||||
if (task.outputPath) {
|
||||
Modal.info({
|
||||
title: "标注结果路径",
|
||||
content: (
|
||||
<div>
|
||||
<p>输出路径:{task.outputPath}</p>
|
||||
<p>检测对象数:{task.detectedObjects}</p>
|
||||
<p>
|
||||
处理图片数:{task.processedImages} / {task.totalImages}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnType<AutoAnnotationTask>[] = [
|
||||
{ title: "任务名称", dataIndex: "name", key: "name", width: 200 },
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
width: 220,
|
||||
render: (_: any, record: AutoAnnotationTask) => {
|
||||
const list =
|
||||
record.sourceDatasets && record.sourceDatasets.length > 0
|
||||
? record.sourceDatasets
|
||||
: record.datasetName
|
||||
? [record.datasetName]
|
||||
: [];
|
||||
|
||||
if (list.length === 0) return "-";
|
||||
|
||||
const text = list.join(",");
|
||||
return (
|
||||
<Tooltip title={text}>
|
||||
<span>{text}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "模型",
|
||||
dataIndex: ["config", "modelSize"],
|
||||
key: "modelSize",
|
||||
width: 120,
|
||||
render: (size: string) => MODEL_SIZE_LABELS[size] || size,
|
||||
},
|
||||
{
|
||||
title: "置信度",
|
||||
dataIndex: ["config", "confThreshold"],
|
||||
key: "confThreshold",
|
||||
width: 100,
|
||||
render: (threshold: number) => `${(threshold * 100).toFixed(0)}%`,
|
||||
},
|
||||
{
|
||||
title: "目标类别",
|
||||
dataIndex: ["config", "targetClasses"],
|
||||
key: "targetClasses",
|
||||
width: 120,
|
||||
render: (classes: number[]) => (
|
||||
<Tooltip
|
||||
title={classes.length > 0 ? classes.join(", ") : "全部类别"}
|
||||
>
|
||||
<span>
|
||||
{classes.length > 0
|
||||
? `${classes.length} 个类别`
|
||||
: "全部类别"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
render: (status: AutoAnnotationStatus) => (
|
||||
<Tag color={STATUS_COLORS[status]}>{STATUS_LABELS[status]}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
width: 150,
|
||||
render: (progress: number, record: AutoAnnotationTask) => (
|
||||
<div>
|
||||
<Progress percent={progress} size="small" />
|
||||
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||
{record.processedImages} / {record.totalImages}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "检测对象数",
|
||||
dataIndex: "detectedObjects",
|
||||
key: "detectedObjects",
|
||||
width: 100,
|
||||
render: (count: number) => count.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 150,
|
||||
render: (time: string) => new Date(time).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
render: (_: any, record: AutoAnnotationTask) => (
|
||||
<Space size="small">
|
||||
{record.status === "completed" && (
|
||||
<>
|
||||
<Tooltip title="查看结果">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewResult(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="下载结果">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleDownload(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title="自动标注任务"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={loading}
|
||||
onClick={() => fetchTasks()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
||||
}}
|
||||
pagination={{ pageSize: 10 }}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<CreateAutoAnnotationDialog
|
||||
visible={showCreateDialog}
|
||||
onCancel={() => setShowCreateDialog(false)}
|
||||
onSuccess={() => {
|
||||
setShowCreateDialog(false);
|
||||
fetchTasks();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Form, Input, Select, Slider, message, Checkbox } from "antd";
|
||||
import { createAutoAnnotationTaskUsingPost } from "../../annotation.api";
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { DatasetType, type DatasetFile, type Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface CreateAutoAnnotationDialogProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const COCO_CLASSES = [
|
||||
{ id: 0, name: "person", label: "人" },
|
||||
{ id: 1, name: "bicycle", label: "自行车" },
|
||||
{ id: 2, name: "car", label: "汽车" },
|
||||
{ id: 3, name: "motorcycle", label: "摩托车" },
|
||||
{ id: 4, name: "airplane", label: "飞机" },
|
||||
{ id: 5, name: "bus", label: "公交车" },
|
||||
{ id: 6, name: "train", label: "火车" },
|
||||
{ id: 7, name: "truck", label: "卡车" },
|
||||
{ id: 8, name: "boat", label: "船" },
|
||||
{ id: 9, name: "traffic light", label: "红绿灯" },
|
||||
{ id: 10, name: "fire hydrant", label: "消防栓" },
|
||||
{ id: 11, name: "stop sign", label: "停止标志" },
|
||||
{ id: 12, name: "parking meter", label: "停车计时器" },
|
||||
{ id: 13, name: "bench", label: "长椅" },
|
||||
{ id: 14, name: "bird", label: "鸟" },
|
||||
{ id: 15, name: "cat", label: "猫" },
|
||||
{ id: 16, name: "dog", label: "狗" },
|
||||
{ id: 17, name: "horse", label: "马" },
|
||||
{ id: 18, name: "sheep", label: "羊" },
|
||||
{ id: 19, name: "cow", label: "牛" },
|
||||
{ id: 20, name: "elephant", label: "大象" },
|
||||
{ id: 21, name: "bear", label: "熊" },
|
||||
{ id: 22, name: "zebra", label: "斑马" },
|
||||
{ id: 23, name: "giraffe", label: "长颈鹿" },
|
||||
{ id: 24, name: "backpack", label: "背包" },
|
||||
{ id: 25, name: "umbrella", label: "雨伞" },
|
||||
{ id: 26, name: "handbag", label: "手提包" },
|
||||
{ id: 27, name: "tie", label: "领带" },
|
||||
{ id: 28, name: "suitcase", label: "行李箱" },
|
||||
{ id: 29, name: "frisbee", label: "飞盘" },
|
||||
{ id: 30, name: "skis", label: "滑雪板" },
|
||||
{ id: 31, name: "snowboard", label: "滑雪板" },
|
||||
{ id: 32, name: "sports ball", label: "球类" },
|
||||
{ id: 33, name: "kite", label: "风筝" },
|
||||
{ id: 34, name: "baseball bat", label: "棒球棒" },
|
||||
{ id: 35, name: "baseball glove", label: "棒球手套" },
|
||||
{ id: 36, name: "skateboard", label: "滑板" },
|
||||
{ id: 37, name: "surfboard", label: "冲浪板" },
|
||||
{ id: 38, name: "tennis racket", label: "网球拍" },
|
||||
{ id: 39, name: "bottle", label: "瓶子" },
|
||||
{ id: 40, name: "wine glass", label: "酒杯" },
|
||||
{ id: 41, name: "cup", label: "杯子" },
|
||||
{ id: 42, name: "fork", label: "叉子" },
|
||||
{ id: 43, name: "knife", label: "刀" },
|
||||
{ id: 44, name: "spoon", label: "勺子" },
|
||||
{ id: 45, name: "bowl", label: "碗" },
|
||||
{ id: 46, name: "banana", label: "香蕉" },
|
||||
{ id: 47, name: "apple", label: "苹果" },
|
||||
{ id: 48, name: "sandwich", label: "三明治" },
|
||||
{ id: 49, name: "orange", label: "橙子" },
|
||||
{ id: 50, name: "broccoli", label: "西兰花" },
|
||||
{ id: 51, name: "carrot", label: "胡萝卜" },
|
||||
{ id: 52, name: "hot dog", label: "热狗" },
|
||||
{ id: 53, name: "pizza", label: "披萨" },
|
||||
{ id: 54, name: "donut", label: "甜甜圈" },
|
||||
{ id: 55, name: "cake", label: "蛋糕" },
|
||||
{ id: 56, name: "chair", label: "椅子" },
|
||||
{ id: 57, name: "couch", label: "沙发" },
|
||||
{ id: 58, name: "potted plant", label: "盆栽" },
|
||||
{ id: 59, name: "bed", label: "床" },
|
||||
{ id: 60, name: "dining table", label: "餐桌" },
|
||||
{ id: 61, name: "toilet", label: "马桶" },
|
||||
{ id: 62, name: "tv", label: "电视" },
|
||||
{ id: 63, name: "laptop", label: "笔记本电脑" },
|
||||
{ id: 64, name: "mouse", label: "鼠标" },
|
||||
{ id: 65, name: "remote", label: "遥控器" },
|
||||
{ id: 66, name: "keyboard", label: "键盘" },
|
||||
{ id: 67, name: "cell phone", label: "手机" },
|
||||
{ id: 68, name: "microwave", label: "微波炉" },
|
||||
{ id: 69, name: "oven", label: "烤箱" },
|
||||
{ id: 70, name: "toaster", label: "烤面包机" },
|
||||
{ id: 71, name: "sink", label: "水槽" },
|
||||
{ id: 72, name: "refrigerator", label: "冰箱" },
|
||||
{ id: 73, name: "book", label: "书" },
|
||||
{ id: 74, name: "clock", label: "钟表" },
|
||||
{ id: 75, name: "vase", label: "花瓶" },
|
||||
{ id: 76, name: "scissors", label: "剪刀" },
|
||||
{ id: 77, name: "teddy bear", label: "玩具熊" },
|
||||
{ id: 78, name: "hair drier", label: "吹风机" },
|
||||
{ id: 79, name: "toothbrush", label: "牙刷" },
|
||||
];
|
||||
|
||||
export default function CreateAutoAnnotationDialog({
|
||||
visible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: CreateAutoAnnotationDialogProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [datasets, setDatasets] = useState<any[]>([]);
|
||||
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
||||
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
|
||||
const [imageFileCount, setImageFileCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchDatasets();
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
modelSize: "l",
|
||||
confThreshold: 0.7,
|
||||
targetClasses: [],
|
||||
});
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
try {
|
||||
const { data } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
const imageDatasets = (data.content || [])
|
||||
.map(mapDataset)
|
||||
.filter((ds: any) => ds.datasetType === DatasetType.IMAGE);
|
||||
setDatasets(imageDatasets);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch datasets:", error);
|
||||
message.error("获取数据集列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
||||
const count = Object.values(selectedFilesMap).filter((file) => {
|
||||
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
||||
return imageExtensions.includes(ext);
|
||||
}).length;
|
||||
setImageFileCount(count);
|
||||
}, [selectedFilesMap]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (imageFileCount === 0) {
|
||||
message.error("请至少选择一个图像文件");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
||||
const imageFileIds = Object.values(selectedFilesMap)
|
||||
.filter((file) => {
|
||||
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
||||
return imageExtensions.includes(ext);
|
||||
})
|
||||
.map((file) => file.id);
|
||||
|
||||
const payload = {
|
||||
name: values.name,
|
||||
datasetId: values.datasetId,
|
||||
fileIds: imageFileIds,
|
||||
config: {
|
||||
modelSize: values.modelSize,
|
||||
confThreshold: values.confThreshold,
|
||||
targetClasses: selectAllClasses ? [] : values.targetClasses || [],
|
||||
outputDatasetName: values.outputDatasetName || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await createAutoAnnotationTaskUsingPost(payload);
|
||||
message.success("自动标注任务创建成功");
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return;
|
||||
console.error("Failed to create auto annotation task:", error);
|
||||
message.error(error.message || "创建任务失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClassSelectionChange = (checked: boolean) => {
|
||||
setSelectAllClasses(checked);
|
||||
if (checked) {
|
||||
form.setFieldsValue({ targetClasses: [] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="创建自动标注任务"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="任务名称"
|
||||
rules={[
|
||||
{ required: true, message: "请输入任务名称" },
|
||||
{ max: 100, message: "任务名称不能超过100个字符" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="选择数据集和图像文件" required>
|
||||
<DatasetFileTransfer
|
||||
open
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
onDatasetSelect={(dataset) => {
|
||||
setSelectedDataset(dataset);
|
||||
form.setFieldsValue({ datasetId: dataset?.id ?? "" });
|
||||
}}
|
||||
datasetTypeFilter={DatasetType.IMAGE}
|
||||
/>
|
||||
{selectedDataset && (
|
||||
<div className="mt-2 p-2 bg-blue-50 rounded border border-blue-200 text-xs">
|
||||
当前数据集:<span className="font-medium">{selectedDataset.name}</span> - 已选择
|
||||
<span className="font-medium text-blue-600"> {imageFileCount} </span>个图像文件
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item hidden name="datasetId" rules={[{ required: true, message: "请选择数据集" }]}>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="modelSize" label="模型规模" rules={[{ required: true, message: "请选择模型规模" }]}>
|
||||
<Select>
|
||||
<Option value="n">YOLOv8n (最快)</Option>
|
||||
<Option value="s">YOLOv8s</Option>
|
||||
<Option value="m">YOLOv8m</Option>
|
||||
<Option value="l">YOLOv8l (推荐)</Option>
|
||||
<Option value="x">YOLOv8x (最精确)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confThreshold"
|
||||
label="置信度阈值"
|
||||
rules={[{ required: true, message: "请选择置信度阈值" }]}
|
||||
>
|
||||
<Slider min={0.1} max={0.9} step={0.05} tooltip={{ formatter: (v) => `${(v || 0) * 100}%` }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="目标类别">
|
||||
<Checkbox checked={selectAllClasses} onChange={(e) => handleClassSelectionChange(e.target.checked)}>
|
||||
选中所有类别
|
||||
</Checkbox>
|
||||
{!selectAllClasses && (
|
||||
<Form.Item name="targetClasses" noStyle>
|
||||
<Select mode="multiple" placeholder="选择目标类别" style={{ marginTop: 8 }}>
|
||||
{COCO_CLASSES.map((cls) => (
|
||||
<Option key={cls.id} value={cls.id}>
|
||||
{cls.label} ({cls.name})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="outputDatasetName" label="输出数据集名称 (可选)">
|
||||
<Input placeholder="留空则将结果写入原数据集的标签中" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./AutoAnnotation";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,501 +1,290 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Table, message, Modal, Tabs, Tag, Progress, Tooltip } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { AnnotationTask } from "../annotation.model";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
deleteAnnotationTaskByIdUsingDelete,
|
||||
queryAnnotationTasksUsingGet,
|
||||
queryAutoAnnotationTasksUsingGet,
|
||||
deleteAutoAnnotationTaskByIdUsingDelete,
|
||||
} from "../annotation.api";
|
||||
import { mapAnnotationTask } from "../annotation.const";
|
||||
import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog";
|
||||
import ExportAnnotationDialog from "./ExportAnnotationDialog";
|
||||
import { ColumnType } from "antd/es/table";
|
||||
import { TemplateList } from "../Template";
|
||||
// Note: DevelopmentInProgress intentionally not used here
|
||||
|
||||
const AUTO_STATUS_LABELS: Record<string, string> = {
|
||||
pending: "等待中",
|
||||
running: "处理中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
|
||||
const AUTO_MODEL_SIZE_LABELS: Record<string, string> = {
|
||||
n: "YOLOv8n (最快)",
|
||||
s: "YOLOv8s",
|
||||
m: "YOLOv8m",
|
||||
l: "YOLOv8l (推荐)",
|
||||
x: "YOLOv8x (最精确)",
|
||||
};
|
||||
|
||||
export default function DataAnnotation() {
|
||||
// return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("tasks");
|
||||
const [viewMode, setViewMode] = useState<"list" | "card">("list");
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [exportTask, setExportTask] = useState<AnnotationTask | null>(null);
|
||||
const [autoTasks, setAutoTasks] = useState<any[]>([]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
|
||||
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
|
||||
// 拉取自动标注任务(供轮询和创建成功后立即刷新复用)
|
||||
const refreshAutoTasks = async (silent = false) => {
|
||||
try {
|
||||
const response = await queryAutoAnnotationTasksUsingGet();
|
||||
const tasks = (response as any)?.data || response || [];
|
||||
if (Array.isArray(tasks)) {
|
||||
setAutoTasks(tasks);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch auto annotation tasks:", error);
|
||||
if (!silent) {
|
||||
message.error("获取自动标注任务失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 自动标注任务轮询(用于在同一表格中展示处理进度)
|
||||
useEffect(() => {
|
||||
refreshAutoTasks();
|
||||
const timer = setInterval(() => refreshAutoTasks(true), 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAnnotate = (task: AnnotationTask) => {
|
||||
const projectId = (task as any)?.id;
|
||||
if (!projectId) {
|
||||
message.error("无法进入标注:缺少标注项目ID");
|
||||
return;
|
||||
}
|
||||
navigate(`/data/annotation/annotate/${projectId}`);
|
||||
};
|
||||
|
||||
const handleExport = (task: AnnotationTask) => {
|
||||
setExportTask(task);
|
||||
};
|
||||
|
||||
const handleDelete = (task: AnnotationTask) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除标注任务「${task.name}」吗?`,
|
||||
content: "删除标注任务不会删除对应数据集,但会删除该任务的所有标注结果。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("映射删除成功");
|
||||
fetchData();
|
||||
// clear selection if deleted item was selected
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAuto = (task: any) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除自动标注任务「${task.name}」吗?`,
|
||||
content: <div>删除任务后,已生成的标注结果不会被删除。</div>,
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAutoAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("自动标注任务删除成功");
|
||||
// 重新拉取自动标注任务
|
||||
setAutoTasks((prev) => prev.filter((t: any) => t.id !== task.id));
|
||||
// 清理选中
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (!selectedRows || selectedRows.length === 0) return;
|
||||
const manualRows = selectedRows.filter((r) => r._kind !== "auto");
|
||||
const autoRows = selectedRows.filter((r) => r._kind === "auto");
|
||||
Modal.confirm({
|
||||
title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`,
|
||||
content: "删除标注任务不会删除对应数据集,但会删除这些任务的所有标注结果。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
[
|
||||
...manualRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id)),
|
||||
...autoRows.map((r) => deleteAutoAnnotationTaskByIdUsingDelete(r.id)),
|
||||
]
|
||||
);
|
||||
message.success("批量删除已完成");
|
||||
fetchData();
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRows([]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("批量删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "annotate",
|
||||
label: "标注",
|
||||
icon: (
|
||||
<EditOutlined
|
||||
className="w-4 h-4 text-green-400"
|
||||
style={{ color: "#52c41a" }}
|
||||
/>
|
||||
),
|
||||
onClick: handleAnnotate,
|
||||
},
|
||||
{
|
||||
key: "export",
|
||||
label: "导出",
|
||||
icon: <DownloadOutlined className="w-4 h-4" style={{ color: "#1890ff" }} />,
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
// 合并手动标注任务与自动标注任务
|
||||
const mergedTableData = [
|
||||
// 手动标注任务
|
||||
...tableData.map((task) => ({
|
||||
...task,
|
||||
_kind: "manual" as const,
|
||||
})),
|
||||
// 自动标注任务
|
||||
...autoTasks.map((task: any) => {
|
||||
const sourceList = Array.isArray(task.sourceDatasets)
|
||||
? task.sourceDatasets
|
||||
: task.datasetName
|
||||
? [task.datasetName]
|
||||
: [];
|
||||
const datasetName = sourceList.length > 0 ? sourceList.join(",") : "-";
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
datasetName,
|
||||
createdAt: task.createdAt || "-",
|
||||
updatedAt: task.updatedAt || "-",
|
||||
_kind: "auto" as const,
|
||||
autoStatus: task.status,
|
||||
autoProgress: task.progress,
|
||||
autoProcessedImages: task.processedImages,
|
||||
autoTotalImages: task.totalImages,
|
||||
autoDetectedObjects: task.detectedObjects,
|
||||
autoConfig: task.config || {},
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
key: "kind",
|
||||
width: 100,
|
||||
render: (_: any, record: any) =>
|
||||
record._kind === "auto" ? "自动标注" : "手动标注",
|
||||
},
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "模型",
|
||||
key: "modelSize",
|
||||
width: 160,
|
||||
render: (_: any, record: any) => {
|
||||
if (record._kind !== "auto") return "-";
|
||||
const size = record.autoConfig?.modelSize;
|
||||
return AUTO_MODEL_SIZE_LABELS[size] || size || "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "置信度",
|
||||
key: "confThreshold",
|
||||
width: 120,
|
||||
render: (_: any, record: any) => {
|
||||
if (record._kind !== "auto") return "-";
|
||||
const threshold = record.autoConfig?.confThreshold;
|
||||
if (typeof threshold !== "number") return "-";
|
||||
return `${(threshold * 100).toFixed(0)}%`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "目标类别",
|
||||
key: "targetClasses",
|
||||
width: 160,
|
||||
render: (_: any, record: any) => {
|
||||
if (record._kind !== "auto") return "-";
|
||||
const classes: number[] = record.autoConfig?.targetClasses || [];
|
||||
if (!classes.length) return "全部类别";
|
||||
const text = classes.join(", ");
|
||||
return (
|
||||
<Tooltip title={text}>
|
||||
<span>{`${classes.length} 个类别`}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "自动标注状态",
|
||||
key: "autoStatus",
|
||||
width: 130,
|
||||
render: (_: any, record: any) => {
|
||||
if (record._kind !== "auto") return "-";
|
||||
const status = record.autoStatus as string;
|
||||
const label = AUTO_STATUS_LABELS[status] || status || "-";
|
||||
return <Tag>{label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "自动标注进度",
|
||||
key: "autoProgress",
|
||||
width: 200,
|
||||
render: (_: any, record: any) => {
|
||||
if (record._kind !== "auto") return "-";
|
||||
const progress = typeof record.autoProgress === "number" ? record.autoProgress : 0;
|
||||
const processed = record.autoProcessedImages ?? 0;
|
||||
const total = record.autoTotalImages ?? 0;
|
||||
return (
|
||||
<div>
|
||||
<Progress percent={progress} size="small" />
|
||||
<div style={{ fontSize: 12, color: "#999" }}>
|
||||
{processed} / {total}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "检测对象数",
|
||||
key: "detectedObjects",
|
||||
width: 120,
|
||||
render: (_: any, record: any) => {
|
||||
if (record._kind !== "auto") return "-";
|
||||
const count = record.autoDetectedObjects;
|
||||
if (typeof count !== "number") return "-";
|
||||
try {
|
||||
return count.toLocaleString();
|
||||
} catch {
|
||||
return String(count);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
dataIndex: "actions",
|
||||
render: (_: any, task: any) => (
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{task._kind === "manual" &&
|
||||
operations.map((operation) => (
|
||||
<Button
|
||||
key={operation.key}
|
||||
type="text"
|
||||
icon={operation.icon}
|
||||
onClick={() => (operation?.onClick as any)?.(task)}
|
||||
title={operation.label}
|
||||
/>
|
||||
))}
|
||||
{task._kind === "auto" && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined style={{ color: "#f5222d" }} />}
|
||||
onClick={() => handleDeleteAuto(task)}
|
||||
title="删除自动标注任务"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据标注</h1>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: "tasks",
|
||||
label: "标注任务",
|
||||
children: (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and view controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: All action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
danger
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
创建标注任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task List/Card */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
key="id"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={mergedTableData}
|
||||
pagination={pagination}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys, rows) => {
|
||||
setSelectedRowKeys(keys as (string | number)[]);
|
||||
setSelectedRows(rows as any[]);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations as any}
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateAnnotationTask
|
||||
open={showCreateDialog}
|
||||
onClose={() => setShowCreateDialog(false)}
|
||||
onRefresh={(mode?: any) => {
|
||||
// 手动标注创建成功后刷新标注任务列表
|
||||
fetchData();
|
||||
// 自动标注创建成功后立即刷新自动标注任务列表
|
||||
if (mode === "auto") {
|
||||
refreshAutoTasks(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ExportAnnotationDialog
|
||||
open={!!exportTask}
|
||||
projectId={exportTask?.id || ""}
|
||||
projectName={exportTask?.name || ""}
|
||||
onClose={() => setExportTask(null)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "templates",
|
||||
label: "标注模板",
|
||||
children: <TemplateList />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Table, message, Modal, Tabs } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { AnnotationTask } from "../annotation.model";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
deleteAnnotationTaskByIdUsingDelete,
|
||||
queryAnnotationTasksUsingGet,
|
||||
} from "../annotation.api";
|
||||
import { mapAnnotationTask } from "../annotation.const";
|
||||
import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog";
|
||||
import ExportAnnotationDialog from "./ExportAnnotationDialog";
|
||||
import { ColumnType } from "antd/es/table";
|
||||
import { TemplateList } from "../Template";
|
||||
// Note: DevelopmentInProgress intentionally not used here
|
||||
|
||||
export default function DataAnnotation() {
|
||||
// return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("tasks");
|
||||
const [viewMode, setViewMode] = useState<"list" | "card">("list");
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [exportTask, setExportTask] = useState<AnnotationTask | null>(null);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
|
||||
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
|
||||
const handleAnnotate = (task: AnnotationTask) => {
|
||||
const projectId = (task as any)?.id;
|
||||
if (!projectId) {
|
||||
message.error("无法进入标注:缺少标注项目ID");
|
||||
return;
|
||||
}
|
||||
navigate(`/data/annotation/annotate/${projectId}`);
|
||||
};
|
||||
|
||||
const handleExport = (task: AnnotationTask) => {
|
||||
setExportTask(task);
|
||||
};
|
||||
|
||||
const handleDelete = (task: AnnotationTask) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除标注任务「${task.name}」吗?`,
|
||||
content: "删除标注任务不会删除对应数据集,但会删除该任务的所有标注结果。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("删除成功");
|
||||
fetchData();
|
||||
// clear selection if deleted item was selected
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (!selectedRows || selectedRows.length === 0) return;
|
||||
Modal.confirm({
|
||||
title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`,
|
||||
content: "删除标注任务不会删除对应数据集,但会删除这些任务的所有标注结果。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id))
|
||||
);
|
||||
message.success("批量删除已完成");
|
||||
fetchData();
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRows([]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("批量删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "annotate",
|
||||
label: "标注",
|
||||
icon: (
|
||||
<EditOutlined
|
||||
className="w-4 h-4 text-green-400"
|
||||
style={{ color: "#52c41a" }}
|
||||
/>
|
||||
),
|
||||
onClick: handleAnnotate,
|
||||
},
|
||||
{
|
||||
key: "export",
|
||||
label: "导出",
|
||||
icon: <DownloadOutlined className="w-4 h-4" style={{ color: "#1890ff" }} />,
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
dataIndex: "actions",
|
||||
render: (_: any, task: any) => (
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{operations.map((operation) => (
|
||||
<Button
|
||||
key={operation.key}
|
||||
type="text"
|
||||
icon={operation.icon}
|
||||
onClick={() => (operation?.onClick as any)?.(task)}
|
||||
title={operation.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据标注</h1>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: "tasks",
|
||||
label: "标注任务",
|
||||
children: (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and view controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: All action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
danger
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
创建标注任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task List/Card */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
key="id"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys, rows) => {
|
||||
setSelectedRowKeys(keys as (string | number)[]);
|
||||
setSelectedRows(rows as any[]);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations as any}
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateAnnotationTask
|
||||
open={showCreateDialog}
|
||||
onClose={() => setShowCreateDialog(false)}
|
||||
onRefresh={() => fetchData()}
|
||||
/>
|
||||
|
||||
<ExportAnnotationDialog
|
||||
open={!!exportTask}
|
||||
projectId={exportTask?.id || ""}
|
||||
projectName={exportTask?.name || ""}
|
||||
onClose={() => setExportTask(null)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "templates",
|
||||
label: "标注模板",
|
||||
children: <TemplateList />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,26 +48,6 @@ export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
|
||||
// 自动标注任务管理
|
||||
export function queryAutoAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/auto", params);
|
||||
}
|
||||
|
||||
export function createAutoAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/auto", data);
|
||||
}
|
||||
|
||||
export function deleteAutoAnnotationTaskByIdUsingDelete(taskId: string) {
|
||||
return del(`/api/annotation/auto/${taskId}`);
|
||||
}
|
||||
|
||||
export function getAutoAnnotationTaskStatusUsingGet(taskId: string) {
|
||||
return get(`/api/annotation/auto/${taskId}/status`);
|
||||
}
|
||||
|
||||
export function downloadAutoAnnotationResultUsingGet(taskId: string) {
|
||||
return download(`/api/annotation/auto/${taskId}/download`);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Label Studio Editor(内嵌版)
|
||||
|
||||
Reference in New Issue
Block a user