You've already forked DataMate
feat(annotation): 添加标注任务创建对话框的可视化配置功能
- 新增模板编辑标签页支持可视化和XML两种模式 - 实现从表单值同步生成XML配置的功能 - 添加模板选择时自动加载配置到表单的逻辑 - 重构配置模式切换逻辑并优化预览功能 - 将XML编辑器替换为带标签页的可视化配置界面 - 更新模板加载提示信息以反映新的配置方式
This commit is contained in:
@@ -121,6 +121,7 @@ export default function CreateAnnotationTask({
|
|||||||
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 [configMode, setConfigMode] = useState<"template" | "custom">("template");
|
||||||
|
const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual");
|
||||||
|
|
||||||
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
||||||
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
||||||
@@ -174,6 +175,7 @@ export default function CreateAnnotationTask({
|
|||||||
setCustomXml("");
|
setCustomXml("");
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
setConfigMode("template");
|
setConfigMode("template");
|
||||||
|
setTemplateEditTab("visual");
|
||||||
}
|
}
|
||||||
}, [open, manualForm, autoForm]);
|
}, [open, manualForm, autoForm]);
|
||||||
|
|
||||||
@@ -222,23 +224,59 @@ export default function CreateAnnotationTask({
|
|||||||
return xml;
|
return xml;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 从表单值同步生成 XML
|
||||||
|
const syncFormToXml = () => {
|
||||||
|
const objects = manualForm.getFieldValue("objects");
|
||||||
|
const labels = manualForm.getFieldValue("labels");
|
||||||
|
if (objects && objects.length > 0) {
|
||||||
|
const xml = generateXmlFromConfig(objects, labels || []);
|
||||||
|
setCustomXml(xml);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当选择模板时,加载模板配置到表单
|
||||||
|
const handleTemplateSelect = (value: string, option: any) => {
|
||||||
|
if (option && option.config) {
|
||||||
|
setCustomXml(option.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从模板列表中找到完整的模板数据
|
||||||
|
const selectedTemplate = templates.find(t => t.id === value);
|
||||||
|
if (selectedTemplate?.configuration) {
|
||||||
|
const { objects, labels } = selectedTemplate.configuration;
|
||||||
|
manualForm.setFieldsValue({
|
||||||
|
objects: objects || [{ name: "image", type: "Image", value: "$image" }],
|
||||||
|
labels: labels || [],
|
||||||
|
});
|
||||||
|
} else if (option && option.config) {
|
||||||
|
// 如果没有结构化配置,设置默认值
|
||||||
|
manualForm.setFieldsValue({
|
||||||
|
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||||
|
labels: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleManualSubmit = async () => {
|
const handleManualSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await manualForm.validateFields();
|
const values = await manualForm.validateFields();
|
||||||
|
|
||||||
let finalLabelConfig = "";
|
let finalLabelConfig = "";
|
||||||
|
const objects = values.objects;
|
||||||
|
const labels = values.labels;
|
||||||
|
|
||||||
if (configMode === "template") {
|
if (configMode === "template") {
|
||||||
if (!customXml.trim()) {
|
// 模板模式:优先使用可视化配置生成 XML,回退到直接使用 XML 编辑器内容
|
||||||
message.error("请配置标注模板或选择一个现有模板");
|
if (templateEditTab === "visual" && objects && objects.length > 0) {
|
||||||
return;
|
finalLabelConfig = generateXmlFromConfig(objects, labels || []);
|
||||||
|
} else if (customXml.trim()) {
|
||||||
|
finalLabelConfig = customXml;
|
||||||
|
} else {
|
||||||
|
message.error("请配置标注模板或选择一个现有模板");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
finalLabelConfig = customXml;
|
|
||||||
} else {
|
} else {
|
||||||
// Custom mode
|
// 自定义模式
|
||||||
const objects = values.objects;
|
|
||||||
const labels = values.labels;
|
|
||||||
|
|
||||||
if (!objects || objects.length === 0) {
|
if (!objects || objects.length === 0) {
|
||||||
message.error("请至少配置一个数据对象");
|
message.error("请至少配置一个数据对象");
|
||||||
return;
|
return;
|
||||||
@@ -247,7 +285,6 @@ export default function CreateAnnotationTask({
|
|||||||
message.error("请至少配置一个标签控件");
|
message.error("请至少配置一个标签控件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
finalLabelConfig = generateXmlFromConfig(objects, labels);
|
finalLabelConfig = generateXmlFromConfig(objects, labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,15 +365,17 @@ export default function CreateAnnotationTask({
|
|||||||
const handleConfigModeChange = (e: any) => {
|
const handleConfigModeChange = (e: any) => {
|
||||||
const mode = e.target.value;
|
const mode = e.target.value;
|
||||||
setConfigMode(mode);
|
setConfigMode(mode);
|
||||||
if (mode === "custom") {
|
// 两种模式都需要初始化默认值
|
||||||
// Set default values for custom configuration if empty
|
const currentObjects = manualForm.getFieldValue("objects");
|
||||||
const currentObjects = manualForm.getFieldValue("objects");
|
if (!currentObjects || currentObjects.length === 0) {
|
||||||
if (!currentObjects || currentObjects.length === 0) {
|
manualForm.setFieldsValue({
|
||||||
manualForm.setFieldsValue({
|
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||||
objects: [{ name: "image", type: "Image", value: "$image" }],
|
labels: [],
|
||||||
labels: [],
|
});
|
||||||
});
|
}
|
||||||
}
|
// 切换到模板模式时,重置 tab 到可视化
|
||||||
|
if (mode === "template") {
|
||||||
|
setTemplateEditTab("visual");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -451,24 +490,40 @@ export default function CreateAnnotationTask({
|
|||||||
<Radio.Button value="custom">自定义配置</Radio.Button>
|
<Radio.Button value="custom">自定义配置</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|
||||||
{configMode === 'template' && (
|
<Button
|
||||||
<Button type="link" size="small" onClick={() => setShowPreview(true)} disabled={!customXml}>
|
type="link"
|
||||||
预览配置效果
|
size="small"
|
||||||
</Button>
|
onClick={() => {
|
||||||
)}
|
// 如果在可视化模式,先同步生成 XML
|
||||||
|
if (configMode === 'template' && templateEditTab === 'visual') {
|
||||||
|
syncFormToXml();
|
||||||
|
} else if (configMode === 'custom') {
|
||||||
|
// 自定义模式也从表单生成
|
||||||
|
const objects = manualForm.getFieldValue("objects");
|
||||||
|
const labels = manualForm.getFieldValue("labels");
|
||||||
|
if (objects && objects.length > 0) {
|
||||||
|
const xml = generateXmlFromConfig(objects, labels || []);
|
||||||
|
setCustomXml(xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowPreview(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
预览配置效果
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{configMode === 'template' ? (
|
{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="加载现有模板"
|
||||||
name="templateId"
|
name="templateId"
|
||||||
style={{ marginBottom: 12 }}
|
style={{ marginBottom: 12 }}
|
||||||
help="选择模板后,配置代码将自动填充到下方编辑器中,您可以继续修改。"
|
help="选择模板后,配置将自动填充到可视化编辑器中,您可以继续修改。"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择一个模板作为基础(可选)"
|
placeholder="选择一个模板作为基础"
|
||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
@@ -478,11 +533,7 @@ export default function CreateAnnotationTask({
|
|||||||
title: template.description,
|
title: template.description,
|
||||||
config: template.labelConfig,
|
config: template.labelConfig,
|
||||||
}))}
|
}))}
|
||||||
onChange={(value, option: any) => {
|
onChange={handleTemplateSelect}
|
||||||
if (option && option.config) {
|
|
||||||
setCustomXml(option.config);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||||
@@ -496,19 +547,46 @@ export default function CreateAnnotationTask({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Tabs
|
||||||
label="XML 配置编辑器"
|
activeKey={templateEditTab}
|
||||||
required
|
onChange={(key) => {
|
||||||
style={{ marginBottom: 0 }}
|
// 切换到 XML 时,从表单同步生成 XML
|
||||||
>
|
if (key === "xml") {
|
||||||
<TextArea
|
syncFormToXml();
|
||||||
rows={8}
|
}
|
||||||
value={customXml}
|
setTemplateEditTab(key as "visual" | "xml");
|
||||||
onChange={(e) => setCustomXml(e.target.value)}
|
}}
|
||||||
placeholder="<View>...</View>"
|
size="small"
|
||||||
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
items={[
|
||||||
/>
|
{
|
||||||
</Form.Item>
|
key: "visual",
|
||||||
|
label: "可视化配置",
|
||||||
|
children: (
|
||||||
|
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
|
||||||
|
<TemplateConfigurationForm form={manualForm} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "xml",
|
||||||
|
label: "XML编辑器(高级)",
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-xs text-gray-500">
|
||||||
|
直接编辑 Label Studio XML 配置。注意:在此修改后切换回可视化配置可能会丢失部分高级设置。
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
rows={10}
|
||||||
|
value={customXml}
|
||||||
|
onChange={(e) => setCustomXml(e.target.value)}
|
||||||
|
placeholder="<View>...</View>"
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
<div className="bg-gray-50 p-4 rounded-md border border-gray-200" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user