From 3e04aecb3470362ba7526e20200664d01dea712a Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 19 Jan 2026 12:02:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(annotation):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=A0=87=E6=B3=A8=E5=8A=9F=E8=83=BD=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=B9=B6=E7=AE=80=E5=8C=96=E5=88=9B=E5=BB=BA=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 AutoAnnotation 相关的所有组件和页面文件 - 从 CreateAnnotationTaskDialog 中移除自动标注相关的表单和逻辑 - 简化 CreateAnnotationTaskDialog 为仅支持手动标注模式 - 移除 COCO_CLASSES 常量和相关依赖项 - 清理无用的导入和状态变量 - 更新对话框布局以适应单一标注模式 --- .../AutoAnnotation/AutoAnnotation.tsx | 302 ---- .../components/CreateAutoAnnotationDialog.tsx | 286 ---- .../DataAnnotation/AutoAnnotation/index.ts | 1 - .../components/CreateAnnotationTaskDialog.tsx | 1293 +++++++---------- .../DataAnnotation/Home/DataAnnotation.tsx | 791 ++++------ .../pages/DataAnnotation/annotation.api.ts | 20 - 6 files changed, 799 insertions(+), 1894 deletions(-) delete mode 100644 frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx delete mode 100644 frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx delete mode 100644 frontend/src/pages/DataAnnotation/AutoAnnotation/index.ts diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx deleted file mode 100644 index deedd54..0000000 --- a/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx +++ /dev/null @@ -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 = { - pending: "default", - running: "processing", - completed: "success", - failed: "error", - cancelled: "default", -}; - -const STATUS_LABELS: Record = { - pending: "等待中", - running: "处理中", - completed: "已完成", - failed: "失败", - cancelled: "已取消", -}; - -const MODEL_SIZE_LABELS: Record = { - n: "YOLOv8n (最快)", - s: "YOLOv8s", - m: "YOLOv8m", - l: "YOLOv8l (推荐)", - x: "YOLOv8x (最精确)", -}; - -export default function AutoAnnotation() { - const [loading, setLoading] = useState(false); - const [tasks, setTasks] = useState([]); - const [showCreateDialog, setShowCreateDialog] = useState(false); - const [selectedRowKeys, setSelectedRowKeys] = useState([]); - - 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: ( -
-

输出路径:{task.outputPath}

-

检测对象数:{task.detectedObjects}

-

- 处理图片数:{task.processedImages} / {task.totalImages} -

-
- ), - }); - } - }; - - const columns: ColumnType[] = [ - { 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 ( - - {text} - - ); - }, - }, - { - 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[]) => ( - 0 ? classes.join(", ") : "全部类别"} - > - - {classes.length > 0 - ? `${classes.length} 个类别` - : "全部类别"} - - - ), - }, - { - title: "状态", - dataIndex: "status", - key: "status", - width: 100, - render: (status: AutoAnnotationStatus) => ( - {STATUS_LABELS[status]} - ), - }, - { - title: "进度", - dataIndex: "progress", - key: "progress", - width: 150, - render: (progress: number, record: AutoAnnotationTask) => ( -
- -
- {record.processedImages} / {record.totalImages} -
-
- ), - }, - { - 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) => ( - - {record.status === "completed" && ( - <> - - - - - } - > - setSelectedRowKeys(keys as string[]), - }} - pagination={{ pageSize: 10 }} - scroll={{ x: 1000 }} - /> - - - setShowCreateDialog(false)} - onSuccess={() => { - setShowCreateDialog(false); - fetchTasks(); - }} - /> - - ); -} \ No newline at end of file diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx deleted file mode 100644 index 2e9adbe..0000000 --- a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx +++ /dev/null @@ -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([]); - const [selectAllClasses, setSelectAllClasses] = useState(true); - const [selectedFilesMap, setSelectedFilesMap] = useState>({}); - const [selectedDataset, setSelectedDataset] = useState(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 ( - -
- - - - - - { - setSelectedDataset(dataset); - form.setFieldsValue({ datasetId: dataset?.id ?? "" }); - }} - datasetTypeFilter={DatasetType.IMAGE} - /> - {selectedDataset && ( -
- 当前数据集:{selectedDataset.name} - 已选择 - {imageFileCount} 个图像文件 -
- )} -
- - - - - - - - - `${(v || 0) * 100}%` }} /> - - - - handleClassSelectionChange(e.target.checked)}> - 选中所有类别 - - {!selectAllClasses && ( - - - - )} - - - - - - -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/index.ts b/frontend/src/pages/DataAnnotation/AutoAnnotation/index.ts deleted file mode 100644 index fd1dd1b..0000000 --- a/frontend/src/pages/DataAnnotation/AutoAnnotation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AutoAnnotation"; \ No newline at end of file diff --git a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx index 403bc7c..1aedcc5 100644 --- a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx +++ b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx @@ -1,784 +1,509 @@ -import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; -import { mapDataset } from "@/pages/DataManagement/dataset.const"; -import { Button, Form, Input, Modal, Select, message, Tabs, Slider, Checkbox, Radio, Space, Typography } from "antd"; -import TextArea from "antd/es/input/TextArea"; -import { useEffect, useState } from "react"; -import { - createAnnotationTaskUsingPost, - queryAnnotationTemplatesUsingGet, - createAutoAnnotationTaskUsingPost, -} from "../../annotation.api"; -import DatasetFileTransfer from "@/components/business/DatasetFileTransfer"; -import { DatasetType, type Dataset, type DatasetFile } from "@/pages/DataManagement/dataset.model"; -import type { AnnotationTemplate } from "../../annotation.model"; -import LabelStudioEmbed from "@/components/business/LabelStudioEmbed"; -import TemplateConfigurationForm from "../../components/TemplateConfigurationForm"; - -const { Option } = Select; - -const COCO_CLASSES = [ - // ... (keep existing 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 CreateAnnotationTask({ - open, - onClose, - onRefresh, -}: { - open: boolean; - onClose: () => void; - onRefresh: () => void; -}) { - const [manualForm] = Form.useForm(); - const [autoForm] = Form.useForm(); - const [datasets, setDatasets] = useState([]); - const [templates, setTemplates] = useState([]); - const [submitting, setSubmitting] = useState(false); - const [nameManuallyEdited, setNameManuallyEdited] = useState(false); - const [activeMode, setActiveMode] = useState<"manual" | "auto">("manual"); - - // Custom template state - const [customXml, setCustomXml] = useState(""); - const [showPreview, setShowPreview] = useState(false); - const [previewTaskData, setPreviewTaskData] = useState>({}); - const [configMode, setConfigMode] = useState<"template" | "custom">("template"); - const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual"); - - const [selectAllClasses, setSelectAllClasses] = useState(true); - const [selectedFilesMap, setSelectedFilesMap] = useState>({}); - const [selectedDataset, setSelectedDataset] = useState(null); - const [imageFileCount, setImageFileCount] = useState(0); - - useEffect(() => { - if (!open) return; - const fetchData = async () => { - try { - // Fetch datasets - const { data: datasetData } = await queryDatasetsUsingGet({ - page: 0, - pageSize: 1000, - }); - setDatasets(datasetData.content.map(mapDataset) || []); - - // Fetch templates - const templateResponse = await queryAnnotationTemplatesUsingGet({ - page: 1, - size: 100, - }); - - if (templateResponse.code === 200 && templateResponse.data) { - const fetchedTemplates = templateResponse.data.content || []; - console.log("Fetched templates:", fetchedTemplates); - setTemplates(fetchedTemplates); - } else { - console.error("Failed to fetch templates:", templateResponse); - setTemplates([]); - } - } catch (error) { - console.error("Error fetching data:", error); - setTemplates([]); - } - }; - fetchData(); - }, [open]); - - // Reset form and manual-edit flag when modal opens - useEffect(() => { - if (open) { - manualForm.resetFields(); - autoForm.resetFields(); - setNameManuallyEdited(false); - setActiveMode("manual"); - setSelectAllClasses(true); - setSelectedFilesMap({}); - setSelectedDataset(null); - setImageFileCount(0); - setCustomXml(""); - setShowPreview(false); - setPreviewTaskData({}); - setConfigMode("template"); - setTemplateEditTab("visual"); - } - }, [open, manualForm, autoForm]); - - 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 generateXmlFromConfig = (objects: any[], labels: any[]) => { - let xml = '\n'; - - // Objects - if (objects) { - objects.forEach((obj: any) => { - xml += ` <${obj.type} name="${obj.name}" value="${obj.value}" />\n`; - }); - } - - // Controls - if (labels) { - labels.forEach((lbl: any) => { - let attrs = `name="${lbl.fromName}" toName="${lbl.toName}"`; - if (lbl.required) attrs += ' required="true"'; - - xml += ` <${lbl.type} ${attrs}>\n`; - - const options = lbl.type === 'Choices' ? lbl.options : lbl.labels; - if (options && options.length) { - options.forEach((opt: string) => { - if (lbl.type === 'Choices') { - xml += ` \n`; - } else { - xml += ` '; - return xml; - }; - - // 从表单值同步生成 XML - const syncFormToXml = () => { - const objects = manualForm.getFieldValue("objects"); - const labels = manualForm.getFieldValue("labels"); - if (objects && objects.length > 0) { - const xml = generateXmlFromConfig(objects, labels || []); - setCustomXml(xml); - } - }; - - // 根据 objects 配置生成预览用的示例数据 - const generateExampleData = (objects: any[]) => { - const exampleUrls: Record = { - Image: "https://labelstud.io/images/opa-header.png", - Audio: "https://labelstud.io/files/sample.wav", - Video: "https://labelstud.io/files/sample.mp4", - }; - const exampleTexts: Record = { - Text: "这是示例文本,用于预览标注界面。", - HyperText: "

这是示例 HTML 内容

", - Header: "示例标题", - Paragraphs: "段落一\n\n段落二\n\n段落三", - }; - - const data: Record = {}; - - if (!objects || objects.length === 0) { - // 默认数据 - return { - image: exampleUrls.Image, - text: exampleTexts.Text, - audio: exampleUrls.Audio, - }; - } - - objects.forEach((obj: any) => { - if (!obj?.name || !obj?.value) return; - // 变量名从 $varName 中提取 - const varName = obj.value.startsWith("$") ? obj.value.slice(1) : obj.name; - - if (exampleUrls[obj.type]) { - data[varName] = exampleUrls[obj.type]; - } else if (exampleTexts[obj.type]) { - data[varName] = exampleTexts[obj.type]; - } else { - // 未知类型,尝试根据名称猜测 - const lowerName = varName.toLowerCase(); - if (lowerName.includes("image") || lowerName.includes("img")) { - data[varName] = exampleUrls.Image; - } else if (lowerName.includes("audio") || lowerName.includes("sound")) { - data[varName] = exampleUrls.Audio; - } else if (lowerName.includes("video")) { - data[varName] = exampleUrls.Video; - } else { - data[varName] = exampleTexts.Text; - } - } - }); - - return data; - }; - - // 当选择模板时,加载模板配置到表单 - const handleTemplateSelect = (value: string, option: any) => { - if (option && option.config) { - setCustomXml(option.config); - } - - // 从模板列表中找到完整的模板数据 - const selectedTemplate = templates.find(t => t.id === value); - if (selectedTemplate?.configuration) { - const { objects, labels } = selectedTemplate.configuration; - manualForm.setFieldsValue({ - objects: objects || [{ name: "image", type: "Image", value: "$image" }], - labels: labels || [], - }); - } else if (option && option.config) { - // 如果没有结构化配置,设置默认值 - manualForm.setFieldsValue({ - objects: [{ name: "image", type: "Image", value: "$image" }], - labels: [], - }); - } - }; - - const handleManualSubmit = async () => { - try { - const values = await manualForm.validateFields(); - - let finalLabelConfig = ""; - const objects = values.objects; - const labels = values.labels; - - if (configMode === "template") { - // 模板模式:优先使用可视化配置生成 XML,回退到直接使用 XML 编辑器内容 - if (templateEditTab === "visual" && objects && objects.length > 0) { - finalLabelConfig = generateXmlFromConfig(objects, labels || []); - } else if (customXml.trim()) { - finalLabelConfig = customXml; - } else { - message.error("请配置标注模板或选择一个现有模板"); - return; - } - } else { - // 自定义模式 - if (!objects || objects.length === 0) { - message.error("请至少配置一个数据对象"); - return; - } - if (!labels || labels.length === 0) { - message.error("请至少配置一个标签控件"); - return; - } - finalLabelConfig = generateXmlFromConfig(objects, labels); - } - - setSubmitting(true); - const requestData = { - name: values.name, - description: values.description, - datasetId: values.datasetId, - templateId: configMode === 'template' ? values.templateId : undefined, - labelConfig: finalLabelConfig, - }; - await createAnnotationTaskUsingPost(requestData); - message?.success?.("创建标注任务成功"); - onClose(); - onRefresh(); - } catch (err: any) { - console.error("Create annotation task failed", err); - const msg = err?.message || err?.data?.message || "创建失败,请稍后重试"; - (message as any)?.error?.(msg); - } finally { - setSubmitting(false); - } - }; - - const handleAutoSubmit = async () => { - // ... (keep existing handleAutoSubmit) - try { - const values = await autoForm.validateFields(); - - if (imageFileCount === 0) { - message.error("请至少选择一个图像文件"); - return; - } - - setSubmitting(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("自动标注任务创建成功"); - // 触发上层刷新自动标注任务列表 - (onRefresh as any)?.("auto"); - onClose(); - } catch (error: any) { - if (error.errorFields) return; - console.error("Failed to create auto annotation task:", error); - message.error(error.message || "创建自动标注任务失败"); - } finally { - setSubmitting(false); - } - }; - - const handleClassSelectionChange = (checked: boolean) => { - setSelectAllClasses(checked); - if (checked) { - autoForm.setFieldsValue({ targetClasses: [] }); - } - }; - - const handleConfigModeChange = (e: any) => { - const mode = e.target.value; - setConfigMode(mode); - // 两种模式都需要初始化默认值 - const currentObjects = manualForm.getFieldValue("objects"); - if (!currentObjects || currentObjects.length === 0) { - manualForm.setFieldsValue({ - objects: [{ name: "image", type: "Image", value: "$image" }], - labels: [], - }); - } - // 切换到模板模式时,重置 tab 到可视化 - if (mode === "template") { - setTemplateEditTab("visual"); - } - }; - - return ( - <> - - - - - } - width={800} - > - setActiveMode(key as "manual" | "auto")} - items={[ - { - key: "manual", - label: "手动标注", - children: ( -
- {/* 数据集 与 标注工程名称 并排显示(数据集在左) */} -
- - setNameManuallyEdited(true)} - /> - -
- {/* 描述变为可选 */} - -