You've already forked DataMate
feat(annotation): 添加自定义标注模板配置功能
- 新增 TemplateConfigurationForm 组件用于自定义配置 - 实现模板模式和自定义模式的切换功能 - 添加 generateXmlFromConfig 函数动态生成 XML 配置 - 支持通过表单方式配置数据对象和标签控件 - 移除模板选择时多余的 XML 清空逻辑 - 优化配置预览按钮显示逻辑
This commit is contained in:
@@ -12,6 +12,7 @@ import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
|||||||
import { DatasetType, type Dataset, type DatasetFile } from "@/pages/DataManagement/dataset.model";
|
import { DatasetType, type Dataset, type DatasetFile } from "@/pages/DataManagement/dataset.model";
|
||||||
import type { AnnotationTemplate } from "../../annotation.model";
|
import type { AnnotationTemplate } from "../../annotation.model";
|
||||||
import LabelStudioEmbed from "@/components/business/LabelStudioEmbed";
|
import LabelStudioEmbed from "@/components/business/LabelStudioEmbed";
|
||||||
|
import TemplateConfigurationForm from "../../components/TemplateConfigurationForm";
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ export default function CreateAnnotationTask({
|
|||||||
// Custom template state
|
// Custom template state
|
||||||
const [customXml, setCustomXml] = useState("");
|
const [customXml, setCustomXml] = useState("");
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
|
||||||
|
|
||||||
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
||||||
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
||||||
@@ -171,6 +173,7 @@ export default function CreateAnnotationTask({
|
|||||||
setImageFileCount(0);
|
setImageFileCount(0);
|
||||||
setCustomXml("");
|
setCustomXml("");
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
|
setConfigMode("template");
|
||||||
}
|
}
|
||||||
}, [open, manualForm, autoForm]);
|
}, [open, manualForm, autoForm]);
|
||||||
|
|
||||||
@@ -183,22 +186,78 @@ export default function CreateAnnotationTask({
|
|||||||
setImageFileCount(count);
|
setImageFileCount(count);
|
||||||
}, [selectedFilesMap]);
|
}, [selectedFilesMap]);
|
||||||
|
|
||||||
|
const generateXmlFromConfig = (objects: any[], labels: any[]) => {
|
||||||
|
let xml = '<View>\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 += ` <Choice value="${opt}" />\n`;
|
||||||
|
} else {
|
||||||
|
xml += ` <Label value="${opt}" />\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
xml += ` </${lbl.type}>\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
xml += '</View>';
|
||||||
|
return xml;
|
||||||
|
};
|
||||||
|
|
||||||
const handleManualSubmit = async () => {
|
const handleManualSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await manualForm.validateFields();
|
const values = await manualForm.validateFields();
|
||||||
|
|
||||||
|
let finalLabelConfig = "";
|
||||||
|
|
||||||
|
if (configMode === "template") {
|
||||||
if (!customXml.trim()) {
|
if (!customXml.trim()) {
|
||||||
message.error("请配置标注模板或选择一个现有模板");
|
message.error("请配置标注模板或选择一个现有模板");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
finalLabelConfig = customXml;
|
||||||
|
} else {
|
||||||
|
// Custom mode
|
||||||
|
const objects = values.objects;
|
||||||
|
const labels = values.labels;
|
||||||
|
|
||||||
|
if (!objects || objects.length === 0) {
|
||||||
|
message.error("请至少配置一个数据对象");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!labels || labels.length === 0) {
|
||||||
|
message.error("请至少配置一个标签控件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalLabelConfig = generateXmlFromConfig(objects, labels);
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const requestData = {
|
const requestData = {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
description: values.description,
|
description: values.description,
|
||||||
datasetId: values.datasetId,
|
datasetId: values.datasetId,
|
||||||
templateId: values.templateId, // Can be null/undefined if user just typed XML
|
templateId: configMode === 'template' ? values.templateId : undefined,
|
||||||
labelConfig: customXml, // Pass the custom XML
|
labelConfig: finalLabelConfig,
|
||||||
};
|
};
|
||||||
await createAnnotationTaskUsingPost(requestData);
|
await createAnnotationTaskUsingPost(requestData);
|
||||||
message?.success?.("创建标注任务成功");
|
message?.success?.("创建标注任务成功");
|
||||||
@@ -266,6 +325,21 @@ export default function CreateAnnotationTask({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConfigModeChange = (e: any) => {
|
||||||
|
const mode = e.target.value;
|
||||||
|
setConfigMode(mode);
|
||||||
|
if (mode === "custom") {
|
||||||
|
// Set default values for custom configuration if empty
|
||||||
|
const currentObjects = manualForm.getFieldValue("objects");
|
||||||
|
if (!currentObjects || currentObjects.length === 0) {
|
||||||
|
manualForm.setFieldsValue({
|
||||||
|
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||||
|
labels: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -370,11 +444,22 @@ export default function CreateAnnotationTask({
|
|||||||
{/* 标注模板选择 */}
|
{/* 标注模板选择 */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<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">
|
||||||
|
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
|
||||||
|
<Radio.Button value="template">现有模板</Radio.Button>
|
||||||
|
<Radio.Button value="custom">自定义配置</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
{configMode === 'template' && (
|
||||||
<Button type="link" size="small" onClick={() => setShowPreview(true)} disabled={!customXml}>
|
<Button type="link" size="small" onClick={() => setShowPreview(true)} disabled={!customXml}>
|
||||||
预览配置效果
|
预览配置效果
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</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="加载现有模板 (可选)"
|
||||||
@@ -396,10 +481,6 @@ export default function CreateAnnotationTask({
|
|||||||
onChange={(value, option: any) => {
|
onChange={(value, option: any) => {
|
||||||
if (option && option.config) {
|
if (option && option.config) {
|
||||||
setCustomXml(option.config);
|
setCustomXml(option.config);
|
||||||
} else if (!value) {
|
|
||||||
// If cleared, maybe clear XML? Or keep it?
|
|
||||||
// User might clear selection to say "I am customizing now".
|
|
||||||
// Let's keep it to be safe, or user can clear manually.
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
@@ -429,6 +510,11 @@ export default function CreateAnnotationTask({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-gray-50 p-4 rounded-md border border-gray-200" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||||
|
<TemplateConfigurationForm form={manualForm} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user