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)}
>
保存