feat(annotation): 添加标注任务自定义配置功能

- 新增 LabelStudioEmbed 组件用于嵌入式标注界面预览
- 在创建标注任务对话框中添加 XML 配置编辑器
- 支持从现有模板加载配置并进行自定义修改
- 实现标注界面实时预览功能
- 后端支持直接传递 label_config 覆盖模板配置
- 更新 CreateAnnotationTaskRequest 模型添加 labelConfig 字段
This commit is contained in:
2026-01-18 14:12:12 +08:00
parent 87c2ef8a58
commit 01dcd16a98
4 changed files with 429 additions and 223 deletions

View File

@@ -0,0 +1,120 @@
import { useEffect, useRef, useState, useMemo } from "react";
import { Spin, message } from "antd";
const LSF_IFRAME_SRC = "/lsf/lsf.html";
export interface LabelStudioEmbedProps {
config: string;
task?: any;
user?: any;
interfaces?: string[];
height?: string | number;
className?: string;
onSubmit?: (result: any) => void;
onUpdate?: (result: any) => void;
onError?: (error: any) => void;
}
export default function LabelStudioEmbed({
config,
task = { id: 1, data: { text: "Preview Text" } },
user = { id: "preview-user" },
interfaces = [
"panel",
"update",
"controls",
"side-column",
"annotations:tabs",
"annotations:menu",
"annotations:current",
"annotations:history",
],
height = "100%",
className = "",
onSubmit,
onUpdate,
onError,
}: LabelStudioEmbedProps) {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [iframeReady, setIframeReady] = useState(false);
const [lsReady, setLsReady] = useState(false);
const origin = useMemo(() => window.location.origin, []);
const postToIframe = (type: string, payload?: any) => {
const win = iframeRef.current?.contentWindow;
if (!win) return;
win.postMessage({ type, payload }, origin);
};
useEffect(() => {
setIframeReady(false);
setLsReady(false);
}, [config]); // Reset when config changes
// Initialize LS when iframe is ready and config is available
useEffect(() => {
if (iframeReady && config) {
postToIframe("LS_INIT", {
labelConfig: config,
task,
user,
interfaces,
selectedAnnotationIndex: 0,
allowCreateEmptyAnnotation: true,
});
}
}, [iframeReady, config, task, user, interfaces]);
useEffect(() => {
const handler = (event: MessageEvent) => {
if (event.origin !== origin) return;
const msg = event.data || {};
if (!msg?.type) return;
if (msg.type === "LS_IFRAME_READY") {
setIframeReady(true);
return;
}
if (msg.type === "LS_READY") {
setLsReady(true);
return;
}
if (msg.type === "LS_EXPORT_RESULT" || msg.type === "LS_SUBMIT") {
if (onSubmit) onSubmit(msg.payload);
else if (onUpdate) onUpdate(msg.payload);
return;
}
if (msg.type === "LS_UPDATE_ANNOTATION") {
if (onUpdate) onUpdate(msg.payload);
return;
}
if (msg.type === "LS_ERROR") {
if (onError) onError(msg.payload);
else message.error(msg.payload?.message || "编辑器发生错误");
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [origin, onSubmit, onUpdate, onError]);
return (
<div className={`relative ${className}`} style={{ height }}>
{!lsReady && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/70">
<Spin tip={!iframeReady ? "加载资源..." : "初始化编辑器..."} />
</div>
)}
<iframe
ref={iframeRef}
title="Label Studio Frontend"
src={LSF_IFRAME_SRC}
className="w-full h-full border-0"
/>
</div>
);
}