diff --git a/frontend/public/lsf/lsf.html b/frontend/public/lsf/lsf.html index f0ac2f4..9b0cff0 100644 --- a/frontend/public/lsf/lsf.html +++ b/frontend/public/lsf/lsf.html @@ -268,6 +268,17 @@ return true; } + function isSaveShortcut(event) { + if (!event || event.defaultPrevented || event.isComposing) return false; + const key = event.key; + const code = event.code; + const isS = key === "s" || key === "S" || code === "KeyS"; + if (!isS) return false; + if (!(event.ctrlKey || event.metaKey)) return false; + if (event.shiftKey || event.altKey) return false; + return true; + } + function handleSaveAndNextShortcut(event) { if (!isSaveAndNextShortcut(event) || event.repeat) return; event.preventDefault(); @@ -280,6 +291,18 @@ } } + function handleSaveShortcut(event) { + if (!isSaveShortcut(event) || event.repeat) return; + event.preventDefault(); + event.stopPropagation(); + try { + const raw = exportSelectedAnnotation(); + postToParent("LS_EXPORT_RESULT", raw); + } catch (e) { + postToParent("LS_ERROR", { message: e?.message || String(e) }); + } + } + function initLabelStudio(payload) { if (!window.LabelStudio) { throw new Error("LabelStudio 未加载(请检查静态资源/网络)"); @@ -351,6 +374,7 @@ } window.addEventListener("keydown", handleSaveAndNextShortcut); + window.addEventListener("keydown", handleSaveShortcut); window.addEventListener("message", (event) => { if (event.origin !== ORIGIN) return; diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 36224c9..0f15606 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -117,6 +117,17 @@ const resolveSegmentIndex = (value: unknown) => { return Number.isFinite(parsed) ? parsed : undefined; }; +const isSaveShortcut = (event: KeyboardEvent) => { + if (event.defaultPrevented || event.isComposing) return false; + const key = event.key; + const code = event.code; + const isS = key === "s" || key === "S" || code === "KeyS"; + if (!isS) return false; + if (!(event.ctrlKey || event.metaKey)) return false; + if (event.shiftKey || event.altKey) return false; + return true; +}; + const normalizePayload = (payload: unknown): ExportPayload | undefined => { if (!payload || typeof payload !== "object") return undefined; return payload as ExportPayload; @@ -851,14 +862,27 @@ export default function LabelStudioTextEditor() { }); }, [modal]); - const requestExport = () => { + const requestExport = useCallback((autoAdvance: boolean) => { if (!selectedFileId) { message.warning("请先选择文件"); return; } - pendingAutoAdvanceRef.current = true; + pendingAutoAdvanceRef.current = autoAdvance; postToIframe("LS_EXPORT", {}); - }; + }, [message, postToIframe, selectedFileId]); + + useEffect(() => { + const handleSaveShortcut = (event: KeyboardEvent) => { + if (!isSaveShortcut(event) || event.repeat) return; + if (saving || loadingTaskDetail || segmentSwitching) return; + if (!iframeReady || !lsReady) return; + event.preventDefault(); + event.stopPropagation(); + requestExport(false); + }; + window.addEventListener("keydown", handleSaveShortcut); + return () => window.removeEventListener("keydown", handleSaveShortcut); + }, [iframeReady, loadingTaskDetail, lsReady, requestExport, saving, segmentSwitching]); // 段落切换处理 const handleSegmentChange = useCallback(async (newIndex: number) => { @@ -1212,7 +1236,7 @@ export default function LabelStudioTextEditor() { icon={} loading={saving} disabled={!iframeReady || !selectedFileId} - onClick={requestExport} + onClick={() => requestExport(true)} > 保存