956 lines
37 KiB
TypeScript
956 lines
37 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
ActionIcon,
|
||
Badge,
|
||
Box,
|
||
Checkbox,
|
||
Collapse,
|
||
Flex,
|
||
Group,
|
||
Paper,
|
||
Popover,
|
||
ScrollArea,
|
||
Stack,
|
||
Text,
|
||
TextInput,
|
||
Title,
|
||
Tooltip,
|
||
} from "@mantine/core"
|
||
import dayjs from "dayjs"
|
||
import {
|
||
ChevronDown,
|
||
ChevronUp,
|
||
CircleDot,
|
||
Eye,
|
||
EyeOff,
|
||
Info,
|
||
RectangleHorizontal,
|
||
Settings,
|
||
Spline,
|
||
} from "lucide-react"
|
||
import paper from "paper"
|
||
import { useCallback, useState } from "react"
|
||
import { Project } from "../api/project/typing"
|
||
import { Task } from "../api/task/typing"
|
||
import { LabelState } from "../LabelNossr"
|
||
import { useKeyEventStore, useLabelStore, useObjectStore } from "../store"
|
||
import { useAllUserStore } from "../store/auth"
|
||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||
import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore"
|
||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
import { getSubAttrStatus } from "../util"
|
||
import { labelTypeMap } from "../utils/constants"
|
||
|
||
interface RightObjectToolsComponentProps {
|
||
labelState: LabelState
|
||
projectDetail?: Project.DetailResponse
|
||
taskDetail?: Task.DataProps
|
||
}
|
||
|
||
export const renderOperationIcon = (
|
||
operationSchema: Project.LabelSchemaList | null
|
||
) => {
|
||
if (operationSchema) {
|
||
let color = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||
const style = { color }
|
||
switch (operationSchema && labelTypeMap.get(operationSchema.label_type)) {
|
||
case "多边形": {
|
||
return <Settings size={20} style={style} />
|
||
}
|
||
case "关键点": {
|
||
return <CircleDot size={20} style={style} />
|
||
}
|
||
case "多线段": {
|
||
return <Spline size={20} style={style} />
|
||
}
|
||
case "2D框": {
|
||
return <RectangleHorizontal size={20} style={style} />
|
||
}
|
||
default:
|
||
return <Settings size={20} />
|
||
}
|
||
} else return <Settings size={20} />
|
||
}
|
||
|
||
const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||
const { labelState, projectDetail, taskDetail } = props
|
||
|
||
const storeLabel = useLabelStore((state) => state.label)
|
||
|
||
const { activeImage } = useBottomToolsStore()
|
||
|
||
const {
|
||
operationStatus,
|
||
pathStatus,
|
||
selectedPath,
|
||
imageStatus,
|
||
updateImageStatus,
|
||
updateOperationStatus,
|
||
updatePathStatus,
|
||
updateSelectedPath,
|
||
} = useObjectStore()
|
||
|
||
const { userOpts } = useAllUserStore()
|
||
|
||
const [searchId, setSearchId] = useState("")
|
||
const [searchSubAttrs, setSearchSubAttrs] = useState<{
|
||
[x: string]: string
|
||
}>({})
|
||
const [checkBoxsChecked, setCheckBoxsChecked] = useState([
|
||
true,
|
||
true,
|
||
true,
|
||
true,
|
||
])
|
||
|
||
const getOperationSchema = useCallback(
|
||
(operationId: string) => {
|
||
return (
|
||
projectDetail?.label_schema_list?.find(
|
||
(item) => item.category_id === +operationId
|
||
) || null
|
||
)
|
||
},
|
||
[projectDetail?.label_schema_list]
|
||
)
|
||
|
||
const renderShortcutKey = (operationId: string) => {
|
||
if (+operationId > 10) {
|
||
return "ctrl+" + (+operationId % 10).toString()
|
||
} else {
|
||
return (+operationId % 10).toString()
|
||
}
|
||
}
|
||
|
||
const getCheckBoxColor = useCallback((type: number) => {
|
||
switch (type) {
|
||
case 3:
|
||
return "yellow"
|
||
case 2:
|
||
return "red"
|
||
case 1:
|
||
return "green"
|
||
case 0:
|
||
return "gray"
|
||
default:
|
||
return "blue"
|
||
}
|
||
}, [])
|
||
|
||
const { shift: pressShift } = useKeyEventStore()
|
||
|
||
const { getItemById, getItemsById } = usePaperStore()
|
||
|
||
// 切换选中对象时,清除所有其他对象的当前选中状态
|
||
const clearSelectedItems = () => {
|
||
usePaperStore
|
||
.getState()
|
||
.paperScope?.project.selectedItems.forEach((item) => {
|
||
item.data.selected = false
|
||
})
|
||
// const group = usePaperStore.getState().group!;
|
||
// const needClear = [
|
||
// ...group.getItems({ data: { type: "mask" } }),
|
||
// ...group.getItems({ data: { type: "text" } }),
|
||
// ];
|
||
// needClear.forEach((item) => {
|
||
// item.remove();
|
||
// });
|
||
usePaperSupportStore.getState().clearAll()
|
||
}
|
||
|
||
const handleSelectedPath = (path: paper.Path) => {
|
||
console.log("handleSelectedPath", path)
|
||
if (path.data.type === "rectangle") {
|
||
path.data.selected = true
|
||
usePaperSupportStore.getState().handleBufferPaths(path)
|
||
usePaperSupportStore.getState().handleCircles(path)
|
||
path.fillColor = path.data.fillColor
|
||
} else if (path.data.type === "brush") {
|
||
path.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
usePaperSupportStore.getState().handleBufferPaths(path)
|
||
usePaperSupportStore.getState().handleCircles(path)
|
||
} else if (path.data.type === "point") {
|
||
path.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
usePaperSupportStore.getState().handleText(path)
|
||
usePaperSupportStore.getState().handleBufferPaths(path)
|
||
usePaperSupportStore.getState().handleCircles(path)
|
||
} else if (path.data.type === "polygon") {
|
||
path.fillColor = path.data.fillColor
|
||
}
|
||
path.bringToFront()
|
||
}
|
||
|
||
const cancelSelectedPathColor = (path: paper.Path | paper.CompoundPath) => {
|
||
if (path.data.type === "rectangle" || path.data.type === "polygon")
|
||
path.fillColor = path.data.blankColor
|
||
}
|
||
|
||
const handleSelect = useCallback(
|
||
(id: number, operationId: any) => {
|
||
if (selectedPath[activeImage]?.length) {
|
||
if (!selectedPath[activeImage]?.includes(id)) {
|
||
if (pressShift) {
|
||
if (getItemsById(id)?.length) {
|
||
getItemsById(id).forEach((item) => {
|
||
if (item instanceof paper.Path) {
|
||
handleSelectedPath(item)
|
||
} else if (item instanceof paper.CompoundPath) {
|
||
if (item.data.type === "polygon")
|
||
item.fillColor = item.data.fillColor
|
||
else item.data.selected = true
|
||
}
|
||
})
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === operationId
|
||
)
|
||
const globalPosition = getItemById(id)!.globalMatrix.transform(
|
||
getItemById(id)!.position
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
globalPosition,
|
||
category!
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
id,
|
||
})
|
||
}
|
||
} else {
|
||
clearSelectedItems()
|
||
selectedPath[activeImage].forEach((selectedId) => {
|
||
if (getItemsById(selectedId)?.length) {
|
||
getItemsById(selectedId).forEach((item) => {
|
||
cancelSelectedPathColor(item)
|
||
})
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
id,
|
||
})
|
||
}
|
||
})
|
||
if (getItemsById(id)?.length) {
|
||
getItemsById(id).forEach((item) => {
|
||
if (item instanceof paper.Path) {
|
||
handleSelectedPath(item)
|
||
} else if (item instanceof paper.CompoundPath) {
|
||
if (item.data.type === "polygon")
|
||
item.fillColor = item.data.fillColor
|
||
else item.data.selected = true
|
||
}
|
||
})
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === operationId
|
||
)
|
||
const globalPosition = getItemById(id)!.globalMatrix.transform(
|
||
getItemById(id)!.position
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
globalPosition,
|
||
category!
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
id,
|
||
})
|
||
}
|
||
}
|
||
updateSelectedPath(activeImage, "ADD", id)
|
||
} else {
|
||
clearSelectedItems()
|
||
updateSelectedPath(activeImage, "DELETE", id)
|
||
if (getItemsById(id)?.length) {
|
||
getItemsById(id).forEach((item) => {
|
||
cancelSelectedPathColor(item)
|
||
})
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
id,
|
||
})
|
||
}
|
||
}
|
||
} else {
|
||
clearSelectedItems()
|
||
updateSelectedPath(activeImage, "ADD", id)
|
||
if (getItemsById(id)?.length) {
|
||
getItemsById(id).forEach((item) => {
|
||
if (item instanceof paper.Path) {
|
||
handleSelectedPath(item)
|
||
} else if (item instanceof paper.CompoundPath) {
|
||
if (item.data.type === "polygon")
|
||
item.fillColor = item.data.fillColor
|
||
else item.data.selected = true
|
||
}
|
||
// if (item.data.type === "rectangle") {
|
||
// item.data.selected = true;
|
||
// usePaperSupportStore.getState().handleCircles(item);
|
||
// }
|
||
// item.fillColor = item.data?.fillColor;
|
||
// item.bringToFront();
|
||
})
|
||
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === operationId
|
||
)
|
||
const globalPosition = getItemById(id)!.globalMatrix.transform(
|
||
getItemById(id)!.position
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
globalPosition,
|
||
category!
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
id,
|
||
})
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
getItemById,
|
||
getItemsById,
|
||
pressShift,
|
||
selectedPath,
|
||
updateSelectedPath,
|
||
]
|
||
)
|
||
|
||
// 根据img operationId 更新操作栏operation状态
|
||
const checkIfOperationNeedsUpdate = (imgId: string, operationId: any) => {
|
||
const operationMap = storeLabel.get(imgId)
|
||
if (operationMap) {
|
||
const objList = operationMap.get(operationId)
|
||
if (objList && objList.length) {
|
||
let hideFlag = true
|
||
objList.forEach(([id]) => {
|
||
const objStatus =
|
||
useObjectStore.getState().pathStatus[`${imgId}${id}`]
|
||
if (objStatus && !objStatus["HIDE"]) {
|
||
hideFlag = false
|
||
}
|
||
})
|
||
updateOperationStatus(imgId + operationId, "HIDE", hideFlag)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 隐藏时清空当前选中的item选中状态
|
||
const setSelectedItemFalse = (id: number) => {
|
||
if (!id) return
|
||
const sameIdItems = usePaperStore.getState().getItemsById(id)
|
||
sameIdItems.forEach((i) => {
|
||
if (i.data.type === "mask" || i.data.type === "text") {
|
||
i.remove()
|
||
}
|
||
if (i.data.selected) i.data.selected = false
|
||
})
|
||
}
|
||
|
||
const operationHide = useCallback(
|
||
(operationId: string, hide: boolean) => {
|
||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
||
|
||
storeLabel
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.map((child) => {
|
||
updatePathStatus(activeImage + child[0], "HIDE", hide)
|
||
if (getItemsById(child[0]) && getItemsById(child[0]).length) {
|
||
getItemsById(child[0]).forEach((item) => {
|
||
item.visible = !hide
|
||
})
|
||
// 隐藏时清空选中状态
|
||
if (hide) setSelectedItemFalse(child[0])
|
||
}
|
||
})
|
||
},
|
||
[
|
||
activeImage,
|
||
getItemsById,
|
||
storeLabel,
|
||
updateOperationStatus,
|
||
updatePathStatus,
|
||
]
|
||
)
|
||
|
||
const objectHide = useCallback(
|
||
(operationId: string, objectId: number, hide: boolean) => {
|
||
if (hide) {
|
||
updatePathStatus(activeImage + objectId, "HIDE", hide)
|
||
if (getItemsById(objectId) && getItemsById(objectId).length) {
|
||
getItemsById(objectId).forEach((item) => (item.visible = !hide))
|
||
// 隐藏时清空选中状态
|
||
setSelectedItemFalse(objectId)
|
||
}
|
||
let childrenHideStatus = storeLabel
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.map((child) => {
|
||
return getItemById(child[0])!.visible
|
||
})
|
||
if (childrenHideStatus?.findIndex((bool) => bool === true) === -1) {
|
||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
||
}
|
||
} else {
|
||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
||
updatePathStatus(activeImage + objectId, "HIDE", hide)
|
||
const configs = useTopToolsStore.getState().showTagsConfigs
|
||
if (getItemsById(objectId) && getItemsById(objectId).length)
|
||
getItemsById(objectId).forEach((item) => {
|
||
let visible = !hide
|
||
if (item.data.type === "tag") {
|
||
if (item.data.tag_category === "mask") {
|
||
visible = configs.includes("外框")
|
||
} else if (item.data.tag_category === "point_text") {
|
||
visible = configs.includes("点ID")
|
||
} else {
|
||
visible = true
|
||
}
|
||
}
|
||
item.visible = visible
|
||
})
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
getItemById,
|
||
getItemsById,
|
||
storeLabel,
|
||
updateOperationStatus,
|
||
updatePathStatus,
|
||
]
|
||
)
|
||
|
||
// const renderObjectName = useCallback((name: string, input: string) => {
|
||
// const index = name?.indexOf(input);
|
||
// const beforeStr = name?.substring(0, index);
|
||
// const afterStr = name?.slice(index + input.length);
|
||
// return (
|
||
// <span>
|
||
// {beforeStr}
|
||
// <span className="text-primary">{input}</span>
|
||
// {afterStr}
|
||
// </span>
|
||
// );
|
||
// }, []);
|
||
|
||
// child 是否存在不符合要求的子属性
|
||
const getOperationSubAttrStatus = useCallback(
|
||
(operationId: string) => {
|
||
let bool = storeLabel
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.some((child) => {
|
||
return getSubAttrStatus(
|
||
getOperationSchema(operationId),
|
||
child[2]?.sub_attributes
|
||
)
|
||
})
|
||
return bool
|
||
},
|
||
[activeImage, getOperationSchema, storeLabel]
|
||
)
|
||
|
||
// 搜索标注对象id
|
||
const handleSelectedBySearchId = (e: any) => {
|
||
if (e.key === "Enter") {
|
||
if (searchId && !isNaN(Number(searchId))) {
|
||
const id = Number(searchId)
|
||
const labelObjMap = storeLabel.get(activeImage)
|
||
Array.from(labelObjMap!.entries()).forEach(([operationId, objList]) => {
|
||
objList.forEach((obj) => {
|
||
if (id === obj[0]) {
|
||
handleSelect(id, operationId)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 搜索标注对象子属性
|
||
const handleSelectedBySearchSubAttr = (e: any, currentOperationId: any) => {
|
||
if (e.key === "Enter") {
|
||
Array.from(storeLabel.entries()).forEach(([imgId, objectMap]) => {
|
||
if (objectMap.get(currentOperationId)) {
|
||
const list = objectMap.get(currentOperationId)
|
||
list!.forEach((item) => {
|
||
const { sub_attributes } = item[2]
|
||
// 如果没有子属性搜索值,则把对象HIDE设为false
|
||
if (!searchSubAttrs[currentOperationId]) {
|
||
updatePathStatus(imgId + item[0], "HIDE", false)
|
||
updateOperationStatus(imgId + currentOperationId, "HIDE", false)
|
||
if (getItemsById(item[0]))
|
||
getItemsById(item[0]).forEach((i) => {
|
||
i.visible = true
|
||
})
|
||
return
|
||
}
|
||
if (sub_attributes && Object.entries(sub_attributes).length) {
|
||
let flag = true
|
||
Object.entries(sub_attributes).forEach(([_k, value]) => {
|
||
if (searchSubAttrs[currentOperationId] === value) {
|
||
flag = false
|
||
}
|
||
})
|
||
if (flag) {
|
||
updatePathStatus(imgId + item[0], "HIDE", true)
|
||
if (getItemsById(item[0])) {
|
||
getItemsById(item[0]).forEach((i) => {
|
||
i.visible = false
|
||
})
|
||
setSelectedItemFalse(item[0])
|
||
}
|
||
} else {
|
||
updatePathStatus(imgId + item[0], "HIDE", false)
|
||
if (getItemsById(item[0]))
|
||
getItemsById(item[0]).forEach((i) => {
|
||
i.visible = true
|
||
})
|
||
}
|
||
} else {
|
||
// 如果有子属性搜索值,但对象无子属性,则把对象HIDE设为true
|
||
updatePathStatus(imgId + item[0], "HIDE", true)
|
||
if (getItemsById(item[0])) {
|
||
getItemsById(item[0]).forEach((i) => {
|
||
i.visible = false
|
||
})
|
||
setSelectedItemFalse(item[0])
|
||
}
|
||
}
|
||
})
|
||
checkIfOperationNeedsUpdate(imgId, currentOperationId)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Box h="100%" w="100%">
|
||
<Flex direction="column" h="100%" gap="xs">
|
||
<Flex justify="space-between" align="center" px="md" py="xs">
|
||
<Title order={4} size="h4">
|
||
对象列表
|
||
</Title>
|
||
<Group gap="xs">
|
||
<TextInput
|
||
w={96}
|
||
size="xs"
|
||
placeholder="输入ID"
|
||
value={searchId}
|
||
onChange={(e) => setSearchId(e.target.value)}
|
||
onKeyDown={handleSelectedBySearchId}
|
||
/>
|
||
<Group gap={4}>
|
||
{[3, 2, 1, 0].map((type, index) => (
|
||
<Checkbox
|
||
key={type}
|
||
id={type + ""}
|
||
color={getCheckBoxColor(type)}
|
||
checked={checkBoxsChecked[index]}
|
||
onChange={(e) => {
|
||
setCheckBoxsChecked((pre) => {
|
||
const arr = [...pre]
|
||
arr[index] = e.target.checked
|
||
return arr
|
||
})
|
||
}}
|
||
size="xs"
|
||
/>
|
||
))}
|
||
</Group>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
onClick={() => {
|
||
updateImageStatus(
|
||
activeImage,
|
||
"COLLAPSE",
|
||
!imageStatus[activeImage]?.["COLLAPSE"]
|
||
)
|
||
}}>
|
||
{!imageStatus[activeImage]?.["COLLAPSE"] ? (
|
||
<ChevronUp size={16} />
|
||
) : (
|
||
<ChevronDown size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
</Group>
|
||
</Flex>
|
||
<Collapse
|
||
in={!imageStatus[activeImage]?.["COLLAPSE"]}
|
||
style={{
|
||
flex: 1,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
overflow: "hidden",
|
||
}}>
|
||
<ScrollArea style={{ flex: 1 }} px="md">
|
||
<Stack gap="xs" pb="md">
|
||
{labelState.get(activeImage) &&
|
||
labelState.get(activeImage)?.map((operationId) => {
|
||
const isCollapsed =
|
||
operationStatus[activeImage + operationId]?.["COLLAPSE"]
|
||
const isHide =
|
||
operationStatus[activeImage + operationId]?.HIDE
|
||
const hasSubAttrError = getOperationSubAttrStatus(operationId)
|
||
return (
|
||
<Box key={operationId} w="100%">
|
||
<Paper withBorder p="xs" radius="md" shadow="sm">
|
||
<Flex justify="space-between" align="center">
|
||
<Group gap="xs">
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
onClick={() =>
|
||
operationHide(operationId, !isHide)
|
||
}>
|
||
{!isHide ? (
|
||
<Eye size={16} />
|
||
) : (
|
||
<EyeOff size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
{renderOperationIcon(
|
||
getOperationSchema(operationId)
|
||
)}
|
||
<Text
|
||
size="sm"
|
||
c={hasSubAttrError ? "red" : undefined}>
|
||
{renderShortcutKey(operationId)}
|
||
</Text>
|
||
</Group>
|
||
<Text
|
||
size="sm"
|
||
c={hasSubAttrError ? "red" : undefined}>
|
||
{getOperationSchema(operationId)?.label_class}
|
||
</Text>
|
||
<Group gap="xs">
|
||
<TextInput
|
||
w={96}
|
||
size="xs"
|
||
placeholder="搜索子属性"
|
||
value={searchSubAttrs[operationId] || ""}
|
||
onChange={(e) => {
|
||
setSearchSubAttrs((searchValues) => ({
|
||
...searchValues,
|
||
[operationId]: e.target.value,
|
||
}))
|
||
}}
|
||
onKeyDown={(e) =>
|
||
handleSelectedBySearchSubAttr(e, operationId)
|
||
}
|
||
/>
|
||
<Text size="sm">
|
||
{storeLabel.get(activeImage)?.get(operationId)
|
||
?.length || 0}
|
||
</Text>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
onClick={() => {
|
||
updateOperationStatus(
|
||
activeImage + operationId,
|
||
"COLLAPSE",
|
||
!isCollapsed
|
||
)
|
||
}}>
|
||
{!isCollapsed ? (
|
||
<ChevronUp size={16} />
|
||
) : (
|
||
<ChevronDown size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
</Group>
|
||
</Flex>
|
||
</Paper>
|
||
<Collapse in={!isCollapsed}>
|
||
<Stack gap="xs" mt="xs" pl="xs">
|
||
{storeLabel
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.map((child) => {
|
||
const { comment } = child[2]
|
||
let visibleFlag = true
|
||
if (comment && comment.length) {
|
||
const latest_comment =
|
||
(taskDetail &&
|
||
taskDetail.label_status === 2) ||
|
||
comment.length === 1
|
||
? comment[comment.length - 1]
|
||
: comment[comment.length - 2]
|
||
let type = latest_comment.comment_type
|
||
if (type === 2) {
|
||
const { last_modified_timestamp } = child[2]
|
||
if (
|
||
last_modified_timestamp &&
|
||
last_modified_timestamp >
|
||
latest_comment.date
|
||
)
|
||
type = 3
|
||
}
|
||
visibleFlag = checkBoxsChecked[3 - type]
|
||
}
|
||
if (checkBoxsChecked.every((value) => !value))
|
||
visibleFlag = true
|
||
|
||
const isSelected = selectedPath[
|
||
activeImage
|
||
]?.includes(child[0])
|
||
const isChildHide =
|
||
pathStatus[activeImage + child[0]]?.["HIDE"]
|
||
const hasChildError = getSubAttrStatus(
|
||
getOperationSchema(operationId),
|
||
child[2]?.sub_attributes
|
||
)
|
||
|
||
let textColor = undefined
|
||
|
||
if (hasChildError) {
|
||
textColor = "red"
|
||
} else if (isSelected) {
|
||
textColor = "white"
|
||
}
|
||
|
||
if (!visibleFlag) return null
|
||
|
||
return (
|
||
<Paper
|
||
key={child[0]}
|
||
withBorder
|
||
p="xs"
|
||
radius="md"
|
||
shadow="sm"
|
||
bg={
|
||
isSelected
|
||
? "var(--mantine-primary-color-filled)"
|
||
: undefined
|
||
}
|
||
c={isSelected ? "white" : undefined}
|
||
onClick={() =>
|
||
handleSelect(child[0], operationId)
|
||
}
|
||
style={{ cursor: "pointer" }}>
|
||
<Flex justify="space-between" align="center">
|
||
<Group gap="xs">
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
c={isSelected ? "white" : undefined}
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
objectHide(
|
||
operationId,
|
||
child[0],
|
||
!isChildHide
|
||
)
|
||
}}>
|
||
{!isChildHide ? (
|
||
<Eye size={16} />
|
||
) : (
|
||
<EyeOff size={16} />
|
||
)}
|
||
</ActionIcon>
|
||
<Text size="sm" c={textColor}>
|
||
{child[0]?.toString()}
|
||
</Text>
|
||
<Tooltip
|
||
label={
|
||
<Stack gap={0}>
|
||
<Group gap={4}>
|
||
<Text size="xs">创建:</Text>
|
||
<Text size="xs">
|
||
{
|
||
userOpts.find(
|
||
(item) =>
|
||
item.value ===
|
||
child[2]?.uid
|
||
)?.label
|
||
}
|
||
</Text>
|
||
<Text size="xs">
|
||
{dayjs(
|
||
(child[2]?.create_timestamp ||
|
||
0) * 1000
|
||
).format("YYYY-MM-DD HH:mm:ss")}
|
||
</Text>
|
||
</Group>
|
||
{child[2]?.first_modified_uid && (
|
||
<Group gap={4}>
|
||
<Text size="xs">
|
||
首次修改:
|
||
</Text>
|
||
<Text size="xs">
|
||
{
|
||
userOpts.find(
|
||
(item) =>
|
||
item.value ===
|
||
child[2]
|
||
?.first_modified_uid
|
||
)?.label
|
||
}
|
||
</Text>
|
||
<Text size="xs">
|
||
{dayjs(
|
||
(child[2]
|
||
?.first_modified_timestamp ||
|
||
0) * 1000
|
||
).format(
|
||
"YYYY-MM-DD HH:mm:ss"
|
||
)}
|
||
</Text>
|
||
</Group>
|
||
)}
|
||
{child[2]?.last_modified_uid && (
|
||
<Group gap={4}>
|
||
<Text size="xs">
|
||
最新修改:
|
||
</Text>
|
||
<Text size="xs">
|
||
{
|
||
userOpts.find(
|
||
(item) =>
|
||
item.value ===
|
||
child[2]
|
||
?.last_modified_uid
|
||
)?.label
|
||
}
|
||
</Text>
|
||
<Text size="xs">
|
||
{dayjs(
|
||
(child[2]
|
||
?.last_modified_timestamp ||
|
||
0) * 1000
|
||
).format(
|
||
"YYYY-MM-DD HH:mm:ss"
|
||
)}
|
||
</Text>
|
||
</Group>
|
||
)}
|
||
</Stack>
|
||
}
|
||
multiline
|
||
bg="white"
|
||
c="black"
|
||
withArrow>
|
||
<Info
|
||
size={16}
|
||
style={{ cursor: "pointer" }}
|
||
color={isSelected ? "white" : "black"}
|
||
/>
|
||
</Tooltip>
|
||
</Group>
|
||
{comment ? (
|
||
<Group gap={4}>
|
||
{comment.map((c: any) => {
|
||
const count =
|
||
c.check_box_comments.length +
|
||
c.text_comments.length
|
||
const { last_modified_timestamp } =
|
||
child[2]
|
||
let flag = false
|
||
if (
|
||
count > 0 &&
|
||
last_modified_timestamp &&
|
||
last_modified_timestamp > c.date
|
||
)
|
||
flag = true
|
||
|
||
let badgeColor = "gray"
|
||
if (c.comment_type === 1)
|
||
badgeColor = "green"
|
||
if (c.comment_type === 2)
|
||
badgeColor = "red"
|
||
if (flag) badgeColor = "yellow"
|
||
|
||
return (
|
||
<Popover
|
||
key={`${child[0]}${c.turn}${c.times}`}
|
||
position="left"
|
||
withArrow>
|
||
<Popover.Target>
|
||
<Badge
|
||
size="xs"
|
||
circle
|
||
color={badgeColor}
|
||
style={{
|
||
cursor: "pointer",
|
||
}}>
|
||
{count}
|
||
</Badge>
|
||
</Popover.Target>
|
||
<Popover.Dropdown>
|
||
<Text size="xs" c={badgeColor}>
|
||
{`P${child[2].image_id}:${
|
||
child[2].uid
|
||
}:${c.check_box_comments.join(
|
||
","
|
||
)},${c.text_comments.join(
|
||
","
|
||
)} ${
|
||
c.date
|
||
? dayjs(
|
||
c.date * 1000
|
||
).format(
|
||
"YYYY-MM-DD HH:mm:ss"
|
||
)
|
||
: ""
|
||
}`}
|
||
</Text>
|
||
</Popover.Dropdown>
|
||
</Popover>
|
||
)
|
||
})}
|
||
</Group>
|
||
) : null}
|
||
</Flex>
|
||
</Paper>
|
||
)
|
||
})}
|
||
</Stack>
|
||
</Collapse>
|
||
</Box>
|
||
)
|
||
})}
|
||
</Stack>
|
||
</ScrollArea>
|
||
</Collapse>
|
||
</Flex>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export default RightObjectTools
|