From 5b5e6373ad6da2e998e6729b446aeb5f8fd86b35 Mon Sep 17 00:00:00 2001 From: o0Shark0o <1191322051@qq.com> Date: Tue, 27 Jan 2026 17:14:36 +0800 Subject: [PATCH] feat(annotation): simplify task creation and streamline sync workflow --- .../business/DatasetFileTransfer.tsx | 171 ++++++- .../AutoAnnotation/AutoAnnotation.tsx | 54 +- .../components/CreateAutoAnnotationDialog.tsx | 5 +- .../EditAutoAnnotationDatasetDialog.tsx | 2 + .../ImportFromLabelStudioDialog.tsx | 50 +- .../components/CreateAnnotationTaskDialog.tsx | 178 ++++--- .../EditManualAnnotationDatasetDialog.tsx | 4 +- .../DataAnnotation/Home/DataAnnotation.tsx | 54 +- .../ManualImportFromLabelStudioDialog.tsx | 50 +- .../pages/DataAnnotation/annotation.api.ts | 4 +- .../app/module/annotation/interface/auto.py | 7 +- .../module/annotation/interface/project.py | 7 +- .../app/module/annotation/schema/auto.py | 14 +- .../app/module/dataset/service/service.py | 78 +++ .../process.py | 13 +- .../datamate/auto_annotation_worker.py | 478 +++++++++--------- 16 files changed, 638 insertions(+), 531 deletions(-) diff --git a/frontend/src/components/business/DatasetFileTransfer.tsx b/frontend/src/components/business/DatasetFileTransfer.tsx index 08aa64cbd..22bfce16b 100644 --- a/frontend/src/components/business/DatasetFileTransfer.tsx +++ b/frontend/src/components/business/DatasetFileTransfer.tsx @@ -21,6 +21,18 @@ interface DatasetFileTransferProps onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void; onDatasetSelect?: (dataset: Dataset | null) => void; datasetTypeFilter?: DatasetType; + /** + * 是否强制“单数据集模式”: + * - 为 true 时,仅允许从同一个数据集选择文件; + * - 当已选文件来自某个数据集时,尝试从其他数据集勾选文件会被阻止并提示。 + */ + singleDatasetOnly?: boolean; + /** + * 固定可选数据集 ID: + * - 设置后,左侧数据集列表只展示该数据集; + * - 主要用于“编辑任务数据集”场景,锁定为任务创建时的数据集。 + */ + fixedDatasetId?: string | number; /** * 锁定的文件ID集合: * - 在左侧文件列表中,这些文件的勾选框会变成灰色且不可交互; @@ -28,6 +40,12 @@ interface DatasetFileTransferProps * - 主要用于“编辑任务数据集”场景下锁死任务初始文件。 */ lockedFileIds?: string[]; + /** + * 整体禁用开关: + * - 为 true 时,禁止切换数据集和选择文件,仅用于展示当前配置; + * - 可配合上层逻辑(如“需先选模板再选数据集”)使用。 + */ + disabled?: boolean; } const fileCols = [ @@ -59,7 +77,10 @@ const DatasetFileTransfer: React.FC = ({ onSelectedFilesChange, onDatasetSelect, datasetTypeFilter, + singleDatasetOnly, + fixedDatasetId, lockedFileIds, + disabled, ...props }) => { const [datasets, setDatasets] = React.useState([]); @@ -91,19 +112,49 @@ const DatasetFileTransfer: React.FC = ({ return new Set((lockedFileIds || []).map((id) => String(id))); }, [lockedFileIds]); + // 在单数据集模式下,根据已选文件反推“当前锁定的数据集ID” + const lockedDatasetId = React.useMemo(() => { + if (!singleDatasetOnly) return undefined; + const ids = new Set( + Object.values(selectedFilesMap) + .map((file: any) => file?.datasetId) + .filter((id) => id !== undefined && id !== null && id !== "") + .map((id) => String(id)) + ); + if (ids.size === 1) { + return Array.from(ids)[0]; + } + return undefined; + }, [singleDatasetOnly, selectedFilesMap]); + const fetchDatasets = async () => { const { data } = await queryDatasetsUsingGet({ // Ant Design Table pagination.current is 1-based; ensure backend also receives 1-based value page: datasetPagination.current, size: datasetPagination.pageSize, keyword: datasetSearch, - // 仅在显式传入过滤类型时才按类型过滤;否则后端返回所有类型 + // 后端在大多数环境下支持按 type 过滤;若未生效,前端仍会基于 datasetTypeFilter 再做一次兜底筛选 type: datasetTypeFilter, }); - setDatasets(data.content.map(mapDataset) || []); + + let mapped: any[] = (data.content || []).map(mapDataset); + + // 兜底:在前端再按 datasetTypeFilter 过滤一次,确保只展示指定类型的数据集 + if (datasetTypeFilter) { + mapped = mapped.filter( + (ds: any) => ds.datasetType === datasetTypeFilter + ); + } + + const filtered = + fixedDatasetId !== undefined && fixedDatasetId !== null + ? mapped.filter((ds: Dataset) => String(ds.id) === String(fixedDatasetId)) + : mapped; + + setDatasets(filtered); setDatasetPagination((prev) => ({ ...prev, - total: data.totalElements, + total: filtered.length, })); }; @@ -111,7 +162,7 @@ const DatasetFileTransfer: React.FC = ({ () => { fetchDatasets(); }, - [datasetSearch, datasetPagination.pageSize, datasetPagination.current], + [datasetSearch, datasetPagination.pageSize, datasetPagination.current, datasetTypeFilter], 300 ); @@ -170,12 +221,40 @@ const DatasetFileTransfer: React.FC = ({ onDatasetSelect?.(selectedDataset); }, [selectedDataset, onDatasetSelect]); + // 在 fixedDatasetId 场景下,数据集列表加载完成后自动选中该数据集 + useEffect(() => { + if (!open) return; + if (fixedDatasetId === undefined || fixedDatasetId === null) return; + if (selectedDataset) return; + if (!datasets.length) return; + + const target = datasets.find((ds) => String(ds.id) === String(fixedDatasetId)); + if (target) { + setSelectedDataset(target); + } + }, [open, fixedDatasetId, datasets, selectedDataset]); + const handleSelectAllInDataset = useCallback(async () => { if (!selectedDataset) { message.warning("请先选择一个数据集"); return; } + // 单数据集模式下,如果当前已选文件来自其他数据集,则阻止一键全选 + if (singleDatasetOnly) { + const existingIds = new Set( + Object.values(selectedFilesMap) + .map((file: any) => file?.datasetId) + .filter((id) => id !== undefined && id !== null && id !== "") + .map((id) => String(id)), + ); + const currentId = String(selectedDataset.id); + if (existingIds.size > 0 && (!existingIds.has(currentId) || existingIds.size > 1)) { + message.warning("当前仅支持从一个数据集选择文件,请先清空已选文件后再切换数据集"); + return; + } + } + try { setSelectingAll(true); @@ -246,6 +325,23 @@ const DatasetFileTransfer: React.FC = ({ if (lockedIdSet.has(String(record.id))) { return; } + + // 单数据集模式:禁止从多个数据集混选文件 + if (singleDatasetOnly && !selectedFilesMap[record.id]) { + const recordDatasetId = (record as any).datasetId; + const existingIds = new Set( + Object.values(selectedFilesMap) + .map((file: any) => file?.datasetId) + .filter((id) => id !== undefined && id !== null && id !== "") + .map((id) => String(id)), + ); + const recId = recordDatasetId !== undefined && recordDatasetId !== null ? String(recordDatasetId) : undefined; + if (existingIds.size > 0 && recId && !existingIds.has(recId)) { + message.warning("当前仅支持从一个数据集选择文件,请先清空已选文件后再切换数据集"); + return; + } + } + if (!selectedFilesMap[record.id]) { onSelectedFilesChange({ ...selectedFilesMap, @@ -321,28 +417,52 @@ const DatasetFileTransfer: React.FC = ({ placeholder="搜索数据集名称..." value={datasetSearch} allowClear - onChange={(e) => setDatasetSearch(e.target.value)} + onChange={(e) => !disabled && setDatasetSearch(e.target.value)} + disabled={disabled} /> - `cursor-pointer ${ - selectedDataset?.id === record.id ? "bg-blue-100" : "" - }` - } + rowClassName={(record) => { + const isActive = selectedDataset?.id === record.id; + const hasSelection = Object.keys(selectedFilesMap).length > 0; + const isLockedOtherDataset = + !!singleDatasetOnly && + !!lockedDatasetId && + hasSelection && + String(record.id) !== lockedDatasetId; + return `cursor-pointer ${ + isActive ? "bg-blue-100" : "" + } ${isLockedOtherDataset ? "text-gray-400 cursor-not-allowed" : ""}`; + }} onRow={(record: Dataset) => ({ onClick: () => { - setSelectedDataset(record); - if (!datasetSelections.find((d) => d.id === record.id)) { - setDatasetSelections([...datasetSelections, record]); - } else { - setDatasetSelections( - datasetSelections.filter((d) => d.id !== record.id) - ); - } + if (disabled) return; + + // 单数据集模式:当已有选中文件且尝试切换到其他数据集时,直接提示并阻止切换 + const hasSelection = + singleDatasetOnly && + Object.keys(selectedFilesMap).length > 0 && + !!lockedDatasetId; + if ( + hasSelection && + String(record.id) !== String(lockedDatasetId) + ) { + message.warning( + "当前仅支持从一个数据集选择文件,请先清空已选文件后再切换数据集" + ); + return; + } + setSelectedDataset(record); + if (!datasetSelections.find((d) => d.id === record.id)) { + setDatasetSelections([...datasetSelections, record]); + } else { + setDatasetSelections( + datasetSelections.filter((d) => d.id !== record.id) + ); + } }, })} dataSource={datasets} @@ -350,6 +470,7 @@ const DatasetFileTransfer: React.FC = ({ pagination={{ ...datasetPagination, onChange: (page, pageSize) => + !disabled && setDatasetPagination({ current: page, pageSize: pageSize || datasetPagination.pageSize, @@ -365,8 +486,8 @@ const DatasetFileTransfer: React.FC = ({ - + {/* 一级功能:编辑(跳转 Label Studio) + 同步(导回结果) */} diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx index b3f867a04..057e7a411 100644 --- a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx +++ b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx @@ -188,7 +188,6 @@ export default function CreateAutoAnnotationDialog({ modelSize: values.modelSize, confThreshold: values.confThreshold, targetClasses: selectAllClasses ? [] : values.targetClasses || [], - outputDatasetName: values.outputDatasetName || undefined, }, }; @@ -243,6 +242,7 @@ export default function CreateAutoAnnotationDialog({ form.setFieldsValue({ datasetId: dataset?.id ?? "" }); }} datasetTypeFilter={DatasetType.IMAGE} + singleDatasetOnly /> {selectedDataset && (
@@ -291,9 +291,6 @@ export default function CreateAutoAnnotationDialog({ )} - - - ); diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/EditAutoAnnotationDatasetDialog.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/EditAutoAnnotationDatasetDialog.tsx index 609dd0f60..4a3fa61ff 100644 --- a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/EditAutoAnnotationDatasetDialog.tsx +++ b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/EditAutoAnnotationDatasetDialog.tsx @@ -186,6 +186,8 @@ export default function EditAutoAnnotationDatasetDialog({ setSelectedDataset(dataset); }} datasetTypeFilter={DatasetType.IMAGE} + singleDatasetOnly + fixedDatasetId={task.datasetId} lockedFileIds={Array.from(initialFileIds)} /> {selectedDataset && ( diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/ImportFromLabelStudioDialog.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/ImportFromLabelStudioDialog.tsx index ac9a2a0b6..402643cdc 100644 --- a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/ImportFromLabelStudioDialog.tsx +++ b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/ImportFromLabelStudioDialog.tsx @@ -1,8 +1,6 @@ import { useEffect, useState } from "react"; import { Modal, Form, Select, Input, message } from "antd"; import type { AutoAnnotationTask } from "../../annotation.model"; -import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; -import type { Dataset } from "@/pages/DataManagement/dataset.model"; import { importAutoAnnotationFromLabelStudioUsingPost } from "../../annotation.api"; interface ImportFromLabelStudioDialogProps { @@ -29,39 +27,12 @@ export default function ImportFromLabelStudioDialog({ onSuccess, }: ImportFromLabelStudioDialogProps) { const [form] = Form.useForm(); - const [datasets, setDatasets] = useState([]); const [loading, setLoading] = useState(false); - useEffect(() => { - if (!visible) return; - - let cancelled = false; - - (async () => { - try { - const resp: any = await queryDatasetsUsingGet({ page: 0, size: 1000 }); - const list: Dataset[] = resp?.content || resp?.data?.content || resp?.data || resp || []; - if (!cancelled && Array.isArray(list)) { - setDatasets(list); - } - } catch (e) { - console.error("Failed to fetch datasets for LS import:", e); - if (!cancelled) { - message.error("获取数据集列表失败"); - } - } - })(); - - return () => { - cancelled = true; - }; - }, [visible]); - useEffect(() => { if (visible && task) { // 默认选中任务原始数据集和 JSON 导出格式 form.setFieldsValue({ - targetDatasetId: task.datasetId, exportFormat: "JSON", }); } @@ -70,7 +41,6 @@ export default function ImportFromLabelStudioDialog({ const handleOk = async () => { try { const values = await form.validateFields(); - const targetDatasetId: string = values.targetDatasetId; const exportFormat: string = values.exportFormat; const fileName: string | undefined = values.fileName; @@ -81,7 +51,6 @@ export default function ImportFromLabelStudioDialog({ setLoading(true); await importAutoAnnotationFromLabelStudioUsingPost(task.id, { - targetDatasetId, exportFormat, // 后端会自动附加正确的扩展名 fileName: fileName?.trim() || undefined, @@ -115,22 +84,6 @@ export default function ImportFromLabelStudioDialog({ {task?.name || "-"} - - - - - {/* 标注工程名称 */} + {/* 任务名称放在第一行,必填 */} { const trimmed = (value || "").trim(); if (!trimmed) { @@ -378,16 +382,12 @@ export default function CreateAnnotationTask({ ]} > setNameManuallyEdited(true)} /> - {/* 描述变为可选 */} - -