diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 1a97e5c..abe543d 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { App, Button, Card, List, Spin, Typography, Tag } from "antd"; +import { App, Button, Card, List, Spin, Typography, Tag, Switch } from "antd"; import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined, CheckOutlined } from "@ant-design/icons"; import { useNavigate, useParams } from "react-router"; @@ -71,6 +71,8 @@ type ExportPayload = { requestId?: string | null; }; +type SwitchDecision = "save" | "discard" | "cancel"; + const LSF_IFRAME_SRC = "/lsf/lsf.html"; const resolveSegmentIndex = (value: unknown) => { @@ -160,6 +162,7 @@ export default function LabelStudioTextEditor() { const [tasks, setTasks] = useState([]); const [selectedFileId, setSelectedFileId] = useState(""); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [autoSaveOnSwitch, setAutoSaveOnSwitch] = useState(false); // 分段相关状态 const [segmented, setSegmented] = useState(false); @@ -402,14 +405,32 @@ export default function LabelStudioTextEditor() { }, [iframeReady, lsReady, postToIframe]); const confirmSaveBeforeSwitch = useCallback(() => { - return new Promise((resolve) => { - modal.confirm({ + return new Promise((resolve) => { + let resolved = false; + let modalInstance: { destroy: () => void } | null = null; + const settle = (decision: SwitchDecision) => { + if (resolved) return; + resolved = true; + resolve(decision); + }; + const handleDiscard = () => { + if (modalInstance) modalInstance.destroy(); + settle("discard"); + }; + modalInstance = modal.confirm({ title: "当前段落有未保存标注", - content: "切换段落前请先保存当前标注。", + content: ( +
+ 切换段落前请先保存当前标注。 + +
+ ), okText: "保存并切换", cancelText: "取消", - onOk: () => resolve(true), - onCancel: () => resolve(false), + onOk: () => settle("save"), + onCancel: () => settle("cancel"), }); }); }, [modal]); @@ -464,10 +485,17 @@ export default function LabelStudioTextEditor() { const hasUnsavedChange = snapshotKey !== undefined && lastSnapshot !== undefined && latestSnapshot !== lastSnapshot; if (hasUnsavedChange) { - const shouldSave = await confirmSaveBeforeSwitch(); - if (!shouldSave) return; - const saved = await saveFromExport(payload); - if (!saved) return; + if (autoSaveOnSwitch) { + const saved = await saveFromExport(payload); + if (!saved) return; + } else { + const decision = await confirmSaveBeforeSwitch(); + if (decision === "cancel") return; + if (decision === "save") { + const saved = await saveFromExport(payload); + if (!saved) return; + } + } } await initEditorForFile(selectedFileId, newIndex); @@ -707,9 +735,16 @@ export default function LabelStudioTextEditor() { ))} - - {currentSegmentIndex + 1} / {segments.length} - +
+ {currentSegmentIndex + 1} / {segments.length} + 切段自动保存 + setAutoSaveOnSwitch(checked)} + disabled={segmentSwitching || saving || loadingTaskDetail || !lsReady} + /> +
)}