From 389c04b46a9f127692f4c40b81b3c3f12f7b5070 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 22 Jan 2026 17:38:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=B7=BB=E5=8A=A0=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E6=AE=B5=E8=90=BD=E6=97=B6=E8=87=AA=E5=8A=A8=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 LabelStudioTextEditor 组件中新增 Switch 组件用于控制自动保存 - 添加 autoSaveOnSwitch 状态管理自动保存开关 - 修改 confirmSaveBeforeSwitch 函数支持保存、放弃、取消三种决策 - 实现自动保存逻辑,当开关开启时直接保存而不弹出确认对话框 - 在段落导航栏添加自动保存开关和标签显示 - 更新切换段落时的未保存更改处理逻辑 --- .../Annotate/LabelStudioTextEditor.tsx | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) 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} + /> +
)}