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 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>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user