Merge branch 'zh' into 'main'

Zh

See merge request prism/lable/labelimage!7
This commit is contained in:
刘耀勇
2026-04-08 16:50:28 +08:00
13 changed files with 1944 additions and 222 deletions

View File

@@ -24,6 +24,7 @@ import {
Collapse, Collapse,
Flex, Flex,
Group, Group,
Modal,
MultiSelect, MultiSelect,
Select, Select,
SimpleGrid, SimpleGrid,
@@ -73,6 +74,8 @@ function createInitialFilters(): FilterFormRecord {
} }
} }
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20]
export interface selectedDataProps { export interface selectedDataProps {
status: number status: number
uid: number | null uid: number | null
@@ -148,6 +151,12 @@ export default function TaskTableContainer(props: {
const [pageNumber, setPageNumber] = useState(1) const [pageNumber, setPageNumber] = useState(1)
const [pageSize, setPageSize] = useState(15) const [pageSize, setPageSize] = useState(15)
const [recordsPerPageOptions, setRecordsPerPageOptions] = useState<number[]>(
() => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS]
)
const [customPageSizeInput, setCustomPageSizeInput] = useState("")
const [customPageSizeModalOpened, setCustomPageSizeModalOpened] =
useState(false)
const [searchExpanded, setSearchExpanded] = useState(false) const [searchExpanded, setSearchExpanded] = useState(false)
@@ -259,6 +268,53 @@ export default function TaskTableContainer(props: {
}) })
}, []) }, [])
const openCustomPageSizeModal = useCallback(() => {
setCustomPageSizeModalOpened(true)
}, [])
const closeCustomPageSizeModal = useCallback(() => {
setCustomPageSizeModalOpened(false)
setCustomPageSizeInput("")
}, [])
const handleAddCustomPageSize = useCallback(() => {
const rawValue = customPageSizeInput.trim()
if (!rawValue) return
const size = Number(rawValue)
if (!Number.isInteger(size) || size <= 0) {
notifications.show({
color: "red",
title: "输入无效",
message: "请填写大于 0 的整数条数",
})
return
}
if (recordsPerPageOptions.includes(size)) {
notifications.show({
color: "blue",
title: "已存在",
message: `每页 ${size} 条已在可选项中`,
})
return
}
setRecordsPerPageOptions((prev) => [...prev, size].sort((a, b) => a - b))
closeCustomPageSizeModal()
notifications.show({
color: "green",
title: "添加成功",
message: `已新增每页 ${size}`,
})
}, [closeCustomPageSizeModal, customPageSizeInput, recordsPerPageOptions])
const handleCustomPageSizeEnter = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key !== "Enter") return
event.preventDefault()
handleAddCustomPageSize()
},
[handleAddCustomPageSize]
)
const clickDispatchTask = () => { const clickDispatchTask = () => {
if (!selectedRecords.length) { if (!selectedRecords.length) {
notifications.show({ notifications.show({
@@ -992,12 +1048,58 @@ export default function TaskTableContainer(props: {
}} }}
selectedRecords={selectedRecords} selectedRecords={selectedRecords}
onSelectedRecordsChange={setSelectedRecords} onSelectedRecordsChange={setSelectedRecords}
recordsPerPageOptions={[10, 15, 20]} recordsPerPageOptions={recordsPerPageOptions}
renderPagination={({ Controls }) => (
<Group
justify="space-between"
align="center"
w="100%"
gap="sm"
wrap="wrap">
<Group gap="xs" align="center" wrap="wrap">
<Controls.Text />
<Controls.PageSizeSelector />
<Button
variant="light"
color="gray"
size="xs"
onClick={openCustomPageSizeModal}>
</Button>
</Group>
<Controls.Pagination />
</Group>
)}
/> />
</Stack> </Stack>
</SettingContentPanel> </SettingContentPanel>
</Stack> </Stack>
<Modal
opened={customPageSizeModalOpened}
onClose={closeCustomPageSizeModal}
title="添加每页条数"
centered>
<Stack gap="sm">
<TextInput
data-autofocus
label="每页条数"
placeholder="请输入大于 0 的整数,例如 99"
value={customPageSizeInput}
onChange={(event) =>
setCustomPageSizeInput(event.currentTarget.value)
}
onKeyDown={handleCustomPageSizeEnter}
/>
<Group justify="flex-end">
<Button variant="default" onClick={closeCustomPageSizeModal}>
</Button>
<Button onClick={handleAddCustomPageSize}></Button>
</Group>
</Stack>
</Modal>
<WorkflowModal <WorkflowModal
opened={workflowTaskId !== null} opened={workflowTaskId !== null}
taskId={workflowTaskId} taskId={workflowTaskId}

View File

@@ -80,6 +80,9 @@ const initialState: LabelState = new Map()
const operationTypeList = [101, 102, 103, 104] const operationTypeList = [101, 102, 103, 104]
let latestProjectDetailRequestSeq = 0 let latestProjectDetailRequestSeq = 0
let latestTaskDetailRequestSeq = 0 let latestTaskDetailRequestSeq = 0
const RIGHT_PANEL_TRANSITION =
"width 160ms ease, min-width 160ms ease, opacity 160ms ease"
const RIGHT_PANEL_WILL_CHANGE = "width, min-width, opacity"
// utils // utils
const getRectPoints = ([sx, sy, w, h]: number[]) => { const getRectPoints = ([sx, sy, w, h]: number[]) => {
@@ -95,6 +98,19 @@ const getSegmentPoints = (arr: Array<number[]>) => {
return arr.map((item) => [item[0], item[1]]) return arr.map((item) => [item[0], item[1]])
} }
const getRightPanelViewportStyle = (width: number) => {
const stableWidth = Math.max(0, Math.round(width))
return {
position: "absolute" as const,
top: 0,
left: 0,
width: stableWidth,
minWidth: stableWidth,
height: "100%",
contain: "layout paint",
}
}
const videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i const videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i
const GLOBAL_LOADING_Z_INDEX = 900 const GLOBAL_LOADING_Z_INDEX = 900
const videoMimeByExt: Record<string, string> = { const videoMimeByExt: Record<string, string> = {
@@ -2170,7 +2186,13 @@ const LabelPage = ({
if (e.altKey && e.key.toLowerCase() === "x") { if (e.altKey && e.key.toLowerCase() === "x") {
e.preventDefault() e.preventDefault()
if (!isView) topRef.current?.commitTaskByActionKey?.() if (!isView) {
if (topRef.current?.requestSubmitTaskConfirm) {
topRef.current.requestSubmitTaskConfirm()
} else {
topRef.current?.commitTaskByActionKey?.()
}
}
return return
} }
@@ -2551,17 +2573,16 @@ const LabelPage = ({
} }
}, []) }, [])
const mainBoxHeight = useMemo(() => { const bottomToolsHeight = useMemo(() => {
if (projectDetail?.label_type === 5 && metaOperation) { if (projectDetail?.label_type === 5 && metaOperation) {
let h = `calc(100% - ${headerHeight}px - 198px)` return 128
return h
} else if (showMultiFrame) {
let h = `calc(100% - ${headerHeight}px - 150px)`
return h
} }
let h = `calc(100% - ${headerHeight}px - 70px)` return 80
return h }, [metaOperation, projectDetail?.label_type])
}, [projectDetail?.label_type, metaOperation, showMultiFrame, headerHeight])
const mainBoxHeight = useMemo(() => {
return `calc(100% - ${headerHeight}px - ${70 + bottomToolsHeight}px)`
}, [bottomToolsHeight, headerHeight])
const selectedTagPreviewList = useMemo(() => { const selectedTagPreviewList = useMemo(() => {
if (!showTags) return [] if (!showTags) return []
@@ -2741,48 +2762,139 @@ const LabelPage = ({
</Flex> </Flex>
</Box> </Box>
</Box> </Box>
{showTaskList && ( <Box
<Box w={sizes[1]} miw={300} style={{ flexShrink: 0 }}> w={sizes[1]}
miw={showTaskList ? 300 : 0}
style={{
position: "relative",
flexShrink: 0,
overflow: "hidden",
opacity: showTaskList ? 1 : 0,
visibility: showTaskList ? "visible" : "hidden",
pointerEvents: showTaskList ? "auto" : "none",
transition: RIGHT_PANEL_TRANSITION,
willChange: RIGHT_PANEL_WILL_CHANGE,
}}>
<Box style={getRightPanelViewportStyle(Math.max(sizes[1], 300))}>
<RightTaskTools <RightTaskTools
projectDetail={projectDetail} projectDetail={projectDetail}
project_id={projectId} project_id={projectId}
task_id={taskId} task_id={taskId}
/> />
</Box> </Box>
)} </Box>
{showGroupList && ( <Box
<Box w={sizes[2]} miw={200} style={{ flexShrink: 0 }}> w={sizes[2]}
miw={showGroupList ? 200 : 0}
style={{
position: "relative",
flexShrink: 0,
overflow: "hidden",
opacity: showGroupList ? 1 : 0,
visibility: showGroupList ? "visible" : "hidden",
pointerEvents: showGroupList ? "auto" : "none",
transition: RIGHT_PANEL_TRANSITION,
willChange: RIGHT_PANEL_WILL_CHANGE,
}}>
<Box style={getRightPanelViewportStyle(Math.max(sizes[2], 200))}>
<RightGroupTools <RightGroupTools
labelState={labelState} labelState={labelState}
projectDetail={projectDetail} projectDetail={projectDetail}
/> />
</Box> </Box>
)} </Box>
{showDescList && projectDetail && projectDetail.label_type === 5 && ( <Box
<Box w={sizes[3]} miw={350} style={{ flexShrink: 0 }}> w={sizes[3]}
miw={
showDescList && projectDetail && projectDetail.label_type === 5
? 350
: 0
}
style={{
position: "relative",
flexShrink: 0,
overflow: "hidden",
opacity:
showDescList && projectDetail && projectDetail.label_type === 5
? 1
: 0,
visibility:
showDescList && projectDetail && projectDetail.label_type === 5
? "visible"
: "hidden",
pointerEvents:
showDescList && projectDetail && projectDetail.label_type === 5
? "auto"
: "none",
transition: RIGHT_PANEL_TRANSITION,
willChange: RIGHT_PANEL_WILL_CHANGE,
}}>
<Box style={getRightPanelViewportStyle(Math.max(sizes[3], 350))}>
<RightDescTools taskDetail={taskDetail} /> <RightDescTools taskDetail={taskDetail} />
</Box> </Box>
)} </Box>
{showDescList && projectDetail && projectDetail.label_type === 6 && ( <Box
<Box w={sizes[4]} miw={350} style={{ flexShrink: 0 }}> w={sizes[4]}
miw={
showDescList && projectDetail && projectDetail.label_type === 6
? 350
: 0
}
style={{
position: "relative",
flexShrink: 0,
overflow: "hidden",
opacity:
showDescList && projectDetail && projectDetail.label_type === 6
? 1
: 0,
visibility:
showDescList && projectDetail && projectDetail.label_type === 6
? "visible"
: "hidden",
pointerEvents:
showDescList && projectDetail && projectDetail.label_type === 6
? "auto"
: "none",
transition: RIGHT_PANEL_TRANSITION,
willChange: RIGHT_PANEL_WILL_CHANGE,
}}>
<Box style={getRightPanelViewportStyle(Math.max(sizes[4], 350))}>
<RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} /> <RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} />
</Box> </Box>
)} </Box>
{showObjectList && ( <Box
<Box w={sizes[5]} miw={350} style={{ flexShrink: 0 }}> w={sizes[5]}
miw={showObjectList ? 350 : 0}
style={{
position: "relative",
flexShrink: 0,
overflow: "hidden",
opacity: showObjectList ? 1 : 0,
visibility: showObjectList ? "visible" : "hidden",
pointerEvents: showObjectList ? "auto" : "none",
transition: RIGHT_PANEL_TRANSITION,
willChange: RIGHT_PANEL_WILL_CHANGE,
}}>
<Box style={getRightPanelViewportStyle(Math.max(sizes[5], 350))}>
<RightObjectTools <RightObjectTools
taskDetail={taskDetail} taskDetail={taskDetail}
labelState={labelState} labelState={labelState}
projectDetail={projectDetail} projectDetail={projectDetail}
/> />
</Box> </Box>
)} </Box>
{/* </Flex> */} {/* </Flex> */}
</Flex> </Flex>
<Box <Box
w="100%" w="100%"
h={projectDetail?.label_type === 5 && metaOperation ? 128 : 80} h={bottomToolsHeight}
display={showMultiFrame ? "block" : "none"}> style={{
overflow: "hidden",
opacity: showMultiFrame ? 1 : 0,
pointerEvents: showMultiFrame ? "auto" : "none",
transition: "opacity 160ms ease",
}}>
<BottomTools <BottomTools
labelState={labelState} labelState={labelState}
projectDetail={projectDetail} projectDetail={projectDetail}

View File

@@ -1,6 +1,6 @@
export const BASE_LABEL_API = export const BASE_LABEL_API =
process.env.NEXT_PUBLIC_ENV === "production" process.env.NEXT_PUBLIC_ENV === "production"
? "http://172.16.104.95:9110" ? "http://172.16.103.224:9110"
: process.env.NEXT_PUBLIC_ENV === "staging" : process.env.NEXT_PUBLIC_ENV === "staging"
? "http://172.16.115.128:9110" ? "http://172.16.115.128:9110"
: "https://label.softtest.cowarobot.com" : "https://label.softtest.cowarobot.com"

View File

@@ -120,9 +120,16 @@ export const getAuxiliaryAnnotation = (data: any) => {
}) })
} }
const BASE_SAM_API =
process.env.NEXT_PUBLIC_ENV === "production"
? "http://172.16.103.224:9120"
: process.env.NEXT_PUBLIC_ENV === "staging"
? "http://172.30.21.211:8000"
: "http://172.30.21.211:8000"
export const getAuxiliaryAnnotation2 = (data: { export const getAuxiliaryAnnotation2 = (data: {
project_id: string project_id: number
image: string image_name: string
prompt: { prompt: {
foreground_points?: Array<[number, number]> foreground_points?: Array<[number, number]>
background_points?: Array<[number, number]> background_points?: Array<[number, number]>
@@ -133,7 +140,7 @@ export const getAuxiliaryAnnotation2 = (data: {
contours: Array<Array<[[number, number]]>> contours: Array<Array<[[number, number]]>>
hollows: Array<boolean> hollows: Array<boolean>
}>({ }>({
url: "http://172.30.21.211:9000/image_predictor", url: BASE_SAM_API + "/api/sam2/image_predictor",
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}) })

View File

@@ -64,8 +64,11 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
inputNumber, inputNumber,
setInputNumber, setInputNumber,
activeImage, activeImage,
pendingImage,
frameSwitchStatus,
setActiveImage, setActiveImage,
setActiveImageId, setActiveImageId,
requestFrameSwitch,
onlyKeyFrame, onlyKeyFrame,
setOnlyKeyFrame, setOnlyKeyFrame,
keyFrameData, keyFrameData,
@@ -168,24 +171,32 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
if (index < 0 || index >= currentKeys.length) return false if (index < 0 || index >= currentKeys.length) return false
const nextImage = currentKeys[index] const nextImage = currentKeys[index]
const absoluteIndex = allKeys.findIndex((k) => k === nextImage) if (!nextImage) return false
scrollToIndex(index) scrollToIndex(index)
setInputNumber(String(index + 1)) if (
setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 0) frameSwitchStatus === "preparing" &&
setActiveImage(nextImage) pendingImage &&
pendingImage === nextImage
) {
closeRightContext()
return true
}
if (nextImage !== activeImage) {
requestFrameSwitch(nextImage)
}
closeRightContext() closeRightContext()
return true return true
}, },
[ [
allKeys, activeImage,
closeRightContext, closeRightContext,
currentKeys, currentKeys,
frameSwitchStatus,
pendingImage,
requestFrameSwitch,
scrollToIndex, scrollToIndex,
setActiveImage,
setActiveImageId,
setInputNumber,
] ]
) )
@@ -569,9 +580,14 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
radius={"sm"} radius={"sm"}
onClick={() => { onClick={() => {
let flag = false let flag = false
keyFrameData.entries().forEach(([key, value]) => { keyFrameData
if (labelState.has(key) && value?.key_frame) flag = true .entries()
}) .forEach(
([key, value]: [string, { key_frame?: boolean }]) => {
if (labelState.has(key) && value?.key_frame)
flag = true
}
)
if (!flag) { if (!flag) {
showNotification({ showNotification({
title: "警告", title: "警告",
@@ -711,6 +727,10 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
<Flex gap={0} w="max-content" h="100%" align="center"> <Flex gap={0} w="max-content" h="100%" align="center">
{currentKeys.map((key, index) => { {currentKeys.map((key, index) => {
const isActive = activeImage === key const isActive = activeImage === key
const isPending =
frameSwitchStatus === "preparing" &&
pendingImage === key &&
!isActive
const isSelected = const isSelected =
selectedItems.length && selectedItems.includes(key) selectedItems.length && selectedItems.includes(key)
const isHighlight = isActive || isSelected const isHighlight = isActive || isSelected
@@ -735,12 +755,17 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
style={{ style={{
border: isHighlight border: isHighlight
? "1px solid var(--mantine-color-blue-6)" ? "1px solid var(--mantine-color-blue-6)"
: "1px solid var(--mantine-color-gray-3)", : isPending
? "1px dashed var(--mantine-color-blue-4)"
: "1px solid var(--mantine-color-gray-3)",
borderRadius: "6px", borderRadius: "6px",
backgroundColor: isHighlight backgroundColor: isHighlight
? "var(--mantine-color-blue-6)" ? "var(--mantine-color-blue-6)"
: undefined, : isPending
? "rgba(34, 139, 230, 0.08)"
: undefined,
color: isHighlight ? "white" : undefined, color: isHighlight ? "white" : undefined,
opacity: isPending ? 0.85 : 1,
cursor: "pointer", cursor: "pointer",
position: "relative", position: "relative",
display: "flex", display: "flex",

File diff suppressed because it is too large Load Diff

View File

@@ -158,7 +158,7 @@ const TopTools = (
renderPolygons, renderPolygons,
} = props } = props
const { backUrl, basePath } = useBackUrlStore() const { backUrl, basePath } = useBackUrlStore()
const { mode, loadingData } = usePaperStore() const { mode, loadingData, requestResizeGhost } = usePaperStore()
const router = useRouter() const router = useRouter()
const user_id = usePermissionStore.getState().user_id const user_id = usePermissionStore.getState().user_id
@@ -169,6 +169,7 @@ const TopTools = (
const [duplicateName, setDuplicateName] = useState("") const [duplicateName, setDuplicateName] = useState("")
const [recoverOpen, setRecoverOpen] = useState(false) const [recoverOpen, setRecoverOpen] = useState(false)
const [taskOpen, setTaskOpen] = useState(false) const [taskOpen, setTaskOpen] = useState(false)
const [submitConfirmOpen, setSubmitConfirmOpen] = useState(false)
const [operationPickerOpened, setOperationPickerOpened] = useState(false) const [operationPickerOpened, setOperationPickerOpened] = useState(false)
const [operationKeyword, setOperationKeyword] = useState("") const [operationKeyword, setOperationKeyword] = useState("")
const labelData = useLabelStore((state) => state.label) const labelData = useLabelStore((state) => state.label)
@@ -232,6 +233,32 @@ const TopTools = (
const autoSaveGap = useIntervalStore((state) => state.autoSaveGap) const autoSaveGap = useIntervalStore((state) => state.autoSaveGap)
const setAutoSaveGap = useIntervalStore((state) => state.setAutoSaveGap) const setAutoSaveGap = useIntervalStore((state) => state.setAutoSaveGap)
const visibleRightPanelCount = useMemo(() => {
return [
showTaskList,
showGroupList,
showDescList &&
!!projectDetail &&
[5, 6].includes(projectDetail.label_type),
showObjectList,
].filter(Boolean).length
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
const toggleRightPanel = useCallback(
(visible: boolean, setter: (val: boolean) => void) => {
const nextVisible = !visible
const nextCount = visibleRightPanelCount + (nextVisible ? 1 : -1)
if (
(visibleRightPanelCount === 0 && nextCount === 1) ||
(visibleRightPanelCount === 1 && nextCount === 0)
) {
requestResizeGhost()
}
setter(nextVisible)
},
[requestResizeGhost, visibleRightPanelCount]
)
// 当前是否可以操作功能按键 // 当前是否可以操作功能按键
const currentEditAuth = useMemo(() => { const currentEditAuth = useMemo(() => {
if (!taskDetail) return false if (!taskDetail) return false
@@ -1389,6 +1416,10 @@ const TopTools = (
getNextTaskData() getNextTaskData()
} }
const requestSubmitTaskConfirm = useCallback(() => {
setSubmitConfirmOpen(true)
}, [])
useEffect(() => { useEffect(() => {
if (objectOperations.length) { if (objectOperations.length) {
setActiveOperation(objectOperations[0].category_id.toString()) setActiveOperation(objectOperations[0].category_id.toString())
@@ -1645,6 +1676,7 @@ const TopTools = (
handleSave, handleSave,
handleBackup, handleBackup,
commitTaskByActionKey, commitTaskByActionKey,
requestSubmitTaskConfirm,
})) }))
const renderEditIcon = useMemo(() => { const renderEditIcon = useMemo(() => {
@@ -1900,7 +1932,7 @@ const TopTools = (
disabled={isView} disabled={isView}
style={{ cursor: isView ? "default" : "pointer" }} style={{ cursor: isView ? "default" : "pointer" }}
onClick={() => { onClick={() => {
if (!isView) commitTaskByActionKey() if (!isView) requestSubmitTaskConfirm()
}}> }}>
</UnstyledButton> </UnstyledButton>
@@ -2432,7 +2464,7 @@ const TopTools = (
c={!showTaskList ? "gray" : "var(--mantine-color-text)"} c={!showTaskList ? "gray" : "var(--mantine-color-text)"}
style={{ width: "auto", display: "flex", gap: 4 }} style={{ width: "auto", display: "flex", gap: 4 }}
onClick={() => { onClick={() => {
setShowTaskList(!showTaskList) toggleRightPanel(showTaskList, setShowTaskList)
}}> }}>
<ClipboardList style={{ width: 20, height: 20 }} /> <ClipboardList style={{ width: 20, height: 20 }} />
<Text size="xs"></Text> <Text size="xs"></Text>
@@ -2452,7 +2484,7 @@ const TopTools = (
c={!showGroupList ? "gray" : "var(--mantine-color-text)"} c={!showGroupList ? "gray" : "var(--mantine-color-text)"}
style={{ width: "auto", display: "flex", gap: 4 }} style={{ width: "auto", display: "flex", gap: 4 }}
onClick={() => { onClick={() => {
setShowGroupList(!showGroupList) toggleRightPanel(showGroupList, setShowGroupList)
}}> }}>
<GroupIcon style={{ width: 20, height: 20 }} /> <GroupIcon style={{ width: 20, height: 20 }} />
<Text size="xs"></Text> <Text size="xs"></Text>
@@ -2463,7 +2495,7 @@ const TopTools = (
c={!showDescList ? "gray" : "var(--mantine-color-text)"} c={!showDescList ? "gray" : "var(--mantine-color-text)"}
style={{ width: "auto", display: "flex", gap: 4 }} style={{ width: "auto", display: "flex", gap: 4 }}
onClick={() => { onClick={() => {
setShowDescList(!showDescList) toggleRightPanel(showDescList, setShowDescList)
}}> }}>
<BadgeInfoIcon style={{ width: 20, height: 20 }} /> <BadgeInfoIcon style={{ width: 20, height: 20 }} />
<Text size="xs"></Text> <Text size="xs"></Text>
@@ -2474,7 +2506,7 @@ const TopTools = (
c={!showObjectList ? "gray" : "var(--mantine-color-text)"} c={!showObjectList ? "gray" : "var(--mantine-color-text)"}
style={{ width: "auto", display: "flex", gap: 4 }} style={{ width: "auto", display: "flex", gap: 4 }}
onClick={() => { onClick={() => {
setShowObjectList(!showObjectList) toggleRightPanel(showObjectList, setShowObjectList)
}}> }}>
<Component style={{ width: 20, height: 20 }} /> <Component style={{ width: 20, height: 20 }} />
<Text size="xs"></Text> <Text size="xs"></Text>
@@ -2528,6 +2560,20 @@ const TopTools = (
handleOk={handleBackup} handleOk={handleBackup}
/> />
)} )}
{submitConfirmOpen && (
<ConfirmModal
open={submitConfirmOpen}
title="提交任务"
content="是否确定提交当前任务?"
onOk={async () => {
setSubmitConfirmOpen(false)
await commitTaskByActionKey()
}}
onCancel={() => {
setSubmitConfirmOpen(false)
}}
/>
)}
{duplicateConfirmOpen && ( {duplicateConfirmOpen && (
<ConfirmModal <ConfirmModal
open={duplicateConfirmOpen} open={duplicateConfirmOpen}

View File

@@ -0,0 +1,56 @@
const parseEnvInt = (
value: string | undefined,
fallback: number,
min: number,
max: number
) => {
if (!value) return fallback
const parsed = Number.parseInt(value, 10)
if (!Number.isFinite(parsed)) return fallback
return Math.min(max, Math.max(min, parsed))
}
export const labelimagePerformanceConfig = {
// 单次渲染的路径切片大小。越小越丝滑,越大越快完成。
polygonRenderBatchSize: parseEnvInt(
process.env.NEXT_PUBLIC_LABELIMAGE_POLYGON_BATCH_SIZE,
12,
1,
200
),
// 每帧渲染预算(毫秒)。越小越不卡操作,越大越快完成绘制。
polygonRenderBatchBudgetMs: parseEnvInt(
process.env.NEXT_PUBLIC_LABELIMAGE_POLYGON_BATCH_BUDGET_MS,
6,
1,
33
),
// 切帧准备超过该阈值后才显示轻量提示,避免短切换频繁闪动提示。
frameSwitchLoadingDelayMs: parseEnvInt(
process.env.NEXT_PUBLIC_LABELIMAGE_FRAME_SWITCH_LOADING_DELAY_MS,
120,
0,
1000
),
// 切帧时新旧图层的交叉淡入时长(毫秒)。设为 0 可关闭淡入。
rasterCrossfadeDurationMs: parseEnvInt(
process.env.NEXT_PUBLIC_LABELIMAGE_RASTER_CROSSFADE_MS,
90,
0,
400
),
// 容器尺寸连续变化时,延迟提交 canvas 实际宽高,减少频繁重置导致的白闪。
resizeCommitDebounceMs: parseEnvInt(
process.env.NEXT_PUBLIC_LABELIMAGE_RESIZE_COMMIT_DEBOUNCE_MS,
90,
0,
500
),
// 容器尺寸变更后的视觉缓动时长(毫秒),用于降低“跳变感”。
resizeInterpolationDurationMs: parseEnvInt(
process.env.NEXT_PUBLIC_LABELIMAGE_RESIZE_INTERPOLATION_MS,
100,
0,
400
),
} as const

View File

@@ -13,6 +13,12 @@ interface KeyFrameData {
interface BottomToolsState { interface BottomToolsState {
activeImage: string activeImage: string
setActiveImage: (val: string) => void setActiveImage: (val: string) => void
pendingImage: string
frameSwitchStatus: "idle" | "preparing"
frameSwitchToken: number
requestFrameSwitch: (val: string) => number
commitFrameSwitch: (val: string, token: number) => void
clearPendingFrameSwitch: (token?: number) => void
inputNumber: string inputNumber: string
setInputNumber: (val: string) => void setInputNumber: (val: string) => void
activeImageId: number activeImageId: number
@@ -36,13 +42,48 @@ const initialBottomToolsState = {
inputNumber: "", inputNumber: "",
} }
export const useBottomToolsStore = create<BottomToolsState>((set) => ({ export const useBottomToolsStore = create<BottomToolsState>()((set, get) => ({
activeImage: initialBottomToolsState.activeImage, activeImage: initialBottomToolsState.activeImage,
setActiveImage: (val: string) => setActiveImage: (val: string) =>
set((state: BottomToolsState) => ({ set((state: BottomToolsState) => ({
...state, ...state,
activeImage: val, activeImage: val,
pendingImage: "",
frameSwitchStatus: "idle",
})), })),
pendingImage: "",
frameSwitchStatus: "idle",
frameSwitchToken: 0,
requestFrameSwitch: (val: string): number => {
const nextToken = get().frameSwitchToken + 1
set((state: BottomToolsState) => ({
...state,
pendingImage: val,
frameSwitchStatus:
val && val !== state.activeImage ? "preparing" : "idle",
frameSwitchToken: nextToken,
}))
return nextToken
},
commitFrameSwitch: (val: string, token: number) =>
set((state: BottomToolsState) => {
if (state.frameSwitchToken !== token) return state
return {
...state,
activeImage: val,
pendingImage: "",
frameSwitchStatus: "idle",
}
}),
clearPendingFrameSwitch: (token?: number) =>
set((state: BottomToolsState) => {
if (token != null && state.frameSwitchToken !== token) return state
return {
...state,
pendingImage: "",
frameSwitchStatus: "idle",
}
}),
inputNumber: initialBottomToolsState.inputNumber, inputNumber: initialBottomToolsState.inputNumber,
setInputNumber: (val: string) => setInputNumber: (val: string) =>
set((state: BottomToolsState) => ({ set((state: BottomToolsState) => ({
@@ -63,7 +104,7 @@ export const useBottomToolsStore = create<BottomToolsState>((set) => ({
})), })),
keyFrameData: new Map(), keyFrameData: new Map(),
setKeyFrameData: (imageId, value) => { setKeyFrameData: (imageId, value) => {
const data = useBottomToolsStore.getState().keyFrameData const data = get().keyFrameData
data.set(imageId, value) data.set(imageId, value)
return set((state) => ({ return set((state) => ({
...state, ...state,
@@ -78,15 +119,13 @@ export const useBottomToolsStore = create<BottomToolsState>((set) => ({
})), })),
selectedItems: [], selectedItems: [],
setSelectedItems: (id, ctrlKey, shiftKey) => { setSelectedItems: (id, ctrlKey, shiftKey) => {
let arr = [...useBottomToolsStore.getState().selectedItems] let arr = [...get().selectedItems]
if ((ctrlKey && shiftKey) || (!ctrlKey && !shiftKey)) arr = [] if ((ctrlKey && shiftKey) || (!ctrlKey && !shiftKey)) arr = []
else if (ctrlKey && !shiftKey) { else if (ctrlKey && !shiftKey) {
arr.push(id) arr.push(id)
} else if (!ctrlKey && shiftKey) { } else if (!ctrlKey && shiftKey) {
const allItems = useBottomToolsStore.getState().allItems const allItems = get().allItems
const prevId = arr.length const prevId = arr.length ? arr[arr.length - 1] : get().activeImage
? arr[arr.length - 1]
: useBottomToolsStore.getState().activeImage
const prevIndex = allItems.findIndex((v) => v === prevId) const prevIndex = allItems.findIndex((v) => v === prevId)
const currentIndex = allItems.findIndex((v) => v === id) const currentIndex = allItems.findIndex((v) => v === id)
arr = arr =

View File

@@ -50,6 +50,7 @@ interface PaperState {
initScope: paper.PaperScope, initScope: paper.PaperScope,
initGroup: paper.Group initGroup: paper.Group
) => void ) => void
setGroup: (group: paper.Group | null) => void
setRasterPath: (paperRaster: paper.Path) => void setRasterPath: (paperRaster: paper.Path) => void
setRasterScale: (id: string, scale: number) => void setRasterScale: (id: string, scale: number) => void
setRasterSize: (id: string, size: [number, number]) => void setRasterSize: (id: string, size: [number, number]) => void
@@ -180,6 +181,8 @@ interface PaperState {
getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath> getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath>
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
setLoadingData: (flag: boolean) => void setLoadingData: (flag: boolean) => void
resizeGhostRequestToken: number
requestResizeGhost: () => void
} }
interface PositionStore { interface PositionStore {
@@ -208,6 +211,7 @@ const initialPaperState = {
rasterScale: {}, rasterScale: {},
rasterSize: {}, rasterSize: {},
reciprocalRasterScale: {}, reciprocalRasterScale: {},
resizeGhostRequestToken: 0,
} }
const handleDelete = () => { const handleDelete = () => {
const group = usePaperStore.getState().group! const group = usePaperStore.getState().group!
@@ -529,6 +533,7 @@ export const usePaperStore = create<PaperState>((set) => ({
rasterScale: initialPaperState.rasterScale, rasterScale: initialPaperState.rasterScale,
rasterSize: initialPaperState.rasterSize, rasterSize: initialPaperState.rasterSize,
reciprocalRasterScale: initialPaperState.reciprocalRasterScale, reciprocalRasterScale: initialPaperState.reciprocalRasterScale,
resizeGhostRequestToken: initialPaperState.resizeGhostRequestToken,
continuePolygonTarget: null, continuePolygonTarget: null,
currentMousePosition: [0, 0], currentMousePosition: [0, 0],
loadingData: false, loadingData: false,
@@ -544,6 +549,12 @@ export const usePaperStore = create<PaperState>((set) => ({
el: initEl, el: initEl,
})) }))
}, },
setGroup: (group) => {
set((state: PaperState) => ({
...state,
group,
}))
},
setRasterPath: (rasterPath) => { setRasterPath: (rasterPath) => {
set((state: PaperState) => ({ set((state: PaperState) => ({
...state, ...state,
@@ -577,6 +588,11 @@ export const usePaperStore = create<PaperState>((set) => ({
...state, ...state,
loadingData: flag, loadingData: flag,
})), })),
requestResizeGhost: () =>
set((state: PaperState) => ({
...state,
resizeGhostRequestToken: state.resizeGhostRequestToken + 1,
})),
setContinuePolygonTarget: (target) => setContinuePolygonTarget: (target) =>
set((state: PaperState) => ({ set((state: PaperState) => ({
...state, ...state,
@@ -5328,6 +5344,15 @@ export const usePaperStore = create<PaperState>((set) => ({
}) as paper.Item[] }) as paper.Item[]
tagItems.forEach((item) => { tagItems.forEach((item) => {
if (item.data?.tag_category === "mask") return if (item.data?.tag_category === "mask") return
const anchor = item.data?.tag_anchor
if (Array.isArray(anchor) && anchor.length >= 2) {
const anchorX = Number(anchor[0])
const anchorY = Number(anchor[1])
if (Number.isFinite(anchorX) && Number.isFinite(anchorY)) {
item.scale(1 / newScale, new paper.Point(anchorX, anchorY))
return
}
}
item.scale(1 / newScale) item.scale(1 / newScale)
}) })
} }

View File

@@ -19,8 +19,20 @@ const useRenderTag = () => {
const dataMap = useLabelStore.getState().label const dataMap = useLabelStore.getState().label
const activeImage = useBottomToolsStore.getState().activeImage const activeImage = useBottomToolsStore.getState().activeImage
const group = usePaperStore.getState().group const group = usePaperStore.getState().group
if (!group) return
const visible = useTopToolsStore.getState().showTags const visible = useTopToolsStore.getState().showTags
const configs = useTopToolsStore.getState().showTagsConfigs const configs = useTopToolsStore.getState().showTagsConfigs
const groupScale =
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
? group.scaling.x
: 1
const applyTagCompensation = (
item: paper.Item,
anchor: [number, number]
) => {
if (Math.abs(groupScale - 1) < 0.000001) return
item.scale(1 / groupScale, new paper.Point(anchor[0], anchor[1]))
}
// 清空之前渲染的标签 // 清空之前渲染的标签
removeTagTexts() removeTagTexts()
@@ -38,7 +50,8 @@ const useRenderTag = () => {
const path = usePaperStore.getState().getItemById(id) const path = usePaperStore.getState().getItemById(id)
if (path) { if (path) {
const bounds = path.bounds const bounds = path.bounds
let pos = [bounds.left, bounds.top - 6] const tagAnchor: [number, number] = [bounds.left, bounds.top - 6]
const pos = [...tagAnchor]
let pathData = path.data let pathData = path.data
let content = `` let content = ``
if (configs.includes("ID")) if (configs.includes("ID"))
@@ -68,10 +81,12 @@ const useRenderTag = () => {
id, id,
type: "tag", type: "tag",
tag_category: "text", tag_category: "text",
tag_anchor: tagAnchor,
}, },
parent: group, parent: group,
visible, visible,
}) })
applyTagCompensation(text, tagAnchor)
// 文本框 // 文本框
const textMask = new paper.Path.Rectangle(text.bounds) const textMask = new paper.Path.Rectangle(text.bounds)
@@ -84,10 +99,12 @@ const useRenderTag = () => {
id, id,
type: "tag", type: "tag",
tag_category: "text_mask", tag_category: "text_mask",
tag_anchor: tagAnchor,
}, },
parent: group, parent: group,
visible, visible,
}) })
applyTagCompensation(textMask, tagAnchor)
// 标注对象外接框 // 标注对象外接框
const mask = new paper.Path.Rectangle(bounds) const mask = new paper.Path.Rectangle(bounds)
mask.set({ mask.set({
@@ -108,7 +125,7 @@ const useRenderTag = () => {
path.children.forEach((child) => { path.children.forEach((child) => {
const segments = (child as paper.Path).segments const segments = (child as paper.Path).segments
segments.forEach((segment) => { segments.forEach((segment) => {
let pointTextPosition = [ const pointTextPosition: [number, number] = [
segment.point.x - 10, segment.point.x - 10,
segment.point.y + 5, segment.point.y + 5,
] ]
@@ -122,17 +139,19 @@ const useRenderTag = () => {
id, id,
type: "tag", type: "tag",
tag_category: "point_text", tag_category: "point_text",
tag_anchor: pointTextPosition,
}, },
parent: group, parent: group,
visible: visible && configs.includes("点ID"), visible: visible && configs.includes("点ID"),
}) })
applyTagCompensation(pointText, pointTextPosition)
index++ index++
}) })
}) })
} else { } else {
const segments = (path as paper.Path).segments const segments = (path as paper.Path).segments
segments.forEach((segment, index) => { segments.forEach((segment, index) => {
let pointTextPosition = [ const pointTextPosition: [number, number] = [
segment.point.x - 10, segment.point.x - 10,
segment.point.y + 5, segment.point.y + 5,
] ]
@@ -146,10 +165,12 @@ const useRenderTag = () => {
id, id,
type: "tag", type: "tag",
tag_category: "point_text", tag_category: "point_text",
tag_anchor: pointTextPosition,
}, },
parent: group, parent: group,
visible: visible && configs.includes("点ID"), visible: visible && configs.includes("点ID"),
}) })
applyTagCompensation(pointText, pointTextPosition)
}) })
} }
} }

View File

@@ -11,20 +11,14 @@ import {
ScrollArea, ScrollArea,
Space, Space,
Stack, Stack,
Text,
Tooltip, Tooltip,
UnstyledButton, UnstyledButton,
useComputedColorScheme, useComputedColorScheme,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core" } from "@mantine/core"
import { useDisclosure, useMediaQuery } from "@mantine/hooks" import { useDisclosure, useMediaQuery } from "@mantine/hooks"
import { import { IconLogout, IconMoon, IconSun, IconUser } from "@tabler/icons-react"
IconLogout,
IconMessageCircle,
IconMoon,
IconSettings,
IconSun,
IconUser,
} from "@tabler/icons-react"
import cx from "clsx" import cx from "clsx"
import { Suspense, useEffect, useMemo, useState } from "react" import { Suspense, useEffect, useMemo, useState } from "react"
import actionToggleClasses from "./ActionToggle.module.css" import actionToggleClasses from "./ActionToggle.module.css"
@@ -32,6 +26,7 @@ import headerLinkClasses from "./HeaderLink.module.css"
import { useUserStore } from "@/app/store/user" import { useUserStore } from "@/app/store/user"
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { usePermissionStore } from "../label/store/auth"
import { deleteCookie } from "../login/api" import { deleteCookie } from "../login/api"
import { MenuItem, withoutLayoutRoutes } from "./common" import { MenuItem, withoutLayoutRoutes } from "./common"
import { ClientIcon } from "./components/ClientIcon" import { ClientIcon } from "./components/ClientIcon"
@@ -147,6 +142,7 @@ export default function AppLayout({
window.location.reload() window.location.reload()
} }
const matches = useMediaQuery("(min-width: 48em)") const matches = useMediaQuery("(min-width: 48em)")
const userName = usePermissionStore((s) => s.user_name)
return isClient ? ( return isClient ? (
withoutLayout ? ( withoutLayout ? (
@@ -199,7 +195,7 @@ export default function AppLayout({
/> />
</UnstyledButton> </UnstyledButton>
<Divider orientation="vertical" mx={"0.5rem"} display="none" /> <Divider orientation="vertical" mx={"0.5rem"} display="none" />
<Menu shadow="md" width={200}> <Menu shadow="md" width={220}>
<Menu.Target> <Menu.Target>
<UnstyledButton> <UnstyledButton>
<IconUser <IconUser
@@ -210,13 +206,12 @@ export default function AppLayout({
</UnstyledButton> </UnstyledButton>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Label></Menu.Label> <Menu.Label></Menu.Label>
<Menu.Item leftSection={<IconSettings size={14} />}> <Box px="sm" py={4}>
<Text size="sm" fw={500} style={{ wordBreak: "break-all" }}>
</Menu.Item> {userName || "-"}
<Menu.Item leftSection={<IconMessageCircle size={14} />}> </Text>
</Box>
</Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Label></Menu.Label> <Menu.Label></Menu.Label>
<Menu.Item <Menu.Item

View File

@@ -4,7 +4,7 @@ import { Login } from "./types"
const BASE_PATH = const BASE_PATH =
process.env.NEXT_PUBLIC_ENV === "production" process.env.NEXT_PUBLIC_ENV === "production"
? "http://172.16.104.95:9110" ? "http://172.16.103.224:9110"
: process.env.NEXT_PUBLIC_ENV === "staging" : process.env.NEXT_PUBLIC_ENV === "staging"
? "http://172.16.115.128:9110" ? "http://172.16.115.128:9110"
: "https://label.softtest.cowarobot.com" : "https://label.softtest.cowarobot.com"