fix(label): fix save bug

This commit is contained in:
zhangheng
2026-04-21 19:36:32 +08:00
parent d7b2195078
commit ea4daa4631
3 changed files with 288 additions and 21 deletions

View File

@@ -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状态

View File

@@ -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<number | string>(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 (
<CustomModal
title="修复标注对象"
open={open}
onCancel={handleCancel}
centered
closeOnClickOutside={false}
footer={
<Group justify="flex-end" gap="xs" mt="md">
<Button variant="default" onClick={handleCancel}>
</Button>
<Button
variant="light"
onClick={() => handleRepairObject(normalizedFactor)}
disabled={!!factorError || selectedObjectCount !== 1}>
</Button>
<Button
onClick={() => handleRepairImage(normalizedFactor)}
disabled={!!factorError || !canRepairImage}>
</Button>
</Group>
}>
<Stack gap="sm">
<Text size="sm" c="dimmed">
`rasterScale`
</Text>
<NumberInput
label="修复因子"
placeholder="请输入大于 0 的修复因子"
value={factorInput}
onChange={setFactorInput}
allowNegative={false}
decimalScale={6}
min={0.000001}
error={factorError || undefined}
/>
<Text size="xs" c="dimmed">
{selectedObjectCount} 1
</Text>
<Text size="xs" c="dimmed">
{canRepairImage ? "已有" : "暂无"}
</Text>
</Stack>
</CustomModal>
)
}
export default RepairScaleModal

View File

@@ -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<number | string>(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<string, Map<string, [number, any[], any, any[]][]>>
) => {
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<string, [number, any[], any, any[]][]>()
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 = (
<CloudCog style={{ width: 20, height: 20 }} />
<Text size="xs"></Text>
</ActionIcon>
<ActionIcon
variant="transparent"
c="var(--mantine-color-text)"
style={{ width: "auto", display: "flex", gap: 4 }}
onClick={() => {
setRepairOpen(true)
}}>
<Wrench style={{ width: 20, height: 20 }} />
<Text size="xs"></Text>
</ActionIcon>
</>
) : (
<>
@@ -2568,6 +2718,14 @@ const TopTools = (
<CloudCog style={{ width: 20, height: 20 }} />
<Text size="xs"></Text>
</Flex>
<Flex
justify="space-between"
align="center"
gap={4}
style={{ opacity: 0.5, cursor: "not-allowed" }}>
<Wrench style={{ width: 20, height: 20 }} />
<Text size="xs"></Text>
</Flex>
</>
)}
</Flex>
@@ -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 && (
<RepairScaleModal
open={repairOpen}
defaultFactor={repairFactorDefault}
selectedObjectCount={currentSelectedObjectIds.length}
canRepairImage={!!activeImageLabelData?.size}
handleCancel={() => {
setRepairOpen(false)
}}
handleRepairObject={(factor) => {
handleRepairByFactor("object", factor)
}}
handleRepairImage={(factor) => {
handleRepairByFactor("image", factor)
}}
/>
)}
{taskOpen && (
<TaskCacheModal
open={taskOpen}