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

View File

@@ -3,6 +3,7 @@ import { Card, Button, Table, message, Modal, Tabs } from "antd";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
FormOutlined,
DeleteOutlined, DeleteOutlined,
DownloadOutlined, DownloadOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
@@ -29,6 +30,7 @@ export default function DataAnnotation() {
const [viewMode, setViewMode] = useState<"list" | "card">("list"); const [viewMode, setViewMode] = useState<"list" | "card">("list");
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
const [exportTask, setExportTask] = useState<AnnotationTask | null>(null); const [exportTask, setExportTask] = useState<AnnotationTask | null>(null);
const [editTask, setEditTask] = useState<AnnotationTask | null>(null);
const { const {
loading, loading,
@@ -56,6 +58,10 @@ export default function DataAnnotation() {
setExportTask(task); setExportTask(task);
}; };
const handleEdit = (task: AnnotationTask) => {
setEditTask(task);
};
const handleDelete = (task: AnnotationTask) => { const handleDelete = (task: AnnotationTask) => {
Modal.confirm({ Modal.confirm({
title: `确认删除标注任务「${task.name}」吗?`, title: `确认删除标注任务「${task.name}」吗?`,
@@ -116,6 +122,12 @@ export default function DataAnnotation() {
), ),
onClick: handleAnnotate, onClick: handleAnnotate,
}, },
{
key: "edit",
label: "编辑",
icon: <FormOutlined className="w-4 h-4" style={{ color: "#722ed1" }} />,
onClick: handleEdit,
},
{ {
key: "export", key: "export",
label: "导出", label: "导出",
@@ -264,9 +276,13 @@ export default function DataAnnotation() {
)} )}
<CreateAnnotationTask <CreateAnnotationTask
open={showCreateDialog} open={showCreateDialog || !!editTask}
onClose={() => setShowCreateDialog(false)} onClose={() => {
setShowCreateDialog(false);
setEditTask(null);
}}
onRefresh={() => fetchData()} onRefresh={() => fetchData()}
editTask={editTask}
/> />
<ExportAnnotationDialog <ExportAnnotationDialog

View File

@@ -21,6 +21,14 @@ export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
return del(`/api/annotation/project/${mappingId}`); return del(`/api/annotation/project/${mappingId}`);
} }
export function getAnnotationTaskByIdUsingGet(taskId: string) {
return get(`/api/annotation/project/${taskId}`);
}
export function updateAnnotationTaskByIdUsingPut(taskId: string, data: any) {
return put(`/api/annotation/project/${taskId}`, data);
}
// 标签配置管理 // 标签配置管理
export function getTagConfigUsingGet() { export function getTagConfigUsingGet() {
return get("/api/annotation/tags/config"); return get("/api/annotation/tags/config");