From 01dcd16a98f4372559adc9390e4ca83fc949f9ba Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 18 Jan 2026 14:12:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=B7=BB=E5=8A=A0=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=E4=BB=BB=E5=8A=A1=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 LabelStudioEmbed 组件用于嵌入式标注界面预览 - 在创建标注任务对话框中添加 XML 配置编辑器 - 支持从现有模板加载配置并进行自定义修改 - 实现标注界面实时预览功能 - 后端支持直接传递 label_config 覆盖模板配置 - 更新 CreateAnnotationTaskRequest 模型添加 labelConfig 字段 --- .../components/business/LabelStudioEmbed.tsx | 120 ++++ .../components/CreateAnnotationTaskDialog.tsx | 526 ++++++++++-------- .../module/annotation/interface/project.py | 5 + .../app/module/annotation/schema/mapping.py | 1 + 4 files changed, 429 insertions(+), 223 deletions(-) create mode 100644 frontend/src/components/business/LabelStudioEmbed.tsx diff --git a/frontend/src/components/business/LabelStudioEmbed.tsx b/frontend/src/components/business/LabelStudioEmbed.tsx new file mode 100644 index 0000000..8063e85 --- /dev/null +++ b/frontend/src/components/business/LabelStudioEmbed.tsx @@ -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(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 ( +
+ {!lsReady && ( +
+ +
+ )} +