feat(annotation): 添加标注任务编辑功能

- 新增编辑模式支持,通过 editTask 属性控制
- 添加 getAnnotationTaskByIdUsingGet 和 updateAnnotationTaskByIdUsingPut API 接口
- 实现编辑模式下的任务详情加载和表单填充
- 编辑模式下禁用数据集修改和配置模式切换
- 更新模态框标题为动态显示(创建/编辑)
- 在任务列表操作菜单中添加编辑按钮
- 编辑模式下只允许修改标签取值,限制模板结构调整
- 添加任务详情加载状态显示
This commit is contained in:
2026-01-19 20:25:56 +08:00
parent 11980a8edd
commit 2229eb218d
3 changed files with 137 additions and 23 deletions

View File

@@ -6,22 +6,30 @@ import { useEffect, useState } from "react";
import { Eye } from "lucide-react";
import {
createAnnotationTaskUsingPost,
getAnnotationTaskByIdUsingGet,
updateAnnotationTaskByIdUsingPut,
queryAnnotationTemplatesUsingGet,
} from "../../annotation.api";
import { type Dataset } from "@/pages/DataManagement/dataset.model";
import type { AnnotationTemplate } from "../../annotation.model";
import type { AnnotationTemplate, AnnotationTask } from "../../annotation.model";
import LabelStudioEmbed from "@/components/business/LabelStudioEmbed";
import TemplateConfigurationForm from "../../components/TemplateConfigurationForm";
interface AnnotationTaskDialogProps {
open: boolean;
onClose: () => void;
onRefresh: () => void;
/** 编辑模式:传入要编辑的任务数据 */
editTask?: AnnotationTask | null;
}
export default function CreateAnnotationTask({
open,
onClose,
onRefresh,
}: {
open: boolean;
onClose: () => void;
onRefresh: () => void;
}) {
editTask,
}: AnnotationTaskDialogProps) {
const isEditMode = !!editTask;
const { message } = App.useApp();
const [manualForm] = Form.useForm();
const [datasets, setDatasets] = useState<Dataset[]>([]);
@@ -34,6 +42,8 @@ export default function CreateAnnotationTask({
const [showPreview, setShowPreview] = useState(false);
const [previewTaskData, setPreviewTaskData] = useState<Record<string, any>>({});
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
// 模板编辑模式切换(可视化 vs XML)
const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual");
// 是否已选择模板(用于启用受限编辑模式)
const [hasSelectedTemplate, setHasSelectedTemplate] = useState(false);
@@ -51,6 +61,9 @@ export default function CreateAnnotationTask({
const [previewFileType, setPreviewFileType] = useState<"text" | "image" | "video" | "audio">("text");
const [previewMediaUrl, setPreviewMediaUrl] = useState("");
// 任务详情加载状态(编辑模式)
const [taskDetailLoading, setTaskDetailLoading] = useState(false);
useEffect(() => {
if (!open) return;
const fetchData = async () => {
@@ -83,7 +96,7 @@ export default function CreateAnnotationTask({
fetchData();
}, [open]);
// Reset form and manual-edit flag when modal opens
// Reset form and manual-edit flag when modal opens, or load task data in edit mode
useEffect(() => {
if (open) {
manualForm.resetFields();
@@ -91,12 +104,58 @@ export default function CreateAnnotationTask({
setCustomXml("");
setShowPreview(false);
setPreviewTaskData({});
setConfigMode("template");
setHasSelectedTemplate(false);
setSelectedDatasetId(null);
setDatasetPreviewData([]);
if (isEditMode && editTask) {
// 编辑模式:加载任务详情
setTaskDetailLoading(true);
getAnnotationTaskByIdUsingGet(editTask.id)
.then((res: any) => {
if (res.code === 200 && res.data) {
const taskDetail = res.data;
// 填充基本信息
manualForm.setFieldsValue({
name: taskDetail.name,
description: taskDetail.description,
datasetId: taskDetail.datasetId,
});
setSelectedDatasetId(taskDetail.datasetId);
// 填充模板配置
if (taskDetail.configuration) {
const { objects, labels } = taskDetail.configuration;
manualForm.setFieldsValue({
objects: objects || [],
labels: labels || [],
});
}
// 设置 XML 配置用于预览
if (taskDetail.labelConfig) {
setCustomXml(taskDetail.labelConfig);
}
// 编辑模式始终使用 custom 配置模式(不改变结构,只改标签)
setConfigMode("custom");
// 编辑模式下启用受限编辑
setHasSelectedTemplate(true);
}
})
.catch((err) => {
console.error("Failed to load task detail:", err);
message.error("加载任务详情失败");
})
.finally(() => {
setTaskDetailLoading(false);
});
} else {
// 创建模式:重置为默认状态
setConfigMode("template");
setHasSelectedTemplate(false);
setSelectedDatasetId(null);
}
}
}, [open, manualForm]);
}, [open, manualForm, isEditMode, editTask, message]);
// 预览数据集
const handlePreviewDataset = async () => {
@@ -342,14 +401,27 @@ export default function CreateAnnotationTask({
datasetId: values.datasetId,
templateId: configMode === 'template' ? values.templateId : undefined,
labelConfig: finalLabelConfig,
// 编辑模式需要传递配置结构,用于后端保存
configuration: {
objects: objects || [],
labels: labels || [],
},
};
await createAnnotationTaskUsingPost(requestData);
message.success("创建标注任务成功");
if (isEditMode && editTask) {
// 编辑模式:调用更新接口
await updateAnnotationTaskByIdUsingPut(editTask.id, requestData);
message.success("更新标注任务成功");
} else {
// 创建模式:调用创建接口
await createAnnotationTaskUsingPost(requestData);
message.success("创建标注任务成功");
}
onClose();
onRefresh();
} catch (err: any) {
console.error("Create annotation task failed", err);
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
console.error(isEditMode ? "Update annotation task failed" : "Create annotation task failed", err);
const msg = err?.message || err?.data?.message || (isEditMode ? "更新失败,请稍后重试" : "创建失败,请稍后重试");
message.error(msg);
} finally {
setSubmitting(false);
@@ -378,7 +450,8 @@ export default function CreateAnnotationTask({
<Modal
open={open}
onCancel={onClose}
title="创建标注任务"
title={isEditMode ? "编辑标注任务" : "创建标注任务"}
loading={taskDetailLoading}
footer={
<>
<Button onClick={onClose} disabled={submitting}>
@@ -417,9 +490,11 @@ export default function CreateAnnotationTask({
}
name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]}
help={isEditMode ? "数据集不可修改" : undefined}
>
<Select
placeholder="请选择数据集"
disabled={isEditMode}
options={datasets.map((dataset) => {
return {
label: (
@@ -487,10 +562,12 @@ export default function CreateAnnotationTask({
<span className="text-sm font-medium text-gray-700 after:content-['*'] after:text-red-500 after:ml-1"></span>
<div className="flex gap-2">
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
<Radio.Button value="template"></Radio.Button>
<Radio.Button value="custom"></Radio.Button>
</Radio.Group>
{!isEditMode && (
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
<Radio.Button value="template"></Radio.Button>
<Radio.Button value="custom"></Radio.Button>
</Radio.Group>
)}
<Button
type="link"
@@ -517,7 +594,20 @@ export default function CreateAnnotationTask({
</div>
</div>
{configMode === 'template' ? (
{isEditMode ? (
// 编辑模式:只允许修改标签取值
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<div className="text-sm text-gray-500 mb-3 bg-blue-50 p-2 rounded border border-blue-200">
/
</div>
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
<TemplateConfigurationForm
form={manualForm}
restrictedMode={true}
/>
</div>
</div>
) : configMode === 'template' ? (
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<Form.Item
label="加载现有模板"