You've already forked DataMate
feat(annotation): 替换模板配置表单为树形编辑器组件
- 移除 TemplateConfigurationForm 组件并引入 TemplateConfigurationTreeEditor - 使用 useTagConfig Hook 获取标签配置 - 将自定义XML状态 customXml 替换为 labelConfig - 删除模板编辑标签页和选择模板状态管理 - 更新XML解析逻辑支持更多对象和标注控件类型 - 添加配置验证功能确保至少包含数据对象和标注控件 - 在模板详情页面使用树形编辑器显示配置详情 - 更新任务创建页面集成新的树形配置编辑器 - 调整预览数据生成功能适配新的XML解析方式
This commit is contained in:
@@ -13,7 +13,8 @@ import {
|
||||
import { type Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate, AnnotationTask } from "../../annotation.model";
|
||||
import LabelStudioEmbed from "@/components/business/LabelStudioEmbed";
|
||||
import TemplateConfigurationForm from "../../components/TemplateConfigurationForm";
|
||||
import TemplateConfigurationTreeEditor from "../../components/TemplateConfigurationTreeEditor";
|
||||
import { useTagConfig } from "@/hooks/useTagConfig";
|
||||
|
||||
interface AnnotationTaskDialogProps {
|
||||
open: boolean;
|
||||
@@ -37,15 +38,10 @@ export default function CreateAnnotationTask({
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
|
||||
// Custom template state
|
||||
const [customXml, setCustomXml] = useState("");
|
||||
const [labelConfig, setLabelConfig] = useState("");
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [previewTaskData, setPreviewTaskData] = useState<Record<string, any>>({});
|
||||
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
|
||||
// 模板编辑模式切换(可视化 vs XML)
|
||||
const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual");
|
||||
// 是否已选择模板(用于启用受限编辑模式)
|
||||
const [hasSelectedTemplate, setHasSelectedTemplate] = useState(false);
|
||||
|
||||
// 数据集预览相关状态
|
||||
const [datasetPreviewVisible, setDatasetPreviewVisible] = useState(false);
|
||||
@@ -63,6 +59,7 @@ export default function CreateAnnotationTask({
|
||||
|
||||
// 任务详情加载状态(编辑模式)
|
||||
const [taskDetailLoading, setTaskDetailLoading] = useState(false);
|
||||
const { config: tagConfig } = useTagConfig(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -101,7 +98,7 @@ export default function CreateAnnotationTask({
|
||||
if (open) {
|
||||
manualForm.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
setCustomXml("");
|
||||
setLabelConfig("");
|
||||
setShowPreview(false);
|
||||
setPreviewTaskData({});
|
||||
setDatasetPreviewData([]);
|
||||
@@ -122,23 +119,14 @@ export default function CreateAnnotationTask({
|
||||
setSelectedDatasetId(taskDetail.datasetId);
|
||||
|
||||
// 获取实际的 labelConfig(优先使用任务自身的配置,回退到模板配置)
|
||||
const labelConfig = taskDetail.labelConfig || taskDetail.template?.labelConfig;
|
||||
const configXml = taskDetail.labelConfig || taskDetail.template?.labelConfig;
|
||||
|
||||
// 设置 XML 配置用于预览
|
||||
if (labelConfig) {
|
||||
setCustomXml(labelConfig);
|
||||
// 始终从 XML 解析配置,确保数据一致性
|
||||
const parsed = parseXmlToConfig(labelConfig);
|
||||
manualForm.setFieldsValue({
|
||||
objects: parsed.objects,
|
||||
labels: parsed.labels,
|
||||
});
|
||||
if (configXml) {
|
||||
setLabelConfig(configXml);
|
||||
}
|
||||
|
||||
// 编辑模式始终使用 custom 配置模式(不改变结构,只改标签)
|
||||
// 编辑模式始终使用 custom 配置模式(不改变结构,只改属性)
|
||||
setConfigMode("custom");
|
||||
// 编辑模式下启用受限编辑
|
||||
setHasSelectedTemplate(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -151,7 +139,6 @@ export default function CreateAnnotationTask({
|
||||
} else {
|
||||
// 创建模式:重置为默认状态
|
||||
setConfigMode("template");
|
||||
setHasSelectedTemplate(false);
|
||||
setSelectedDatasetId(null);
|
||||
}
|
||||
}
|
||||
@@ -243,229 +230,203 @@ export default function CreateAnnotationTask({
|
||||
}
|
||||
};
|
||||
|
||||
// 从 Label Studio XML 配置解析出 objects 和 labels
|
||||
const parseXmlToConfig = (xml: string): { objects: any[], labels: any[] } => {
|
||||
const objects: any[] = [];
|
||||
const labels: any[] = [];
|
||||
const DEFAULT_OBJECT_TAGS = [
|
||||
"Image",
|
||||
"Text",
|
||||
"Audio",
|
||||
"Video",
|
||||
"HyperText",
|
||||
"PDF",
|
||||
"Markdown",
|
||||
"Paragraphs",
|
||||
"Table",
|
||||
"AudioPlus",
|
||||
"Timeseries",
|
||||
"TimeSeries",
|
||||
"Vector",
|
||||
"Chat",
|
||||
];
|
||||
const DEFAULT_LABELING_CONTROL_TAGS = [
|
||||
"Choices",
|
||||
"Labels",
|
||||
"RectangleLabels",
|
||||
"PolygonLabels",
|
||||
"EllipseLabels",
|
||||
"KeyPointLabels",
|
||||
"BrushLabels",
|
||||
"TextArea",
|
||||
"Number",
|
||||
"DateTime",
|
||||
"Rating",
|
||||
"Taxonomy",
|
||||
"ParagraphLabels",
|
||||
"HyperTextLabels",
|
||||
"Relations",
|
||||
"Relation",
|
||||
"Pairwise",
|
||||
"TimeseriesLabels",
|
||||
"TimeSeriesLabels",
|
||||
"VectorLabels",
|
||||
"VideoRectangle",
|
||||
"MagicWand",
|
||||
"BitmaskLabels",
|
||||
];
|
||||
|
||||
const resolveObjectTags = () => {
|
||||
const configTags = tagConfig?.objects ? Object.keys(tagConfig.objects) : [];
|
||||
return new Set(configTags.length > 0 ? configTags : DEFAULT_OBJECT_TAGS);
|
||||
};
|
||||
|
||||
const resolveLabelingControlTags = () => {
|
||||
if (tagConfig?.controls) {
|
||||
const labelingTags = Object.entries(tagConfig.controls)
|
||||
.filter(([, config]) => config.category === "labeling")
|
||||
.map(([tag]) => tag);
|
||||
if (labelingTags.length > 0) {
|
||||
return new Set(labelingTags);
|
||||
}
|
||||
}
|
||||
return new Set(DEFAULT_LABELING_CONTROL_TAGS);
|
||||
};
|
||||
|
||||
const parseXmlElements = (xml: string): Element[] => {
|
||||
if (!xml) return [];
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
|
||||
// 数据对象类型列表
|
||||
const objectTypes = ["Image", "Text", "Audio", "Video", "HyperText", "Header", "Paragraphs", "TimeSeries", "TimeSeriesChannel"];
|
||||
// 标签控件类型列表
|
||||
const controlTypes = ["Choices", "Labels", "RectangleLabels", "PolygonLabels", "EllipseLabels", "KeyPointLabels", "BrushLabels", "TextArea", "Number", "DateTime", "Rating", "Taxonomy"];
|
||||
|
||||
// 解析数据对象
|
||||
objectTypes.forEach(type => {
|
||||
const elements = doc.getElementsByTagName(type);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i];
|
||||
const name = el.getAttribute("name") || "";
|
||||
const value = el.getAttribute("value") || "";
|
||||
if (name) {
|
||||
objects.push({ name, type, value });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 解析标签控件
|
||||
controlTypes.forEach(type => {
|
||||
const elements = doc.getElementsByTagName(type);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i];
|
||||
const fromName = el.getAttribute("name") || "";
|
||||
const toName = el.getAttribute("toName") || "";
|
||||
const required = el.getAttribute("required") === "true";
|
||||
|
||||
if (fromName) {
|
||||
const label: any = {
|
||||
fromName,
|
||||
toName,
|
||||
type,
|
||||
required,
|
||||
};
|
||||
|
||||
// 解析选项/标签值
|
||||
if (type === "Choices") {
|
||||
const choices: string[] = [];
|
||||
const choiceElements = el.getElementsByTagName("Choice");
|
||||
for (let j = 0; j < choiceElements.length; j++) {
|
||||
const value = choiceElements[j].getAttribute("value");
|
||||
if (value) choices.push(value);
|
||||
}
|
||||
label.options = choices;
|
||||
} else if (["Labels", "RectangleLabels", "PolygonLabels", "EllipseLabels", "KeyPointLabels", "BrushLabels"].includes(type)) {
|
||||
const labelValues: string[] = [];
|
||||
const labelElements = el.getElementsByTagName("Label");
|
||||
for (let j = 0; j < labelElements.length; j++) {
|
||||
const value = labelElements[j].getAttribute("value");
|
||||
if (value) labelValues.push(value);
|
||||
}
|
||||
label.labels = labelValues;
|
||||
}
|
||||
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to parse XML config:", e);
|
||||
if (doc.getElementsByTagName("parsererror").length > 0) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(doc.getElementsByTagName("*"));
|
||||
} catch (error) {
|
||||
console.error("解析 XML 失败", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return { objects, labels };
|
||||
};
|
||||
|
||||
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 extractObjectsFromLabelConfig = (xml: string) => {
|
||||
const objectTags = resolveObjectTags();
|
||||
const elements = parseXmlElements(xml);
|
||||
return elements
|
||||
.filter((element) => objectTags.has(element.tagName))
|
||||
.map((element) => ({
|
||||
name: element.getAttribute("name") || "",
|
||||
type: element.tagName,
|
||||
value: element.getAttribute("value") || "",
|
||||
}))
|
||||
.filter((item) => item.name || item.value);
|
||||
};
|
||||
|
||||
// 根据 objects 配置生成预览用的示例数据
|
||||
const generateExampleData = (objects: any[]) => {
|
||||
const exampleUrls: Record<string, string> = {
|
||||
const generatePreviewTaskDataFromLabelConfig = (xml: string) => {
|
||||
const exampleDataByType: Record<string, any> = {
|
||||
Image: "https://labelstud.io/images/opa-header.png",
|
||||
Audio: "https://labelstud.io/files/sample.wav",
|
||||
AudioPlus: "https://labelstud.io/files/sample.wav",
|
||||
Video: "https://labelstud.io/files/sample.mp4",
|
||||
};
|
||||
const exampleTexts: Record<string, string> = {
|
||||
Text: "这是示例文本,用于预览标注界面。",
|
||||
HyperText: "<p>这是示例 HTML 内容</p>",
|
||||
Header: "示例标题",
|
||||
Markdown: "# 示例标题\n\n这里是示例 Markdown 内容。",
|
||||
Paragraphs: "段落一\n\n段落二\n\n段落三",
|
||||
PDF: "https://labelstud.io/files/sample.pdf",
|
||||
Table: [
|
||||
{ key: "字段A", value: "示例值A" },
|
||||
{ key: "字段B", value: "示例值B" },
|
||||
],
|
||||
Chat: [
|
||||
{ text: "你好,我想了解一下产品。", author: "user" },
|
||||
{ text: "当然可以,请告诉我你的需求。", author: "assistant" },
|
||||
],
|
||||
Timeseries: "https://labelstud.io/files/sample.csv",
|
||||
TimeSeries: "https://labelstud.io/files/sample.csv",
|
||||
Vector: [0.12, 0.52, 0.33],
|
||||
};
|
||||
|
||||
const data: Record<string, any> = {};
|
||||
|
||||
if (!objects || objects.length === 0) {
|
||||
// 默认数据
|
||||
const objects = extractObjectsFromLabelConfig(xml);
|
||||
if (objects.length === 0) {
|
||||
return {
|
||||
image: exampleUrls.Image,
|
||||
text: exampleTexts.Text,
|
||||
audio: exampleUrls.Audio,
|
||||
image: exampleDataByType.Image,
|
||||
text: exampleDataByType.Text,
|
||||
audio: exampleDataByType.Audio,
|
||||
};
|
||||
}
|
||||
|
||||
objects.forEach((obj: any) => {
|
||||
if (!obj?.name || !obj?.value) return;
|
||||
// 变量名从 $varName 中提取
|
||||
const varName = obj.value.startsWith("$") ? obj.value.slice(1) : obj.name;
|
||||
const data: Record<string, any> = {};
|
||||
objects.forEach((obj) => {
|
||||
const name = obj.name || "";
|
||||
const value = obj.value || "";
|
||||
const varName = value.startsWith("$") ? value.slice(1) : name || value;
|
||||
if (!varName) return;
|
||||
|
||||
if (exampleUrls[obj.type]) {
|
||||
data[varName] = exampleUrls[obj.type];
|
||||
} else if (exampleTexts[obj.type]) {
|
||||
data[varName] = exampleTexts[obj.type];
|
||||
if (exampleDataByType[obj.type]) {
|
||||
data[varName] = exampleDataByType[obj.type];
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerName = varName.toLowerCase();
|
||||
if (lowerName.includes("image") || lowerName.includes("img")) {
|
||||
data[varName] = exampleDataByType.Image;
|
||||
} else if (lowerName.includes("audio") || lowerName.includes("sound")) {
|
||||
data[varName] = exampleDataByType.Audio;
|
||||
} else if (lowerName.includes("video")) {
|
||||
data[varName] = exampleDataByType.Video;
|
||||
} else if (lowerName.includes("chat")) {
|
||||
data[varName] = exampleDataByType.Chat;
|
||||
} else {
|
||||
// 未知类型,尝试根据名称猜测
|
||||
const lowerName = varName.toLowerCase();
|
||||
if (lowerName.includes("image") || lowerName.includes("img")) {
|
||||
data[varName] = exampleUrls.Image;
|
||||
} else if (lowerName.includes("audio") || lowerName.includes("sound")) {
|
||||
data[varName] = exampleUrls.Audio;
|
||||
} else if (lowerName.includes("video")) {
|
||||
data[varName] = exampleUrls.Video;
|
||||
} else {
|
||||
data[varName] = exampleTexts.Text;
|
||||
}
|
||||
data[varName] = exampleDataByType.Text;
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// 当选择模板时,加载模板配置到表单
|
||||
// 当选择模板时,加载 XML 配置到树编辑器(仅快速填充)
|
||||
const handleTemplateSelect = (value: string, option: any) => {
|
||||
// 处理清除选择的情况
|
||||
if (!value) {
|
||||
setHasSelectedTemplate(false);
|
||||
setCustomXml("");
|
||||
setLabelConfig("");
|
||||
return;
|
||||
}
|
||||
|
||||
setHasSelectedTemplate(true);
|
||||
const selectedTemplate = templates.find((template) => template.id === value);
|
||||
const configXml = selectedTemplate?.labelConfig || option?.config || "";
|
||||
setLabelConfig(configXml);
|
||||
};
|
||||
|
||||
if (option && option.config) {
|
||||
setCustomXml(option.config);
|
||||
const validateLabelConfigForSubmit = () => {
|
||||
const xml = labelConfig.trim();
|
||||
if (!xml) {
|
||||
message.error("请配置标注模板");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从模板列表中找到完整的模板数据
|
||||
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 elements = parseXmlElements(xml);
|
||||
if (elements.length === 0) {
|
||||
message.error("标注配置 XML 格式有误");
|
||||
return false;
|
||||
}
|
||||
|
||||
const objectTags = resolveObjectTags();
|
||||
const labelingControlTags = resolveLabelingControlTags();
|
||||
const objectCount = elements.filter((element) => objectTags.has(element.tagName)).length;
|
||||
const labelingControlCount = elements.filter((element) => labelingControlTags.has(element.tagName)).length;
|
||||
|
||||
if (objectCount === 0) {
|
||||
message.error("至少需要一个数据对象标签");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (labelingControlCount === 0) {
|
||||
message.error("至少需要一个标注控件标签");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleManualSubmit = async () => {
|
||||
try {
|
||||
const values = await manualForm.validateFields();
|
||||
|
||||
let finalLabelConfig = "";
|
||||
const objects = values.objects;
|
||||
const labels = values.labels;
|
||||
|
||||
if (configMode === "template") {
|
||||
// 模板模式:优先使用可视化配置生成 XML,回退到直接使用 XML 编辑器内容
|
||||
if (templateEditTab === "visual" && objects && objects.length > 0) {
|
||||
finalLabelConfig = generateXmlFromConfig(objects, labels || []);
|
||||
} else if (customXml.trim()) {
|
||||
finalLabelConfig = customXml;
|
||||
} else {
|
||||
message.error("请配置标注模板或选择一个现有模板");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 自定义模式
|
||||
if (!objects || objects.length === 0) {
|
||||
message.error("请至少配置一个数据对象");
|
||||
return;
|
||||
}
|
||||
if (!labels || labels.length === 0) {
|
||||
message.error("请至少配置一个标签控件");
|
||||
return;
|
||||
}
|
||||
finalLabelConfig = generateXmlFromConfig(objects, labels);
|
||||
if (!validateLabelConfigForSubmit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
@@ -473,13 +434,8 @@ export default function CreateAnnotationTask({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: configMode === 'template' ? values.templateId : undefined,
|
||||
labelConfig: finalLabelConfig,
|
||||
// 编辑模式需要传递配置结构,用于后端保存
|
||||
configuration: {
|
||||
objects: objects || [],
|
||||
labels: labels || [],
|
||||
},
|
||||
templateId: configMode === "template" ? values.templateId : undefined,
|
||||
labelConfig: labelConfig.trim(),
|
||||
};
|
||||
|
||||
if (isEditMode && editTask) {
|
||||
@@ -505,17 +461,8 @@ export default function CreateAnnotationTask({
|
||||
const handleConfigModeChange = (e: any) => {
|
||||
const mode = e.target.value;
|
||||
setConfigMode(mode);
|
||||
// 两种模式都需要初始化默认值
|
||||
const currentObjects = manualForm.getFieldValue("objects");
|
||||
if (!currentObjects || currentObjects.length === 0) {
|
||||
manualForm.setFieldsValue({
|
||||
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||
labels: [],
|
||||
});
|
||||
}
|
||||
// 切换到模板模式时,重置 tab 到可视化
|
||||
if (mode === "template") {
|
||||
setTemplateEditTab("visual");
|
||||
if (mode === "custom") {
|
||||
manualForm.setFieldsValue({ templateId: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -647,19 +594,13 @@ export default function CreateAnnotationTask({
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const objects = manualForm.getFieldValue("objects");
|
||||
const labels = manualForm.getFieldValue("labels");
|
||||
|
||||
// 生成 XML
|
||||
if (objects && objects.length > 0) {
|
||||
const xml = generateXmlFromConfig(objects, labels || []);
|
||||
setCustomXml(xml);
|
||||
if (!labelConfig.trim()) {
|
||||
message.warning("请先配置标注模板");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成适配的示例数据
|
||||
const exampleData = generateExampleData(objects);
|
||||
const exampleData = generatePreviewTaskDataFromLabelConfig(labelConfig);
|
||||
setPreviewTaskData(exampleData);
|
||||
|
||||
setShowPreview(true);
|
||||
}}
|
||||
>
|
||||
@@ -672,14 +613,14 @@ export default function CreateAnnotationTask({
|
||||
// 编辑模式:只允许修改标签取值
|
||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
<div className="text-sm text-gray-500 mb-3 bg-blue-50 p-2 rounded border border-blue-200">
|
||||
编辑模式下,模板结构(数据对象、控件类型等)不可修改,仅可修改来源名称、标签/选项的取值。
|
||||
</div>
|
||||
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
|
||||
<TemplateConfigurationForm
|
||||
form={manualForm}
|
||||
restrictedMode={true}
|
||||
/>
|
||||
编辑模式下,模板结构(数据对象、控件类型等)不可修改,仅可修改属性值(来源名称、标签/选项取值等)。
|
||||
</div>
|
||||
<TemplateConfigurationTreeEditor
|
||||
value={labelConfig}
|
||||
onChange={setLabelConfig}
|
||||
readOnlyStructure={true}
|
||||
height={360}
|
||||
/>
|
||||
</div>
|
||||
) : configMode === 'template' ? (
|
||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
@@ -688,6 +629,7 @@ export default function CreateAnnotationTask({
|
||||
name="templateId"
|
||||
style={{ marginBottom: 12 }}
|
||||
help="选择模板后,配置将自动填充到可视化编辑器中,您可以继续修改。"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择一个模板作为基础"
|
||||
@@ -714,16 +656,19 @@ export default function CreateAnnotationTask({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
|
||||
<TemplateConfigurationForm
|
||||
form={manualForm}
|
||||
restrictedMode={hasSelectedTemplate}
|
||||
/>
|
||||
</div>
|
||||
<TemplateConfigurationTreeEditor
|
||||
value={labelConfig}
|
||||
onChange={setLabelConfig}
|
||||
height={360}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||
<TemplateConfigurationForm form={manualForm} />
|
||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
<TemplateConfigurationTreeEditor
|
||||
value={labelConfig}
|
||||
onChange={setLabelConfig}
|
||||
height={360}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -745,7 +690,7 @@ export default function CreateAnnotationTask({
|
||||
<div style={{ height: '600px', overflow: 'hidden' }}>
|
||||
{showPreview && (
|
||||
<LabelStudioEmbed
|
||||
config={customXml}
|
||||
config={labelConfig}
|
||||
task={{
|
||||
id: 1,
|
||||
data: previewTaskData,
|
||||
|
||||
Reference in New Issue
Block a user