fix(label): fix save bug
This commit is contained in:
@@ -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状态
|
||||
|
||||
99
components/label/components/RepairScaleModal.tsx
Normal file
99
components/label/components/RepairScaleModal.tsx
Normal 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
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user