feat(annotation): 添加标注任务预览功能

- 新增 previewTaskData 状态管理预览数据
- 实现 generateExampleData 函数根据对象配置生成示例数据
- 支持多种数据类型(图片、音频、视频、文本等)的示例生成
- 优化预览按钮逻辑,自动生成适配的示例数据
- 移除固定示例数据,使用动态生成的数据进行预览
- 调整模板列表组件的分页参数以修复数据获取问题
This commit is contained in:
2026-01-19 10:32:34 +08:00
parent bc43d442fc
commit 163c93142e
2 changed files with 75 additions and 24 deletions

View File

@@ -116,10 +116,11 @@ export default function CreateAnnotationTask({
const [submitting, setSubmitting] = useState(false);
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
const [activeMode, setActiveMode] = useState<"manual" | "auto">("manual");
// Custom template state
const [customXml, setCustomXml] = useState("");
const [showPreview, setShowPreview] = useState(false);
const [previewTaskData, setPreviewTaskData] = useState<Record<string, any>>({});
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual");
@@ -174,6 +175,7 @@ export default function CreateAnnotationTask({
setImageFileCount(0);
setCustomXml("");
setShowPreview(false);
setPreviewTaskData({});
setConfigMode("template");
setTemplateEditTab("visual");
}
@@ -190,7 +192,7 @@ export default function CreateAnnotationTask({
const generateXmlFromConfig = (objects: any[], labels: any[]) => {
let xml = '<View>\n';
// Objects
if (objects) {
objects.forEach((obj: any) => {
@@ -203,9 +205,9 @@ export default function CreateAnnotationTask({
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) => {
@@ -234,6 +236,58 @@ export default function CreateAnnotationTask({
}
};
// 根据 objects 配置生成预览用的示例数据
const generateExampleData = (objects: any[]) => {
const exampleUrls: Record<string, string> = {
Image: "https://labelstud.io/images/opa-header.png",
Audio: "https://labelstud.io/files/sample.wav",
Video: "https://labelstud.io/files/sample.mp4",
};
const exampleTexts: Record<string, string> = {
Text: "这是示例文本,用于预览标注界面。",
HyperText: "<p>这是示例 HTML 内容</p>",
Header: "示例标题",
Paragraphs: "段落一\n\n段落二\n\n段落三",
};
const data: Record<string, any> = {};
if (!objects || objects.length === 0) {
// 默认数据
return {
image: exampleUrls.Image,
text: exampleTexts.Text,
audio: exampleUrls.Audio,
};
}
objects.forEach((obj: any) => {
if (!obj?.name || !obj?.value) return;
// 变量名从 $varName 中提取
const varName = obj.value.startsWith("$") ? obj.value.slice(1) : obj.name;
if (exampleUrls[obj.type]) {
data[varName] = exampleUrls[obj.type];
} else if (exampleTexts[obj.type]) {
data[varName] = exampleTexts[obj.type];
} 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;
}
}
});
return data;
};
// 当选择模板时,加载模板配置到表单
const handleTemplateSelect = (value: string, option: any) => {
if (option && option.config) {
@@ -483,7 +537,7 @@ 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>
<div className="flex gap-2">
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
<Radio.Button value="template"></Radio.Button>
@@ -494,18 +548,19 @@ export default function CreateAnnotationTask({
type="link"
size="small"
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);
}
const objects = manualForm.getFieldValue("objects");
const labels = manualForm.getFieldValue("labels");
// 生成 XML
if (objects && objects.length > 0) {
const xml = generateXmlFromConfig(objects, labels || []);
setCustomXml(xml);
}
// 生成适配的示例数据
const exampleData = generateExampleData(objects);
setPreviewTaskData(exampleData);
setShowPreview(true);
}}
>
@@ -513,7 +568,7 @@ export default function CreateAnnotationTask({
</Button>
</div>
</div>
{configMode === 'template' ? (
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<Form.Item
@@ -713,15 +768,11 @@ export default function CreateAnnotationTask({
>
<div style={{ height: '600px', overflow: 'hidden' }}>
{showPreview && (
<LabelStudioEmbed
<LabelStudioEmbed
config={customXml}
task={{
id: 1,
data: {
image: "https://labelstud.io/images/opa-header.png",
text: "这是示例文本,用于预览标注界面。",
audio: "https://labelstud.io/files/sample.wav"
}
data: previewTaskData,
}}
/>
)}