feat(annotation): 添加自定义标注模板配置功能

- 新增 TemplateConfigurationForm 组件用于自定义配置
- 实现模板模式和自定义模式的切换功能
- 添加 generateXmlFromConfig 函数动态生成 XML 配置
- 支持通过表单方式配置数据对象和标签控件
- 移除模板选择时多余的 XML 清空逻辑
- 优化配置预览按钮显示逻辑
This commit is contained in:
2026-01-18 21:32:01 +08:00
parent fc978620a7
commit 5057457329

View File

@@ -12,6 +12,7 @@ import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
import { DatasetType, type Dataset, type DatasetFile } from "@/pages/DataManagement/dataset.model";
import type { AnnotationTemplate } from "../../annotation.model";
import LabelStudioEmbed from "@/components/business/LabelStudioEmbed";
import TemplateConfigurationForm from "../../components/TemplateConfigurationForm";
const { Option } = Select;
@@ -119,6 +120,7 @@ export default function CreateAnnotationTask({
// Custom template state
const [customXml, setCustomXml] = useState("");
const [showPreview, setShowPreview] = useState(false);
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
const [selectAllClasses, setSelectAllClasses] = useState(true);
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
@@ -171,6 +173,7 @@ export default function CreateAnnotationTask({
setImageFileCount(0);
setCustomXml("");
setShowPreview(false);
setConfigMode("template");
}
}, [open, manualForm, autoForm]);
@@ -183,13 +186,69 @@ export default function CreateAnnotationTask({
setImageFileCount(count);
}, [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 () => {
try {
const values = await manualForm.validateFields();
if (!customXml.trim()) {
message.error("请配置标注模板或选择一个现有模板");
return;
let finalLabelConfig = "";
if (configMode === "template") {
if (!customXml.trim()) {
message.error("请配置标注模板或选择一个现有模板");
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);
@@ -197,8 +256,8 @@ export default function CreateAnnotationTask({
name: values.name,
description: values.description,
datasetId: values.datasetId,
templateId: values.templateId, // Can be null/undefined if user just typed XML
labelConfig: customXml, // Pass the custom XML
templateId: configMode === 'template' ? values.templateId : undefined,
labelConfig: finalLabelConfig,
};
await createAnnotationTaskUsingPost(requestData);
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 (
<>
<Modal
@@ -370,65 +444,77 @@ export default function CreateAnnotationTask({
{/* 标注模板选择 */}
<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>
<Button type="link" size="small" onClick={() => setShowPreview(true)} disabled={!customXml}>
</Button>
<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>
)}
</div>
</div>
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<Form.Item
label="加载现有模板 (可选)"
name="templateId"
style={{ marginBottom: 12 }}
help="选择模板后,配置代码将自动填充到下方编辑器中,您可以继续修改。"
>
<Select
placeholder="选择一个模板作为基础(可选)"
showSearch
allowClear
optionFilterProp="label"
options={templates.map((template) => ({
label: template.name,
value: template.id,
title: template.description,
config: template.labelConfig,
}))}
onChange={(value, option: any) => {
if (option && 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) => (
<div>
<div style={{ fontWeight: 500 }}>{option.label}</div>
{option.data.title && (
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
{option.data.title}
</div>
)}
</div>
)}
/>
</Form.Item>
{configMode === 'template' ? (
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<Form.Item
label="加载现有模板 (可选)"
name="templateId"
style={{ marginBottom: 12 }}
help="选择模板后,配置代码将自动填充到下方编辑器中,您可以继续修改。"
>
<Select
placeholder="选择一个模板作为基础(可选)"
showSearch
allowClear
optionFilterProp="label"
options={templates.map((template) => ({
label: template.name,
value: template.id,
title: template.description,
config: template.labelConfig,
}))}
onChange={(value, option: any) => {
if (option && option.config) {
setCustomXml(option.config);
}
}}
optionRender={(option) => (
<div>
<div style={{ fontWeight: 500 }}>{option.label}</div>
{option.data.title && (
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
{option.data.title}
</div>
)}
</div>
)}
/>
</Form.Item>
<Form.Item
label="XML 配置编辑器"
required
style={{ marginBottom: 0 }}
>
<TextArea
rows={8}
value={customXml}
onChange={(e) => setCustomXml(e.target.value)}
placeholder="<View>...</View>"
style={{ fontFamily: 'monospace', fontSize: 12 }}
/>
</Form.Item>
</div>
<Form.Item
label="XML 配置编辑器"
required
style={{ marginBottom: 0 }}
>
<TextArea
rows={8}
value={customXml}
onChange={(e) => setCustomXml(e.target.value)}
placeholder="<View>...</View>"
style={{ fontFamily: 'monospace', fontSize: 12 }}
/>
</Form.Item>
</div>
) : (
<div className="bg-gray-50 p-4 rounded-md border border-gray-200" style={{ maxHeight: '400px', overflowY: 'auto' }}>
<TemplateConfigurationForm form={manualForm} />
</div>
)}
</Form>
),