feat(annotation): 添加标注任务创建对话框的可视化配置功能

- 新增模板编辑标签页支持可视化和XML两种模式
- 实现从表单值同步生成XML配置的功能
- 添加模板选择时自动加载配置到表单的逻辑
- 重构配置模式切换逻辑并优化预览功能
- 将XML编辑器替换为带标签页的可视化配置界面
- 更新模板加载提示信息以反映新的配置方式
This commit is contained in:
2026-01-19 10:26:37 +08:00
parent 4a986b5466
commit bc43d442fc

View File

@@ -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' }}>