From ea4daa46313fb072064aee25ba3ebb24cca0c510 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Tue, 21 Apr 2026 19:36:32 +0800 Subject: [PATCH] fix(label): fix save bug --- components/label/LabelNossr.tsx | 5 +- .../label/components/RepairScaleModal.tsx | 99 +++++++++ components/label/components/TopTools.tsx | 205 ++++++++++++++++-- 3 files changed, 288 insertions(+), 21 deletions(-) create mode 100644 components/label/components/RepairScaleModal.tsx diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index 580a8d6..1b34bc8 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -2434,10 +2434,9 @@ const LabelPage = ({ const handleBeforeUnload = (e: any) => { e.preventDefault() + e.returnValue = "" // 备份 - topRef.current.handleBackup("auto_backup") - // 清空操作栏状态 - useObjectStore.getState().resetPathAndOperationStatus() + void topRef.current?.handleBackup?.("auto_backup") } // 更新Splitter状态 diff --git a/components/label/components/RepairScaleModal.tsx b/components/label/components/RepairScaleModal.tsx new file mode 100644 index 0000000..5a08669 --- /dev/null +++ b/components/label/components/RepairScaleModal.tsx @@ -0,0 +1,99 @@ +"use client" + +import { Button, Group, NumberInput, Stack, Text } from "@mantine/core" +import { useEffect, useMemo, useState } from "react" +import CustomModal from "./CustomModal" + +interface ComponentProps { + open: boolean + defaultFactor: number + selectedObjectCount: number + canRepairImage: boolean + handleCancel: () => void + handleRepairObject: (factor: number) => void + handleRepairImage: (factor: number) => void +} + +const RepairScaleModal = ({ + open, + defaultFactor, + selectedObjectCount, + canRepairImage, + handleCancel, + handleRepairObject, + handleRepairImage, +}: ComponentProps) => { + const [factorInput, setFactorInput] = useState(1) + + useEffect(() => { + if (!open) return + const nextFactor = + Number.isFinite(defaultFactor) && defaultFactor > 0 + ? Number(defaultFactor.toFixed(6)) + : 1 + setFactorInput(nextFactor) + }, [defaultFactor, open]) + + const normalizedFactor = useMemo(() => { + if (typeof factorInput === "number") return factorInput + return Number(factorInput) + }, [factorInput]) + + const factorError = + Number.isFinite(normalizedFactor) && normalizedFactor > 0 + ? null + : "请输入大于 0 的修复因子" + + return ( + + + + + + }> + + + 该工具会将当前页面里的标注几何点统一除以修复因子,默认值取当前图片比例 + `rasterScale`。修复后请先检查显示效果,再决定是否保存。 + + + + 当前已选对象数:{selectedObjectCount}。修复当前对象需要恰好选中 1 + 个对象。 + + + 当前图片{canRepairImage ? "已有" : "暂无"}可修复标注数据。 + + + + ) +} + +export default RepairScaleModal diff --git a/components/label/components/TopTools.tsx b/components/label/components/TopTools.tsx index a3f4ae9..51dac7c 100644 --- a/components/label/components/TopTools.tsx +++ b/components/label/components/TopTools.tsx @@ -59,6 +59,7 @@ import { SquarePen, Tag, Timer, + Wrench, } from "lucide-react" import { useRouter } from "next/navigation" import { @@ -102,12 +103,13 @@ import { import { buildSubAttributeFormValues, getSubAttrStatus } from "../util" import { safeClone } from "../utils/clone" import { labelTypeMap, TaskStatusEnum } from "../utils/constants" -import { adjustAllPoints } from "../utils/paperjs" +import { adjustAllPoints, adjustPoints } from "../utils/paperjs" import BackConfirmModal from "./BackConfirmModal" import BackupModal from "./BackupModal" import ConfirmModal from "./ConfirmModal" import { splitWord } from "./EditorContainer" import RecoverModal from "./RecoverModal" +import RepairScaleModal from "./RepairScaleModal" import { renderOperationIcon } from "./RightObjectTools" import TaskCacheModal from "./TaskCacheModal" import TaskStatusTag from "./TaskStatusTag" @@ -143,6 +145,8 @@ const initialWorkLoad: WorkLoad = { question_size: {}, } +const EMPTY_SELECTED_OBJECT_IDS: number[] = [] + const TaskTimerDisplay = ({ currentTimeKey, }: { @@ -179,6 +183,7 @@ const TopTools = ( const [duplicateConfirmOpen, setDuplicateConfirmOpen] = useState(false) const [duplicateName, setDuplicateName] = useState("") const [recoverOpen, setRecoverOpen] = useState(false) + const [repairOpen, setRepairOpen] = useState(false) const [taskOpen, setTaskOpen] = useState(false) const [submitConfirmOpen, setSubmitConfirmOpen] = useState(false) const [operationPickerOpened, setOperationPickerOpened] = useState(false) @@ -191,6 +196,12 @@ const TopTools = ( const pushStateStack = useLabelStore((state) => state.pushStateStack) const setLabelTime = useLabelTimeStore((state) => state.setLabelTime) const { activeImage } = useBottomToolsStore() + const selectedPathMap = useObjectStore((state) => state.selectedPath) + const currentRasterScale = usePaperStore( + (state) => state.rasterScale[activeImage] ?? 1 + ) + const currentSelectedObjectIds = + selectedPathMap[activeImage] ?? EMPTY_SELECTED_OBJECT_IDS const { isView, setIsView, @@ -249,6 +260,15 @@ const TopTools = ( useState(auxiliarySizeThreshold) const { pathGroupMap } = useRightToolsStore() + const activeImageLabelData = useMemo( + () => labelData.get(activeImage), + [activeImage, labelData] + ) + const repairFactorDefault = useMemo(() => { + return Number.isFinite(currentRasterScale) && currentRasterScale > 0 + ? Number(currentRasterScale.toFixed(6)) + : 1 + }, [currentRasterScale]) const isAutoSave = useIntervalStore((state) => state.isAutoSave) const setIsAutoSave = useIntervalStore((state) => state.setIsAutoSave) const autoSaveGap = useIntervalStore((state) => state.autoSaveGap) @@ -953,6 +973,36 @@ const TopTools = ( loadingData, ]) + const leaveCurrentPage = useCallback((url: string) => { + // `beforeunload` may still be cancelled by the browser/user, so do not + // mutate persisted label state after requesting a full-page navigation. + window.location.href = url + }, []) + + const rerenderActiveImageAnnotations = useCallback( + ( + nextLabelData: Map> + ) => { + if (!renderPolygons || !activeImage) return + + usePaperStore + .getState() + .group?.getItems({}) + .filter((item: any) => item.data?.id) + .forEach((item: any) => { + item.remove() + }) + + const currentImageMap = nextLabelData.get(activeImage) + if (!currentImageMap) return + + for (const key of currentImageMap.keys()) { + renderPolygons(key, currentImageMap) + } + }, + [activeImage, renderPolygons] + ) + const handleBackup = useCallback( async (name: string) => { let newTaskLabelData = new Map() @@ -1143,6 +1193,102 @@ const TopTools = ( ] ) + const handleRepairByFactor = useCallback( + (scope: "object" | "image", factor: number) => { + if (!activeImage) { + notifications.show({ + color: "yellow", + message: "当前图片未准备完成,暂时无法修复", + }) + return + } + + const normalizedFactor = Number(factor) + if (!Number.isFinite(normalizedFactor) || normalizedFactor <= 0) { + notifications.show({ + color: "yellow", + message: "请输入大于 0 的修复因子", + }) + return + } + + const currentImageData = labelData.get(activeImage) + if (!currentImageData?.size) { + notifications.show({ + color: "yellow", + message: "当前图片暂无可修复标注数据", + }) + return + } + + const scaleRatio = 1 / normalizedFactor + let nextLabelData = adjustPoints(activeImage, labelData, scaleRatio) + + if (scope === "object") { + const selectedIds = + useObjectStore.getState().selectedPath[activeImage] || [] + if (selectedIds.length !== 1) { + notifications.show({ + color: "yellow", + message: "请先选中 1 个标注对象后再修复", + }) + return + } + + const scaledImageData = nextLabelData.get(activeImage) + if (!scaledImageData) { + notifications.show({ + color: "yellow", + message: "当前对象修复失败,请重试", + }) + return + } + + const selectedSet = new Set(selectedIds) + const mergedImageData = new Map() + + currentImageData.forEach((objects, operationId) => { + const scaledObjects = scaledImageData.get(operationId) || objects + const scaledObjectById = new Map( + scaledObjects.map((item) => [item[0], item] as const) + ) + + mergedImageData.set( + operationId, + objects.map((item) => { + if (!selectedSet.has(item[0])) return item + return scaledObjectById.get(item[0]) || item + }) as [number, any[], any, any[]][] + ) + }) + + nextLabelData = safeClone(labelData) + nextLabelData.set(activeImage, mergedImageData) + } + + const finalData = safeClone(nextLabelData) + setLabel(finalData) + pushStateStack(finalData) + rerenderActiveImageAnnotations(finalData) + setRepairOpen(false) + + notifications.show({ + color: "green", + message: + scope === "object" + ? "已按修复因子更新当前对象,请检查后再保存" + : "已按修复因子更新当前图片,请检查后再保存", + }) + }, + [ + activeImage, + labelData, + pushStateStack, + rerenderActiveImageAnnotations, + setLabel, + ] + ) + useEffect(() => { if (needBackup) { handleBackup("auto_backup") @@ -1190,10 +1336,7 @@ const TopTools = ( // 无id返回时跳转回任务列表页 const url = backUrl ? basePath + backUrl : "/" // router.push(url) - window.location.href = url - // 重置图片比例及选中标注对象 - usePaperStore.getState().resetRasterScale() - useObjectStore.getState().resetPathAndOperationStatus() + leaveCurrentPage(url) } } @@ -1851,11 +1994,8 @@ const TopTools = ( if (isView) { const url = backUrl ? basePath + backUrl : "/" // router.push(url) - window.location.href = url + leaveCurrentPage(url) // handleBackup("auto_backup"); - // 重置图片比例及选中标注对象 - usePaperStore.getState().resetRasterScale() - useObjectStore.getState().resetPathAndOperationStatus() } else { setConfirmOpen(true) } @@ -2541,6 +2681,16 @@ const TopTools = ( 恢复 + { + setRepairOpen(true) + }}> + + 修复 + ) : ( <> @@ -2568,6 +2718,14 @@ const TopTools = ( 恢复 + + + 修复 + )} @@ -2658,21 +2816,15 @@ const TopTools = ( setConfirmOpen(false) const url = backUrl ? basePath + backUrl : "/" // router.push(url) - window.location.href = url - // 重置图片比例及选中标注对象 - usePaperStore.getState().resetRasterScale() - useObjectStore.getState().resetPathAndOperationStatus() + leaveCurrentPage(url) }} handleCancel={async () => { try { setConfirmOpen(false) - handleBackup("auto_backup") + await handleBackup("auto_backup") const url = backUrl ? basePath + backUrl : "/" // router.push(url) - window.location.href = url - // 重置图片比例及选中标注对象 - usePaperStore.getState().resetRasterScale() - useObjectStore.getState().resetPathAndOperationStatus() + leaveCurrentPage(url) } catch (error) { console.log(error) } @@ -2775,6 +2927,23 @@ const TopTools = ( handleOk={handleRecover} /> )} + {repairOpen && ( + { + setRepairOpen(false) + }} + handleRepairObject={(factor) => { + handleRepairByFactor("object", factor) + }} + handleRepairImage={(factor) => { + handleRepairByFactor("image", factor) + }} + /> + )} {taskOpen && (