1078 lines
42 KiB
TypeScript
1078 lines
42 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, useMemo, 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"
|
||
import { toggleObjectHideByShortcut } from "../utils/objectVisibility"
|
||
|
||
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 continuePolygonTarget = usePaperStore(
|
||
(state) => state.continuePolygonTarget
|
||
)
|
||
const { getItemById, getItemsById } = usePaperStore()
|
||
const continueObjectId =
|
||
continuePolygonTarget?.imageId === activeImage
|
||
? continuePolygonTarget.objectId
|
||
: null
|
||
const objectOperations = useTopToolsStore((state) => state.objectOperations)
|
||
|
||
const getOperationChildren = useCallback(
|
||
(imageId: string, operationId: string) => {
|
||
const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
|
||
if (continueObjectId === null || imageId !== activeImage) {
|
||
return operationChildren
|
||
}
|
||
return operationChildren.filter(
|
||
([objectId]) => objectId !== continueObjectId
|
||
)
|
||
},
|
||
[activeImage, continueObjectId, storeLabel]
|
||
)
|
||
|
||
// 切换选中对象时,清除所有其他对象的当前选中状态
|
||
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) {
|
||
const hideFlag = objList.every(([id]) => {
|
||
return useObjectStore.getState().pathStatus[`${imgId}${id}`]?.["HIDE"]
|
||
? true
|
||
: 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)
|
||
|
||
getOperationChildren(activeImage, operationId).map((child) => {
|
||
toggleObjectHideByShortcut(activeImage, operationId, child[0], hide)
|
||
})
|
||
},
|
||
[activeImage, getOperationChildren, updateOperationStatus]
|
||
)
|
||
|
||
const objectHide = useCallback(
|
||
(operationId: string, objectId: number, hide: boolean) => {
|
||
toggleObjectHideByShortcut(activeImage, operationId, objectId, hide)
|
||
},
|
||
[activeImage]
|
||
)
|
||
|
||
const activeImageOperations = useMemo(() => {
|
||
const schemaOperationIds = objectOperations.map((item) =>
|
||
item.category_id.toString()
|
||
)
|
||
const imageOperationIds = labelState.get(activeImage) || []
|
||
return Array.from(new Set([...schemaOperationIds, ...imageOperationIds]))
|
||
}, [activeImage, labelState, objectOperations])
|
||
|
||
const isObjectHidden = (imageId: string, objectId: number) => {
|
||
return pathStatus[imageId + objectId]?.["HIDE"] ?? false
|
||
}
|
||
|
||
const isOperationHidden = (imageId: string, operationId: string) => {
|
||
const operationChildren = getOperationChildren(imageId, operationId)
|
||
return (
|
||
operationChildren.length > 0 &&
|
||
operationChildren.every(([objectId]) => isObjectHidden(imageId, objectId))
|
||
)
|
||
}
|
||
|
||
const totalObjectCount = activeImageOperations.reduce(
|
||
(count, operationId) => {
|
||
return count + getOperationChildren(activeImage, operationId).length
|
||
},
|
||
0
|
||
)
|
||
|
||
const areAllObjectsHidden =
|
||
totalObjectCount > 0 &&
|
||
activeImageOperations.every((operationId) => {
|
||
const operationChildren = getOperationChildren(activeImage, operationId)
|
||
return operationChildren.every(([objectId]) =>
|
||
isObjectHidden(activeImage, objectId)
|
||
)
|
||
})
|
||
|
||
// 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) => {
|
||
if (continueObjectId !== null && child[0] === continueObjectId) {
|
||
return false
|
||
}
|
||
return getSubAttrStatus(
|
||
getOperationSchema(operationId),
|
||
child[2]?.sub_attributes
|
||
)
|
||
})
|
||
return bool
|
||
},
|
||
[activeImage, continueObjectId, 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]) => {
|
||
false && objList
|
||
getOperationChildren(activeImage, operationId).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 = getOperationChildren(imgId, 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"
|
||
wrap="nowrap"
|
||
gap={6}
|
||
px="md"
|
||
py="xs"
|
||
style={{ minWidth: 0 }}>
|
||
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
|
||
<ActionIcon
|
||
size="sm"
|
||
variant="subtle"
|
||
aria-label={areAllObjectsHidden ? "显示全部对象" : "隐藏全部对象"}
|
||
onClick={() => {
|
||
if (!totalObjectCount) return
|
||
activeImageOperations.forEach((operationId) => {
|
||
operationHide(operationId, !areAllObjectsHidden)
|
||
})
|
||
}}>
|
||
{areAllObjectsHidden ? <EyeOff size={16} /> : <Eye size={16} />}
|
||
</ActionIcon>
|
||
<Title
|
||
order={5}
|
||
size="h5"
|
||
style={{ whiteSpace: "nowrap", lineHeight: 1.1 }}>
|
||
对象列表
|
||
</Title>
|
||
</Group>
|
||
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
|
||
<TextInput
|
||
w={72}
|
||
size="xs"
|
||
placeholder="输入ID"
|
||
value={searchId}
|
||
onChange={(e) => setSearchId(e.target.value)}
|
||
onKeyDown={handleSelectedBySearchId}
|
||
/>
|
||
<Group gap={2} wrap="nowrap">
|
||
{[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
|
||
size="sm"
|
||
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">
|
||
{activeImageOperations.map((operationId) => {
|
||
const operationSchema = getOperationSchema(operationId)
|
||
const isCollapsed =
|
||
operationStatus[activeImage + operationId]?.["COLLAPSE"]
|
||
const isHide = isOperationHidden(activeImage, operationId)
|
||
const hasSubAttrError = getOperationSubAttrStatus(operationId)
|
||
const operationLabel = operationSchema?.label_class
|
||
const operationChildren = getOperationChildren(
|
||
activeImage,
|
||
operationId
|
||
)
|
||
const objectCount = operationChildren.length
|
||
const visibleChildren = operationChildren.filter((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
|
||
return visibleFlag
|
||
})
|
||
return (
|
||
<Box key={operationId} w="100%">
|
||
<Paper withBorder p="xs" radius="md" shadow="sm">
|
||
<Flex align="center" gap="xs" style={{ minWidth: 0 }}>
|
||
<Flex
|
||
align="center"
|
||
gap="xs"
|
||
style={{ flex: 1, minWidth: 0 }}>
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
onClick={() => operationHide(operationId, !isHide)}>
|
||
{!isHide ? <Eye size={16} /> : <EyeOff size={16} />}
|
||
</ActionIcon>
|
||
<Group
|
||
gap={8}
|
||
wrap="nowrap"
|
||
style={{ flex: 1, minWidth: 0 }}>
|
||
<Group
|
||
gap={6}
|
||
wrap="nowrap"
|
||
px="xs"
|
||
py={4}
|
||
style={{
|
||
flexShrink: 0,
|
||
borderRadius: 999,
|
||
border: `1px solid ${
|
||
hasSubAttrError
|
||
? "var(--mantine-color-red-2)"
|
||
: "var(--mantine-color-gray-2)"
|
||
}`,
|
||
background: hasSubAttrError
|
||
? "var(--mantine-color-red-0)"
|
||
: "var(--mantine-color-gray-0)",
|
||
}}>
|
||
{renderOperationIcon(operationSchema)}
|
||
<Text
|
||
size="xs"
|
||
fw={700}
|
||
c={hasSubAttrError ? "red" : "dimmed"}
|
||
style={{
|
||
whiteSpace: "nowrap",
|
||
lineHeight: 1,
|
||
}}>
|
||
{renderShortcutKey(operationId)}
|
||
</Text>
|
||
</Group>
|
||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||
<Text
|
||
size="sm"
|
||
fw={500}
|
||
c={hasSubAttrError ? "red" : undefined}
|
||
title={operationLabel || undefined}
|
||
style={{
|
||
minWidth: 0,
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
whiteSpace: "nowrap",
|
||
}}>
|
||
{operationLabel}
|
||
</Text>
|
||
</Box>
|
||
</Group>
|
||
</Flex>
|
||
<Group
|
||
gap={6}
|
||
wrap="nowrap"
|
||
style={{ flexShrink: 0, marginLeft: "auto" }}>
|
||
<TextInput
|
||
w={88}
|
||
size="xs"
|
||
placeholder="搜索子属性"
|
||
value={searchSubAttrs[operationId] || ""}
|
||
onChange={(e) => {
|
||
setSearchSubAttrs((searchValues) => ({
|
||
...searchValues,
|
||
[operationId]: e.target.value,
|
||
}))
|
||
}}
|
||
onKeyDown={(e) =>
|
||
handleSelectedBySearchSubAttr(e, operationId)
|
||
}
|
||
/>
|
||
<Badge
|
||
size="sm"
|
||
variant="light"
|
||
radius="xl"
|
||
color={hasSubAttrError ? "red" : "gray"}>
|
||
{objectCount}
|
||
</Badge>
|
||
<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" ml="xs">
|
||
{visibleChildren.map((child, childIndex) => {
|
||
const { comment } = child[2]
|
||
const isSelected = selectedPath[
|
||
activeImage
|
||
]?.includes(child[0])
|
||
const isChildHide =
|
||
pathStatus[activeImage + child[0]]?.["HIDE"]
|
||
const hasChildError = getSubAttrStatus(
|
||
getOperationSchema(operationId),
|
||
child[2]?.sub_attributes
|
||
)
|
||
const isLastVisibleChild =
|
||
childIndex === visibleChildren.length - 1
|
||
|
||
let textColor = undefined
|
||
let connectorColor = "var(--mantine-color-gray-4)"
|
||
|
||
if (hasChildError) {
|
||
textColor = "red"
|
||
connectorColor = "var(--mantine-color-red-3)"
|
||
} else if (isSelected) {
|
||
textColor = "white"
|
||
connectorColor =
|
||
"var(--mantine-primary-color-filled)"
|
||
}
|
||
|
||
return (
|
||
<Box
|
||
key={child[0]}
|
||
pl="lg"
|
||
style={{ position: "relative" }}>
|
||
<Box
|
||
style={{
|
||
position: "absolute",
|
||
left: 7,
|
||
top: 0,
|
||
bottom: isLastVisibleChild ? "75%" : 0,
|
||
width: 1.5,
|
||
borderRadius: 999,
|
||
background: connectorColor,
|
||
opacity: isSelected ? 0.9 : 0.35,
|
||
}}
|
||
/>
|
||
{isLastVisibleChild ? (
|
||
<Box
|
||
style={{
|
||
position: "absolute",
|
||
left: 7,
|
||
top: "calc(50% - 10px)",
|
||
width: 14,
|
||
height: 10,
|
||
borderLeft: `1.5px solid ${connectorColor}`,
|
||
borderBottom: `1.5px solid ${connectorColor}`,
|
||
borderBottomLeftRadius: 10,
|
||
opacity: isSelected ? 0.9 : 0.55,
|
||
}}
|
||
/>
|
||
) : (
|
||
<>
|
||
<Box
|
||
style={{
|
||
position: "absolute",
|
||
left: 7,
|
||
top: "50%",
|
||
width: 14,
|
||
borderTop: `1.5px solid ${connectorColor}`,
|
||
opacity: isSelected ? 0.9 : 0.45,
|
||
}}
|
||
/>
|
||
<Box
|
||
style={{
|
||
position: "absolute",
|
||
left: 4,
|
||
top: "calc(50% - 3px)",
|
||
width: 6,
|
||
height: 6,
|
||
borderRadius: 999,
|
||
background: connectorColor,
|
||
boxShadow:
|
||
"0 0 0 2px var(--mantine-color-body)",
|
||
opacity: isSelected ? 1 : 0.85,
|
||
}}
|
||
/>
|
||
</>
|
||
)}
|
||
<Paper
|
||
withBorder
|
||
p="xs"
|
||
radius="md"
|
||
shadow="sm"
|
||
bg={
|
||
isSelected
|
||
? "var(--mantine-primary-color-filled)"
|
||
: "var(--mantine-color-gray-0)"
|
||
}
|
||
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>
|
||
</Box>
|
||
)
|
||
})}
|
||
</Stack>
|
||
</Collapse>
|
||
</Box>
|
||
)
|
||
})}
|
||
</Stack>
|
||
</ScrollArea>
|
||
</Collapse>
|
||
</Flex>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export default RightObjectTools
|