855 lines
27 KiB
TypeScript
855 lines
27 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
ActionIcon,
|
||
Box,
|
||
Button,
|
||
Flex,
|
||
Group,
|
||
ScrollArea,
|
||
Text,
|
||
TextInput,
|
||
} from "@mantine/core"
|
||
import { showNotification } from "@mantine/notifications"
|
||
import {
|
||
IconChevronLeft,
|
||
IconChevronRight,
|
||
IconChevronsLeft,
|
||
IconChevronsRight,
|
||
IconFlag,
|
||
IconPlayerTrackNext,
|
||
IconPlayerTrackNextFilled,
|
||
IconPlayerTrackPrev,
|
||
IconPlayerTrackPrevFilled,
|
||
} from "@tabler/icons-react"
|
||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||
import { Project } from "../api/project/typing"
|
||
import { Task } from "../api/task/typing"
|
||
import { LabelState } from "../LabelNossr"
|
||
import { useLabelStore } from "../store"
|
||
import { usePermissionStore } from "../store/auth"
|
||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||
import { useDescToolsStore } from "../useDescToolsStore"
|
||
import { useKeyboardStore } from "../useKeyBoardStore"
|
||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||
import { usePaperStore } from "../usePaperStore"
|
||
import { useLabelTimeStore } from "../useTimerStore"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
import { getSubAttrStatus } from "../util"
|
||
import { safeClone } from "../utils/clone"
|
||
import CustomModal from "./CustomModal"
|
||
import MetaOperationTool from "./MetaOperationTool"
|
||
|
||
interface BottomToolsComponentProps {
|
||
labelState: LabelState
|
||
taskDetail?: Task.DataProps
|
||
projectDetail?: Project.DetailResponse
|
||
renderPolygons: (
|
||
operationId: string,
|
||
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
||
) => void | null
|
||
}
|
||
|
||
type PlaybackDirection = "backward" | "forward"
|
||
|
||
const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
|
||
const { labelState, projectDetail, taskDetail, renderPolygons } = props
|
||
|
||
const itemRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||
|
||
const storeLabel = useLabelStore((state) => state.label)
|
||
const setLabel = useLabelStore((state) => state.setLabel)
|
||
const pushStateStack = useLabelStore((state) => state.pushStateStack)
|
||
const {
|
||
inputNumber,
|
||
setInputNumber,
|
||
activeImage,
|
||
pendingImage,
|
||
frameSwitchStatus,
|
||
setActiveImage,
|
||
setActiveImageId,
|
||
requestFrameSwitch,
|
||
onlyKeyFrame,
|
||
setOnlyKeyFrame,
|
||
keyFrameData,
|
||
setKeyFrameData,
|
||
selectedItems,
|
||
setSelectedItems,
|
||
} = useBottomToolsStore()
|
||
const { metaOperation } = useDescToolsStore()
|
||
const { ctrl: ctrlKey, shift: shiftKey } = useKeyboardStore()
|
||
|
||
const [open, setOpen] = useState(false)
|
||
const [conflictOpen, setConflictOpen] = useState(false)
|
||
const [playDirection, setPlayDirection] = useState<PlaybackDirection | null>(
|
||
null
|
||
)
|
||
const allKeys = useMemo(() => Array.from(labelState.keys()), [labelState])
|
||
const isShowKeyFrame =
|
||
projectDetail &&
|
||
[5, 6].includes(projectDetail.label_type) &&
|
||
allKeys.some(
|
||
(k) => keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
|
||
) &&
|
||
onlyKeyFrame
|
||
const currentKeys = useMemo(
|
||
() =>
|
||
allKeys.filter((k) =>
|
||
isShowKeyFrame
|
||
? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
|
||
: true
|
||
),
|
||
[allKeys, isShowKeyFrame, keyFrameData]
|
||
)
|
||
// 上一帧图片id
|
||
const previousFrameKey = useMemo(() => {
|
||
const currentIndex = currentKeys.findIndex((k) => k === activeImage)
|
||
return currentIndex > 0 ? currentKeys[currentIndex - 1] : ""
|
||
}, [activeImage, currentKeys])
|
||
|
||
useEffect(() => {
|
||
if (!currentKeys.length) {
|
||
setActiveImage("")
|
||
setInputNumber("")
|
||
setActiveImageId(0)
|
||
return
|
||
}
|
||
|
||
const currentIndex = activeImage
|
||
? currentKeys.findIndex((k) => k === activeImage)
|
||
: -1
|
||
if (currentIndex >= 0) {
|
||
setInputNumber(String(currentIndex + 1))
|
||
const absoluteIndex = allKeys.findIndex((k) => k === activeImage)
|
||
setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 0)
|
||
return
|
||
}
|
||
|
||
const firstKey = currentKeys[0]
|
||
const absoluteIndex = allKeys.findIndex((k) => k === firstKey)
|
||
setActiveImage(firstKey)
|
||
setInputNumber("1")
|
||
setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 1)
|
||
}, [
|
||
activeImage,
|
||
allKeys,
|
||
currentKeys,
|
||
setActiveImage,
|
||
setActiveImageId,
|
||
setInputNumber,
|
||
])
|
||
|
||
const scrollToIndex = useCallback(
|
||
(index: number) => {
|
||
const key = currentKeys[index]
|
||
if (key && itemRefs.current[key]) {
|
||
itemRefs.current[key]?.scrollIntoView({
|
||
behavior: "smooth",
|
||
block: "nearest",
|
||
inline: "center",
|
||
})
|
||
}
|
||
},
|
||
[currentKeys]
|
||
)
|
||
|
||
const closeRightContext = useCallback(() => {
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: useOtherToolsStore.getState().imageId,
|
||
operationId: useOtherToolsStore.getState().operationId,
|
||
id: useOtherToolsStore.getState().id,
|
||
})
|
||
}, [])
|
||
|
||
const goToVisibleIndex = useCallback(
|
||
(index: number) => {
|
||
if (index < 0 || index >= currentKeys.length) return false
|
||
|
||
const nextImage = currentKeys[index]
|
||
if (!nextImage) return false
|
||
|
||
scrollToIndex(index)
|
||
if (
|
||
frameSwitchStatus === "preparing" &&
|
||
pendingImage &&
|
||
pendingImage === nextImage
|
||
) {
|
||
closeRightContext()
|
||
return true
|
||
}
|
||
if (nextImage !== activeImage) {
|
||
requestFrameSwitch(nextImage)
|
||
}
|
||
closeRightContext()
|
||
|
||
return true
|
||
},
|
||
[
|
||
activeImage,
|
||
closeRightContext,
|
||
currentKeys,
|
||
frameSwitchStatus,
|
||
pendingImage,
|
||
requestFrameSwitch,
|
||
scrollToIndex,
|
||
]
|
||
)
|
||
|
||
const stepVisibleImage = useCallback(
|
||
(direction: PlaybackDirection) => {
|
||
const currentIndex = currentKeys.findIndex((k) => k === activeImage)
|
||
if (currentIndex < 0) return false
|
||
|
||
const nextIndex =
|
||
direction === "backward" ? currentIndex - 1 : currentIndex + 1
|
||
|
||
return goToVisibleIndex(nextIndex)
|
||
},
|
||
[activeImage, currentKeys, goToVisibleIndex]
|
||
)
|
||
|
||
const togglePlayback = useCallback(
|
||
(direction: PlaybackDirection) => {
|
||
if (currentKeys.length < 2) return
|
||
setPlayDirection((current) => (current === direction ? null : direction))
|
||
},
|
||
[currentKeys.length]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (!playDirection) return
|
||
if (currentKeys.length < 2) {
|
||
setPlayDirection(null)
|
||
return
|
||
}
|
||
|
||
const timer = window.setInterval(() => {
|
||
const moved = stepVisibleImage(playDirection)
|
||
if (!moved) {
|
||
setPlayDirection(null)
|
||
}
|
||
}, 1000)
|
||
|
||
return () => {
|
||
window.clearInterval(timer)
|
||
}
|
||
}, [currentKeys.length, playDirection, stepVisibleImage])
|
||
|
||
const getOperationSchema = useCallback(
|
||
(operationId: string) => {
|
||
return (
|
||
projectDetail?.label_schema_list?.find(
|
||
(item) => item.category_id === +operationId
|
||
) || null
|
||
)
|
||
},
|
||
[projectDetail?.label_schema_list]
|
||
)
|
||
|
||
// child 是否存在不符合要求的子属性
|
||
const getOperationSubAttrStatus = useCallback(
|
||
(imageId: string) => {
|
||
let parentBool = labelState.get(imageId)?.some((operationId) => {
|
||
let bool = storeLabel
|
||
.get(imageId)
|
||
?.get(operationId)
|
||
?.some((child) => {
|
||
return getSubAttrStatus(
|
||
getOperationSchema(operationId),
|
||
child[2]?.sub_attributes
|
||
)
|
||
})
|
||
return bool
|
||
})
|
||
return parentBool
|
||
},
|
||
[getOperationSchema, labelState, storeLabel]
|
||
)
|
||
|
||
// 获取上一帧和当前帧标注对象数组
|
||
const getLabelObjectsData = () => {
|
||
let map1 =
|
||
storeLabel.get(previousFrameKey) ??
|
||
new Map<string, [number, any[], any, any[]][]>()
|
||
let arr1 = []
|
||
for (const value of map1.values()) arr1.push(...value)
|
||
let map2 =
|
||
storeLabel.get(activeImage) ??
|
||
new Map<string, [number, any[], any, any[]][]>()
|
||
let arr2 = []
|
||
for (const value of map2.values()) arr2.push(...value)
|
||
return [arr1, arr2]
|
||
}
|
||
|
||
// 上一帧是否有文本描述
|
||
const prevFramehasTextDesc = () => {
|
||
if (projectDetail?.label_type === 5) {
|
||
let flag = false
|
||
const currentDescData = safeClone(useDescToolsStore.getState().descData)
|
||
const prevData1 = currentDescData.get(previousFrameKey) ?? new Map()
|
||
flag = prevData1.size ? true : false
|
||
if (flag) return true
|
||
const currentMetaData = safeClone(useDescToolsStore.getState().metaData)
|
||
const prevData2 = currentMetaData.get(previousFrameKey) ?? []
|
||
flag = prevData2.length ? true : false
|
||
return flag
|
||
} else if (projectDetail?.label_type === 6) {
|
||
const currentQaData = safeClone(useDescToolsStore.getState().qaData)
|
||
const prevData = currentQaData.get(previousFrameKey) ?? {}
|
||
return JSON.stringify(prevData) === "{}" ? false : true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 更新数据绘制
|
||
const updateDataRender = (
|
||
data: Map<string, [number, any[], any, any[]][]>
|
||
) => {
|
||
if (!renderPolygons) return
|
||
const list = usePaperStore.getState().group?.getItems({})
|
||
list?.forEach((item) => {
|
||
if (item.data.id) {
|
||
item.remove()
|
||
}
|
||
})
|
||
|
||
useTopToolsStore.getState().objectOperations?.forEach((item) => {
|
||
renderPolygons(item.category_id.toString(), data)
|
||
})
|
||
}
|
||
|
||
// 大语言标注继承上一帧时数据处理
|
||
const handleLLMData = () => {
|
||
// LLM
|
||
if (projectDetail?.label_type === 5) {
|
||
// 文本描述
|
||
const currentDescData = safeClone(useDescToolsStore.getState().descData)
|
||
const prevData1 = currentDescData.get(previousFrameKey) ?? new Map()
|
||
currentDescData.set(activeImage, prevData1)
|
||
useDescToolsStore.getState().setDescData(currentDescData)
|
||
useDescToolsStore.getState().updateFlag(true)
|
||
// 元操作序列
|
||
const currentMetaData = safeClone(useDescToolsStore.getState().metaData)
|
||
const prevData2 = currentMetaData.get(previousFrameKey) ?? []
|
||
currentMetaData.set(activeImage, prevData2)
|
||
useDescToolsStore.getState().setMetaData(currentMetaData)
|
||
}
|
||
// QA
|
||
if (projectDetail?.label_type === 6) {
|
||
const currentQaData = safeClone(useDescToolsStore.getState().qaData)
|
||
const prevData = currentQaData.get(previousFrameKey) ?? {}
|
||
currentQaData.set(activeImage, prevData)
|
||
useDescToolsStore.getState().setQaData(currentQaData)
|
||
useDescToolsStore.getState().updateFlag(true)
|
||
}
|
||
}
|
||
|
||
// 继承上一帧的数据
|
||
const handleCopyData = () => {
|
||
// 图形绘制
|
||
const [perviousData] = getLabelObjectsData()
|
||
let map =
|
||
storeLabel.get(activeImage) ??
|
||
new Map<string, [number, any[], any, any[]][]>()
|
||
perviousData.forEach((item) => {
|
||
const id = item[2].category_id.toString()
|
||
if (map.has(id)) {
|
||
let data = map.get(id)
|
||
data!.push(item)
|
||
map.set(id, data as any)
|
||
} else {
|
||
map.set(id, [item])
|
||
}
|
||
})
|
||
let updateData = new Map()
|
||
for (const [key, value] of storeLabel) {
|
||
if (key === activeImage) updateData.set(key, map)
|
||
else updateData.set(key, value)
|
||
}
|
||
if (!storeLabel.has(activeImage)) updateData.set(activeImage, map)
|
||
updateDataRender(updateData.get(activeImage)!)
|
||
setLabel(updateData)
|
||
pushStateStack(updateData)
|
||
handleLLMData()
|
||
setOpen(false)
|
||
}
|
||
|
||
const handleCopyConflictData = (flag: boolean) => {
|
||
const [perviousData, nowData] = getLabelObjectsData()
|
||
|
||
let map = new Map<string, [number, any[], any, any[]][]>()
|
||
let arr = flag
|
||
? [
|
||
...perviousData,
|
||
...nowData.filter(
|
||
(item) => !perviousData.map((p) => p[0]).includes(item[0])
|
||
),
|
||
]
|
||
: [
|
||
...perviousData.filter(
|
||
(item) => !nowData.map((d) => d[0]).includes(item[0])
|
||
),
|
||
...nowData,
|
||
]
|
||
arr.forEach((item) => {
|
||
const id = item[2].category_id.toString()
|
||
if (map.has(id)) {
|
||
let data = map.get(id)
|
||
data!.push(item)
|
||
map.set(id, data as any)
|
||
} else {
|
||
map.set(id, [item])
|
||
}
|
||
})
|
||
let updateData = new Map()
|
||
for (const [key, value] of storeLabel) {
|
||
if (key === activeImage) updateData.set(key, map)
|
||
else updateData.set(key, value)
|
||
}
|
||
updateDataRender(updateData.get(activeImage)!)
|
||
setLabel(updateData)
|
||
pushStateStack(updateData)
|
||
handleLLMData()
|
||
setConflictOpen(false)
|
||
}
|
||
|
||
return (
|
||
<Box w="100%" h="100%">
|
||
{projectDetail?.label_type === 5 && metaOperation ? (
|
||
<Flex
|
||
h={48}
|
||
w="100%"
|
||
align="center"
|
||
style={{
|
||
borderTop:
|
||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Box px="md" fw={600}>
|
||
元操作序列
|
||
</Box>
|
||
<Box
|
||
style={{
|
||
maxWidth: "calc(100% - 98px)",
|
||
overflowX: "auto",
|
||
height: "100%",
|
||
}}>
|
||
<MetaOperationTool />
|
||
</Box>
|
||
</Flex>
|
||
) : null}
|
||
<Box
|
||
h={28}
|
||
style={{
|
||
border:
|
||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Flex justify="space-between" align="center" h="100%">
|
||
<Box flex={1} />
|
||
<Flex flex={1} align="center" justify="center" gap="xs">
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
size="sm"
|
||
onClick={() => {
|
||
goToVisibleIndex(0)
|
||
}}>
|
||
<IconChevronsLeft size={16} />
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant={playDirection === "backward" ? "filled" : "subtle"}
|
||
color={playDirection === "backward" ? "blue" : "gray"}
|
||
size="sm"
|
||
disabled={currentKeys.length < 2}
|
||
aria-label="向前播放"
|
||
title="向前播放"
|
||
onClick={() => {
|
||
togglePlayback("backward")
|
||
}}>
|
||
{playDirection === "backward" ? (
|
||
<IconPlayerTrackPrevFilled size={16} />
|
||
) : (
|
||
<IconPlayerTrackPrev size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
size="sm"
|
||
onClick={() => {
|
||
stepVisibleImage("backward")
|
||
}}>
|
||
<IconChevronLeft size={16} />
|
||
</ActionIcon>
|
||
<TextInput
|
||
w={42}
|
||
size="compact-xs"
|
||
radius={"sm"}
|
||
styles={{
|
||
input: {
|
||
height: 20,
|
||
fontSize: "14px",
|
||
textAlign: "center",
|
||
},
|
||
}}
|
||
value={inputNumber}
|
||
onChange={(e) => {
|
||
setInputNumber(e.target.value)
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
const targetIndex = Number(inputNumber)
|
||
if (
|
||
Number.isInteger(targetIndex) &&
|
||
targetIndex >= 1 &&
|
||
targetIndex <= currentKeys.length
|
||
) {
|
||
goToVisibleIndex(targetIndex - 1)
|
||
} else {
|
||
const currentIndex = currentKeys.findIndex(
|
||
(key) => key === activeImage
|
||
)
|
||
setInputNumber(
|
||
currentIndex >= 0 ? String(currentIndex + 1) : ""
|
||
)
|
||
}
|
||
}
|
||
}}
|
||
/>
|
||
<Text size="xs">/ {currentKeys.length}</Text>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
size="sm"
|
||
onClick={() => {
|
||
stepVisibleImage("forward")
|
||
}}>
|
||
<IconChevronRight size={16} />
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant={playDirection === "forward" ? "filled" : "subtle"}
|
||
color={playDirection === "forward" ? "blue" : "gray"}
|
||
size="sm"
|
||
disabled={currentKeys.length < 2}
|
||
aria-label="向后播放"
|
||
title="向后播放"
|
||
onClick={() => {
|
||
togglePlayback("forward")
|
||
}}>
|
||
{playDirection === "forward" ? (
|
||
<IconPlayerTrackNextFilled size={16} />
|
||
) : (
|
||
<IconPlayerTrackNext size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
size="sm"
|
||
onClick={() => {
|
||
goToVisibleIndex(currentKeys.length - 1)
|
||
}}>
|
||
<IconChevronsRight size={16} />
|
||
</ActionIcon>
|
||
</Flex>
|
||
<Flex flex={1} justify="flex-end" align="center" gap="xs" pr="xs">
|
||
{projectDetail && [5, 6].includes(projectDetail.label_type) ? (
|
||
<>
|
||
{isShowKeyFrame ? (
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
radius={"sm"}
|
||
onClick={() => {
|
||
const [firstKey] = labelState.keys()
|
||
setActiveImage(firstKey)
|
||
setInputNumber("1")
|
||
setActiveImageId(1)
|
||
setOnlyKeyFrame(false)
|
||
}}>
|
||
查看全部帧
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
radius={"sm"}
|
||
onClick={() => {
|
||
let flag = false
|
||
keyFrameData
|
||
.entries()
|
||
.forEach(
|
||
([key, value]: [string, { key_frame?: boolean }]) => {
|
||
if (labelState.has(key) && value?.key_frame)
|
||
flag = true
|
||
}
|
||
)
|
||
if (!flag) {
|
||
showNotification({
|
||
title: "警告",
|
||
message: "当前任务暂未设定关键帧",
|
||
color: "yellow",
|
||
})
|
||
return
|
||
}
|
||
const filterKeys = Array.from(labelState.keys()).filter(
|
||
(key) =>
|
||
keyFrameData.has(key) &&
|
||
keyFrameData.get(key)?.key_frame
|
||
)
|
||
setActiveImage(filterKeys[0])
|
||
setInputNumber("1")
|
||
setActiveImageId(1)
|
||
setOnlyKeyFrame(true)
|
||
}}>
|
||
仅保留关键帧
|
||
</Button>
|
||
)}
|
||
{keyFrameData.has(activeImage) &&
|
||
keyFrameData.get(activeImage)?.key_frame ? (
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
radius={"sm"}
|
||
disabled={
|
||
!taskDetail ||
|
||
![2, 4, 6].includes(taskDetail.label_status) ||
|
||
useTopToolsStore.getState().isView
|
||
}
|
||
onClick={() => {
|
||
if (!taskDetail) return
|
||
const data = keyFrameData.get(activeImage)!
|
||
data.key_frame = false
|
||
if (taskDetail.label_status === 2) {
|
||
data.label1_ts = 0
|
||
data.label1_uid = 0
|
||
} else if (taskDetail.label_status === 4) {
|
||
data.review1_ts = 0
|
||
data.review1_uid = 0
|
||
} else if (taskDetail.label_status === 6) {
|
||
data.review2_ts = 0
|
||
data.review2_uid = 0
|
||
}
|
||
setKeyFrameData(activeImage, data)
|
||
}}>
|
||
设定为非关键帧
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
radius={"sm"}
|
||
disabled={
|
||
!taskDetail ||
|
||
![2, 4, 6].includes(taskDetail.label_status) ||
|
||
useTopToolsStore.getState().isView
|
||
}
|
||
onClick={() => {
|
||
if (!taskDetail) return
|
||
const data = keyFrameData.get(activeImage) || {
|
||
key_frame: false,
|
||
label1_ts: 0,
|
||
label1_uid: 0,
|
||
review1_ts: 0,
|
||
review1_uid: 0,
|
||
review2_ts: 0,
|
||
review2_uid: 0,
|
||
}
|
||
data.key_frame = true
|
||
const currentLabelTime =
|
||
useLabelTimeStore.getState().labelTime
|
||
if (taskDetail.label_status === 2) {
|
||
data.label1_ts = currentLabelTime.label
|
||
data.label1_uid = usePermissionStore.getState().user_id
|
||
} else if (taskDetail.label_status === 4) {
|
||
data.review1_ts = currentLabelTime.review1
|
||
data.review1_uid = usePermissionStore.getState().user_id
|
||
} else if (taskDetail.label_status === 6) {
|
||
data.review2_ts = currentLabelTime.review2
|
||
data.review2_uid = usePermissionStore.getState().user_id
|
||
}
|
||
setKeyFrameData(activeImage, data)
|
||
}}>
|
||
设定为关键帧
|
||
</Button>
|
||
)}
|
||
</>
|
||
) : null}
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
radius={"sm"}
|
||
disabled={
|
||
(!getLabelObjectsData()[0].length && !prevFramehasTextDesc()) ||
|
||
useTopToolsStore.getState().isView
|
||
}
|
||
onClick={() => {
|
||
const [perviousData, nowData] = getLabelObjectsData()
|
||
// check merge conflict
|
||
if (!nowData.length) {
|
||
setOpen(true)
|
||
return
|
||
}
|
||
let flag = false
|
||
const perviousIds = perviousData.map((item) => item[0])
|
||
const nowIds = nowData.map((item) => item[0])
|
||
perviousIds.forEach((id) => {
|
||
if (nowIds.includes(id)) flag = true
|
||
})
|
||
if (flag) setConflictOpen(true)
|
||
else setOpen(true)
|
||
}}>
|
||
继承上一帧
|
||
</Button>
|
||
</Flex>
|
||
</Flex>
|
||
</Box>
|
||
<Box
|
||
h={
|
||
projectDetail?.label_type === 5 && metaOperation
|
||
? "calc(100% - 76px)"
|
||
: "calc(100% - 28px)"
|
||
}
|
||
style={{
|
||
border:
|
||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<ScrollArea.Autosize
|
||
mah="100%"
|
||
h={"100%"}
|
||
w="100%"
|
||
scrollbars="x"
|
||
style={{ display: "flex", alignItems: "center" }}>
|
||
<Flex gap={0} w="max-content" h="100%" align="center">
|
||
{currentKeys.map((key, index) => {
|
||
const isActive = activeImage === key
|
||
const isPending =
|
||
frameSwitchStatus === "preparing" &&
|
||
pendingImage === key &&
|
||
!isActive
|
||
const isSelected =
|
||
selectedItems.length && selectedItems.includes(key)
|
||
const isHighlight = isActive || isSelected
|
||
|
||
let textColor = "inherit"
|
||
if (getOperationSubAttrStatus(key)) {
|
||
textColor = "red"
|
||
} else if (isHighlight) {
|
||
textColor = "white"
|
||
}
|
||
|
||
return (
|
||
<Box
|
||
key={key}
|
||
w={120}
|
||
h="100%"
|
||
p={6}
|
||
ref={(el) => {
|
||
itemRefs.current[key] = el
|
||
}}>
|
||
<Box
|
||
style={{
|
||
border: isHighlight
|
||
? "1px solid var(--mantine-color-blue-6)"
|
||
: isPending
|
||
? "1px dashed var(--mantine-color-blue-4)"
|
||
: "1px solid var(--mantine-color-gray-3)",
|
||
borderRadius: "6px",
|
||
backgroundColor: isHighlight
|
||
? "var(--mantine-color-blue-6)"
|
||
: isPending
|
||
? "rgba(34, 139, 230, 0.08)"
|
||
: undefined,
|
||
color: isHighlight ? "white" : undefined,
|
||
opacity: isPending ? 0.85 : 1,
|
||
cursor: "pointer",
|
||
position: "relative",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
onClick={(e) => {
|
||
if (e.shiftKey) {
|
||
e.preventDefault()
|
||
}
|
||
if (ctrlKey || shiftKey) {
|
||
setSelectedItems(key, ctrlKey, shiftKey)
|
||
} else {
|
||
goToVisibleIndex(index)
|
||
setSelectedItems("", false, false)
|
||
}
|
||
closeRightContext()
|
||
}}>
|
||
{keyFrameData.has(key) &&
|
||
keyFrameData.get(key)?.key_frame ? (
|
||
<Box
|
||
pos="absolute"
|
||
top={0}
|
||
right={8}
|
||
c={isHighlight ? "white" : "gray"}>
|
||
<IconFlag size={16} />
|
||
</Box>
|
||
) : null}
|
||
<Text fw={600} size="lg" c={textColor}>
|
||
{index + 1}
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
})}
|
||
</Flex>
|
||
</ScrollArea.Autosize>
|
||
</Box>
|
||
<CustomModal
|
||
title="提示"
|
||
open={open}
|
||
centered
|
||
onCancel={() => {
|
||
setOpen(false)
|
||
}}
|
||
onOk={handleCopyData}>
|
||
确实要复制上一帧数据吗?
|
||
</CustomModal>
|
||
<CustomModal
|
||
title="提示"
|
||
open={conflictOpen}
|
||
centered
|
||
onCancel={() => {
|
||
setConflictOpen(false)
|
||
}}
|
||
footer={
|
||
<Group justify="flex-end" gap="xs" mt="md">
|
||
<Button
|
||
variant="default"
|
||
onClick={() => {
|
||
setConflictOpen(false)
|
||
}}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
color="red"
|
||
onClick={() => {
|
||
handleCopyConflictData(false)
|
||
}}>
|
||
否
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
handleCopyConflictData(true)
|
||
}}>
|
||
是
|
||
</Button>
|
||
</Group>
|
||
}>
|
||
与上一帧数据存在冲突,是否要复制上一帧数据并覆盖吗?
|
||
</CustomModal>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export default BottomTools
|