You've already forked DataMate
feat(annotation): 添加标注任务编辑功能
- 新增编辑模式支持,通过 editTask 属性控制 - 添加 getAnnotationTaskByIdUsingGet 和 updateAnnotationTaskByIdUsingPut API 接口 - 实现编辑模式下的任务详情加载和表单填充 - 编辑模式下禁用数据集修改和配置模式切换 - 更新模态框标题为动态显示(创建/编辑) - 在任务列表操作菜单中添加编辑按钮 - 编辑模式下只允许修改标签取值,限制模板结构调整 - 添加任务详情加载状态显示
This commit is contained in:
@@ -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({});
|
||||||
setConfigMode("template");
|
|
||||||
setHasSelectedTemplate(false);
|
|
||||||
setSelectedDatasetId(null);
|
|
||||||
setDatasetPreviewData([]);
|
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 () => {
|
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 || [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
await createAnnotationTaskUsingPost(requestData);
|
|
||||||
message.success("创建标注任务成功");
|
if (isEditMode && editTask) {
|
||||||
|
// 编辑模式:调用更新接口
|
||||||
|
await updateAnnotationTaskByIdUsingPut(editTask.id, requestData);
|
||||||
|
message.success("更新标注任务成功");
|
||||||
|
} else {
|
||||||
|
// 创建模式:调用创建接口
|
||||||
|
await createAnnotationTaskUsingPost(requestData);
|
||||||
|
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">
|
||||||
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
|
{!isEditMode && (
|
||||||
<Radio.Button value="template">现有模板</Radio.Button>
|
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
|
||||||
<Radio.Button value="custom">自定义配置</Radio.Button>
|
<Radio.Button value="template">现有模板</Radio.Button>
|
||||||
</Radio.Group>
|
<Radio.Button value="custom">自定义配置</Radio.Button>
|
||||||
|
</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="加载现有模板"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user