"use client" import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react" import ScaleComponent from "./components/ScaleComponent" import { processVideo } from "@/app/component/label/video2image/processVideo" import { Box, Flex, LoadingOverlay, Stack } from "@mantine/core" import { showNotification } from "@mantine/notifications" import { getLabelResult, getServerImage } from "./api/label" import { Comment, ImageObjects, LabelResult, WorkLoad, } from "./api/label/typing" import { getProjectDetail } from "./api/project" import { Project } from "./api/project/typing" import { getTaskList, getTaskWorkFlow } from "./api/task" import { Task } from "./api/task/typing" import BottomTools from "./components/BottomTools" import ConfirmModal from "./components/ConfirmModal" import { splitWord } from "./components/EditorContainer" import PaperContainer from "./components/PaperContainer" import RightDescTools from "./components/RightDescTools" import RightGroupTools from "./components/RightGroupTools" import RightObjectTools from "./components/RightObjectTools" import RightQATools from "./components/RightQATools" import RightTaskTools from "./components/RightTaskTools" import ScaleToolContainer from "./components/ScaleToolContainer" import TopTools from "./components/TopTools" import { useImagesStore, useKeyEventStore, useLabelStore, useObjectStore, useVideoFrameStore, } from "./store" import { usePermissionStore } from "./store/auth" import { useBottomToolsStore } from "./useBottomToolsStore" import { useDescToolsStore } from "./useDescToolsStore" import { useKeyboardStore } from "./useKeyBoardStore" import { usePaperStore } from "./usePaperStore" import { useRightToolsStore } from "./useRightToolsStore" import { useIntervalStore, useTimerStore } from "./useTimerStore" import { useTopToolsStore } from "./useTopToolsStore" import { findGroupKey } from "./util" import { labelTypeMap } from "./utils/constants" // Splitter component - will need custom implementation or alternative // 定义状态的类型 export type LabelState = Map // 定义操作的类型 type Action = | { type: "INIT_IMAGES" imageIds: string[] objectOperations: Project.LabelSchemaList[] } | { type: "REMOVE_IMAGE"; imageId: string } // 定义初始状态 const initialState: LabelState = new Map() const operationTypeList = [101, 102, 103, 104] // utils const getRectPoints = ([sx, sy, w, h]: number[]) => { return [ [sx, sy + h], [sx, sy], [sx + w, sy], [sx + w, sy + h], ] } const getSegmentPoints = (arr: Array) => { return arr.map((item) => [item[0], item[1]]) } const videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i const normalizeNamePrefix = (name: string) => { const value = name .replace(/[^a-zA-Z0-9_-]+/g, "_") .replace(/_+/g, "_") .replace(/^_|_$/g, "") return value || "video" } interface ResultImageEntry { frameName: string imageObjects: ImageObjects } const expandLabelResultImages = (item: LabelResult): ResultImageEntry[] => { if (item.image_objects) { return [ { frameName: item.data_name, imageObjects: item.image_objects, }, ] } if (!item.video_objects) return [] return Object.entries(item.video_objects).map(([frameName, imageObjects]) => { return { frameName, imageObjects, } }) } interface LabelProps { headerHeight: number leftWidth: number project_id: number task_id: number } const LabelPage = ({ headerHeight = 0, leftWidth = 0, project_id = -1, task_id = -1, }: LabelProps) => { const projectId = useMemo(() => { return project_id }, [project_id]) const taskId = useMemo(() => { return task_id }, [task_id]) // const searchParams = useSearchParams() // const id1 = searchParams.get("project_id") // const projectId = // typeof id1 === "string" && !isNaN(Number(id1)) ? Number(id1) : 1 // const id2 = searchParams.get("task_id") // const taskId = // typeof id2 === "string" && !isNaN(Number(id2)) ? Number(id2) : 2 const user_id = usePermissionStore.getState().user_id const [projectDetail, setProjectDetail] = useState() const [taskDetail, setTaskDetail] = useState() const [oldWorkLoad, setOldWorkLoad] = useState(null) // const [objectOperations, setObjectOperations] = useState< // Project.LabelSchemaList[] // >([]); const topRef = useRef(null) const paperContainerRef = useRef(null) const qaToolContainerRef = useRef(null) const [isConfirmSave, setIsConfirmSave] = useState(false) const [loading, setLoading] = useState(false) const [sizes, setSizes] = useState([]) const { label: labelData, setLabel, setStateStack, setLabelTime, } = useLabelStore() const { isAutoSave, autoSaveGap } = useIntervalStore() const { editMode, setEditMode, setPressA, activeOperation, setActiveOperation, showObjectList, showGroupList, showTaskList, showDescList, showMultiFrame, setShowMultiFrame, isView, setIsView, scale, setScale, objectOperations, setObjectOperations, } = useTopToolsStore() const { activeImage } = useBottomToolsStore() const { setDescOperations, metaOperation, setMetaOperation, setQaOperations, setQaData, resetData, } = useDescToolsStore() const asyncGetProjectDetail = useCallback(async () => { if (projectId) { try { const res = await getProjectDetail(projectId) setProjectDetail(res) let operations: Project.LabelSchemaList[] = [] let textOperations: Project.LabelSchemaList[] = [] let metaOperation: Project.LabelSchemaList | null = null res.label_schema_list?.forEach((schema) => { if (operationTypeList.includes(schema.label_type)) { operations.push(schema) } else if ([201].includes(schema.label_type)) { if (schema.label_class === "元操作序列") metaOperation = schema else textOperations.push(schema) } }) setObjectOperations(operations) if (res.label_type === 5) { setDescOperations(textOperations) if (metaOperation) setMetaOperation(metaOperation) } else { setDescOperations([]) setMetaOperation(null) } if (res.label_type === 6) { let obj: any = {} textOperations.forEach((item) => { let arr = item.sub_attributes_describe.map((sub) => { return { [sub.chinese_name]: { value: "", id: sub.round_id, is_pre: true, tag: 0, uid: 0, create_timestamp: 0, modify_uid: 0, modify_timestamp: 0, comment: "", flag: 0, }, } }) obj[item.label_class] = arr }) let map = new Map() map.set("init", obj) console.log(map) setQaData(map) setQaOperations(textOperations) } else { setQaOperations([]) } resetData() } catch {} } }, [ projectId, resetData, setDescOperations, setMetaOperation, setObjectOperations, setQaData, setQaOperations, ]) const normalizeVideoTaskData = useCallback( async (detail: Task.DataProps) => { const sourceNames = detail.data_name .map((item) => item?.name?.[0] || "") .filter((name) => !!name) if (!sourceNames.length || !projectId || !taskId) return detail const taskDataType = Number( (detail as { [key: string]: any })?.data_type ?? (detail as { [key: string]: any })?.task_data_type ?? -1 ) const isVideoTask = taskDataType === 2 || taskDataType === 7 || sourceNames.some((name) => videoExtPattern.test(name)) if (!isVideoTask) return detail const imagesMap = new Map(useImagesStore.getState().images) const frameMapUpdates = new Map() let imageMapUpdated = false const normalizedDataName: Task.DataProps["data_name"] = [] for (const videoName of sourceNames) { const cacheKey = `${projectId}:${taskId}:${videoName}` let frameNames = useVideoFrameStore.getState().videoFrames.get(cacheKey) ?? [] const hasAllCachedFrames = frameNames.length > 0 && frameNames.every((frameName) => { const cache = imagesMap.get(frameName) return typeof cache === "string" && cache.length > 0 }) if (!hasAllCachedFrames) { let frameData: Awaited> = [] try { const base64Video = await getServerImage({ data_names: [videoName], data_type: 2, project_id: projectId, }) frameData = await processVideo({ base64Data: String(base64Video || ""), fileName: videoName, namePrefix: normalizeNamePrefix( `${projectId}_${taskId}_${videoName.replace(/\.[^.]+$/, "")}` ), }) } catch (error) { const message = error instanceof Error ? error.message : "未知错误" throw new Error(`视频帧生成失败:${videoName}(${message})`) } frameNames = frameData.map((frame) => frame.name) if (!frameNames.length) { throw new Error(`视频帧生成失败:${videoName}`) } frameData.forEach((frame) => { imagesMap.set(frame.name, frame.dataUrl) }) imageMapUpdated = true frameMapUpdates.set(cacheKey, frameNames) } if (!frameNames.length) { throw new Error(`视频帧映射为空:${videoName}`) } frameNames.forEach((frameName) => { normalizedDataName.push({ name: [frameName] as [string], related_images: [], }) }) } if (!normalizedDataName.length) { throw new Error("未生成可用视频帧") } if (imageMapUpdated) { useImagesStore.getState().setImages(imagesMap) } frameMapUpdates.forEach((frameNames, cacheKey) => { useVideoFrameStore.getState().setVideoFrames(cacheKey, frameNames) }) return { ...detail, data_name: normalizedDataName, } }, [projectId, taskId] ) const asyncGetTaskDetail = useCallback(async () => { if (taskId) { try { usePaperStore.getState().setLoadingData(true) // 获取当前任务全量数据 const res = await getTaskList({ id: [taskId], get_data: true, page_number: 1, page_size: 1, }) let detail = res.task_list[0] detail = await normalizeVideoTaskData(detail) setTaskDetail(detail) // 帧列表 useBottomToolsStore .getState() .setAllItems(detail.data_name.map((item) => item.name[0] || "")) // 获取当前任务流转记录 let flowCommentArr: Comment[] = [] let turn1time = 1 let turn2time = 1 const flowRes = await getTaskWorkFlow(taskId) flowRes.forEach((item) => { if (item.task_status_src !== 4 && item.task_status_dst === 4) { flowCommentArr.push({ uid: item.new_uid, turn: 1, times: turn1time, check_box_comments: [], text_comments: [], comment_type: 0, date: 0, scene_info: { type: 0, }, }) turn1time++ } else if (item.task_status_src !== 6 && item.task_status_dst === 6) { flowCommentArr.push({ uid: item.new_uid, turn: 2, times: turn2time, check_box_comments: [], text_comments: [], comment_type: 0, date: 0, scene_info: { type: 0, }, }) turn2time++ } }) console.log(flowCommentArr) useLabelStore.getState().setLabelDefaultComments(flowCommentArr) const { work_time, label_status } = detail setLabelTime({ label: work_time.label_work_time, review1: work_time.first_review_work_time, review2: work_time.second_review_work_time, }) // 获取并处理当前任务标注结果数据 const dataRes = await getLabelResult(taskId) const { results } = dataRes const resultImageEntries = results.flatMap((item) => expandLabelResultImages(item) ) let currentWorkTime = 0 if (label_status === 2) { currentWorkTime = work_time.label_work_time } else if (label_status === 4) { currentWorkTime = work_time.first_review_work_time } else if (label_status === 6) { currentWorkTime = work_time.second_review_work_time } // 生成当前workload const workLoad: WorkLoad = { object_size: {}, prelabel_modify_size: {}, review_size: 0, comment_size: 0, work_time: currentWorkTime, dynamic_size: {}, static_size: {}, review_dynamic_size: {}, review_static_size: {}, review_question_size: {}, key_frame_size: {}, review_key_frame_size: {}, question_size: {}, } resultImageEntries.forEach(({ frameName, imageObjects }) => { const { annotations, images, key_frame, desc } = imageObjects const rasterName = frameName || images.file_name if (rasterName) { usePaperStore .getState() .setRasterSize(rasterName, [images.width, images.height]) } annotations.forEach((annotation) => { let reviewed = false const { comment, prelabel, uid } = annotation if (prelabel) workLoad.prelabel_modify_size[uid] ? workLoad.prelabel_modify_size[uid]++ : (workLoad.prelabel_modify_size[uid] = 1) else workLoad.object_size[uid] ? workLoad.object_size[uid]++ : (workLoad.object_size[uid] = 1) if (comment) { comment.forEach((c) => { if (c.comment_type > 0 && c.uid === user_id) reviewed = true if (c.comment_type === 2 && c.uid === user_id) workLoad.comment_size++ }) } if (reviewed) workLoad.review_size++ }) if (key_frame) { if (label_status === 2) { let id = imageObjects.label1_uid! if (id) workLoad.key_frame_size[id] ? workLoad.key_frame_size[id]++ : (workLoad.key_frame_size[id] = 1) } else if (label_status === 4) { let id = imageObjects.review1_uid! if (id) workLoad.review_key_frame_size[id] ? workLoad.review_key_frame_size[id]++ : (workLoad.review_key_frame_size[id] = 1) } else if (label_status === 6) { let id = imageObjects.review2_uid! if (id) workLoad.review_key_frame_size[id] ? workLoad.review_key_frame_size[id]++ : (workLoad.review_key_frame_size[id] = 1) } } if (useDescToolsStore.getState().qaOperations.length) { if (desc) { const { other_desc } = desc const descData = other_desc ? JSON.parse(other_desc) : {} Object.values(descData).forEach((questionArr: any) => { questionArr.forEach((q: any) => { const v: any = Object.values(q)[0] if (v.uid) { workLoad.question_size[v.uid] ? workLoad.question_size[v.uid]++ : (workLoad.question_size[v.uid] = 1) } if (v.tag) { const uid = usePermissionStore.getState().user_id workLoad.review_question_size[uid] ? workLoad.review_question_size[uid]++ : (workLoad.review_question_size[uid] = 1) } }) }) } } }) console.log("获取时计算的工作量统计", workLoad) setOldWorkLoad(workLoad) const taskLabelData = new Map() const pathGroupData = new Map() // 重置 PathIds useRightToolsStore.getState().setPathIds([]) // 重置 PathGroupIds useRightToolsStore.getState().setPathGroupIds([]) // 生成标注结果 resultImageEntries.forEach(({ frameName, imageObjects }) => { const key = frameName if (!key) return const categoryMap = new Map() const { annotations, image_groups, desc } = imageObjects // 赋值用过的 id annotations.map((annotation) => { useRightToolsStore.getState().pushPathId(annotation.id) }) // 赋值用过的 groupId image_groups && Object.keys(image_groups)?.map((groupId) => { useRightToolsStore.getState().pushPathGroupId(+groupId) }) annotations.forEach((annotation) => { const { id, category_id, comment } = annotation let current_comment: Comment[] = [] if (comment) { current_comment = flowCommentArr.map((c) => { const { turn, times } = c let modified_comment = null comment.forEach((item) => { if (item.turn === turn && item.times === times) modified_comment = item }) return modified_comment ? modified_comment : c }) } else { current_comment = [...flowCommentArr] } let detail: any = { ...annotation, comment: current_comment, } // group info if (image_groups) { const key = findGroupKey(image_groups as any, id) if (key !== -1) detail.parentGroupId = key } const categoryKey = `${category_id}` let arr = categoryMap.has(categoryKey) ? categoryMap.get(categoryKey) : [] if (annotation.rect) { // arr.push([id, getRectPoints(annotation.rect), detail, []]); arr.push([id, [getRectPoints(annotation.rect)], detail, []]) } if (annotation.line_segment) { arr.push([ id, annotation.line_segment.map((item) => getSegmentPoints(item)), detail, [], ]) } if (annotation.segmentation) { arr.push([ id, // getSegmentPoints(annotation.segmentation[0]), annotation.segmentation.map((item) => getSegmentPoints(item)), detail, annotation.hollow_segmentation?.map((item) => getSegmentPoints(item) ) || [], ]) } if (annotation.key_points) { arr.push([ id, // getSegmentPoints(annotation.segmentation[0]), [ getSegmentPoints( annotation.key_points.map((item) => item.point) ), ], Object.assign(detail, { circles: annotation.key_points, }), [], ]) } categoryMap.set(categoryKey, arr) }) taskLabelData.set(key, categoryMap) // 关键帧信息 const keyFrame = { key_frame: imageObjects.key_frame || false, label1_ts: imageObjects.label1_ts || 0, label1_uid: imageObjects.label1_uid || 0, review1_ts: imageObjects.review1_ts || 0, review1_uid: imageObjects.review1_uid || 0, review2_ts: imageObjects.review2_ts || 0, review2_uid: imageObjects.review2_uid || 0, } useBottomToolsStore.getState().setKeyFrameData(key, keyFrame) // 大语言模型 两种数据情况 if (useDescToolsStore.getState().qaOperations.length) { if (desc) { const { other_desc } = desc let current_data: any = {} const descData = other_desc ? JSON.parse(other_desc) : {} Object.entries(descData).forEach( ([label_class, questionArr]: any) => { let arr = questionArr.map((question: any) => { let [name, val]: any = Object.entries(question)[0] return { [name]: { ...val, value: val.value ? `${val.value}${splitWord}${val.value}` : undefined, comment: val.comment ? `${val.comment}${splitWord}${val.comment}` : undefined, }, } }) current_data[label_class] = arr } ) const qaData = useDescToolsStore.getState().qaData // 无文本描述时 赋予预设值 if (JSON.stringify(current_data) === "{}") { current_data = qaData.get("init") } const qaMap = structuredClone(qaData) qaMap.set(key, current_data) useDescToolsStore.getState().setQaData(qaMap) } } else if (useDescToolsStore.getState().descOperations.length) { if (desc) { const { meta_operation, other_desc } = desc const currentMetaMap = structuredClone( useDescToolsStore.getState().metaData ) currentMetaMap.set(key, meta_operation || []) useDescToolsStore.getState().setMetaData(currentMetaMap) const currentDescMap = structuredClone( useDescToolsStore.getState().descData ) let descMap = new Map() const descData = other_desc ? JSON.parse(other_desc) : {} console.log(descData) Object.entries(descData).forEach( ([label_class, { value, is_correct, comment }]: any) => { descMap.set(label_class, { value: (typeof value === "string" ? `${value}${splitWord}${value}` : value) || undefined, is_correct: is_correct || undefined, comment: comment || undefined, }) } ) currentDescMap.set(key, descMap) useDescToolsStore.getState().setDescData(currentDescMap) } } image_groups && pathGroupData.set( key, new Map( Object.entries(image_groups).map(([key, value]: any) => [ key * 1, value, ]) ) ) }) setLabel(taskLabelData) // 获取任务详情时设置状态栈初始值 setStateStack([taskLabelData]) useDescToolsStore.getState().updateFlag(true) useRightToolsStore.getState().setPathGroupMap(pathGroupData) } catch (err) { console.log(err) setOldWorkLoad(null) setTaskDetail(undefined) useBottomToolsStore.getState().setAllItems([]) setLabel(new Map()) setStateStack([]) useRightToolsStore.getState().setPathGroupMap(new Map()) if (err instanceof Error && err.message.includes("视频")) { showNotification({ color: "red", title: "视频解码失败", message: err.message, }) } } finally { usePaperStore.getState().setLoadingData(false) } } }, [ normalizeVideoTaskData, setLabel, setLabelTime, setStateStack, taskId, user_id, ]) // 保存时更新当前workload const updateOldWorkLoad = (data: WorkLoad) => { setOldWorkLoad(data) } useEffect(() => { asyncGetProjectDetail() asyncGetTaskDetail() }, [asyncGetProjectDetail, asyncGetTaskDetail]) // 定义 reducer 函数 const reducer = useCallback( (state: LabelState, action: Action): LabelState => { switch (action.type) { case "INIT_IMAGES": const newState = new Map() // 创建状态的副本 action.imageIds.forEach((imageId) => { newState.set( imageId, action.objectOperations.map((item) => item.category_id.toString()) ) // 将操作列表添加到新状态中 }) return newState case "REMOVE_IMAGE": const stateAfterRemoval = new Map(state) if (stateAfterRemoval.has(action.imageId)) { stateAfterRemoval.delete(action.imageId) // 删除该图片的条目 } return stateAfterRemoval default: return state // 返回当前状态 } }, [] ) const [labelState, dispatch] = useReducer(reducer, initialState) const initImages = ( imageIds: string[], objectOperations: Project.LabelSchemaList[] ) => { dispatch({ type: "INIT_IMAGES", imageIds, objectOperations, }) } const [isLabelTask, setIsLabelTask] = useState(false) useEffect(() => { if (!taskDetail || !projectDetail) { setIsLabelTask(false) setIsView(true) setEditMode(false) return } if (taskDetail) { if ( ![201, 202, 203, 500].includes(projectDetail!.status) || ![2, 4, 6, 7].includes(taskDetail.label_status) || taskDetail.current_uid !== user_id ) { // 如果不是能标注的任务 setIsLabelTask(false) setIsView(true) setEditMode(false) } else { setIsLabelTask(true) setIsView(false) } initImages( taskDetail.data_name.map((item) => item.name?.[0]), objectOperations ) if (taskDetail.data_name?.length) setShowMultiFrame(true) } }, [ objectOperations, setIsView, setShowMultiFrame, user_id, taskDetail, projectDetail, setEditMode, ]) const { setPaperMode, setToolOption, setGroupScale } = usePaperStore() // @ts-expect-error to_do const forceUpdate = useReducer((bool) => !bool)[1] const getOperationSchema = useCallback( (operationId: string) => { return ( projectDetail?.label_schema_list?.find( (item) => item.category_id === +operationId && item.label_type !== 201 ) || null ) }, [projectDetail?.label_schema_list] ) const setDrawType = useCallback(() => { if (isView) { setPaperMode("pan") } else { let operationSchema = getOperationSchema(activeOperation) if (operationSchema) { let strokeColor = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})` let fillColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)` let blankColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)` if (editMode) { switch ( operationSchema && labelTypeMap.get(operationSchema.label_type) ) { case "多边形": setToolOption({ strokeColor: strokeColor, strokeWidth: 2, fillColor: fillColor, blankColor: blankColor, }) setPaperMode("polygon") return case "关键点": setToolOption({ strokeColor: strokeColor, strokeWidth: 2, fillColor: fillColor, blankColor: blankColor, }) setPaperMode("point") return case "多线段": setToolOption({ strokeColor: strokeColor, strokeWidth: 2, fillColor: fillColor, blankColor: blankColor, }) setPaperMode("brush") return case "2D框": setToolOption({ strokeColor: strokeColor, strokeWidth: 2, fillColor: fillColor, blankColor: blankColor, }) setPaperMode("rectangle") return default: setPaperMode("polygon") return } } else { setPaperMode("pan") } } } }, [ activeOperation, editMode, getOperationSchema, isView, setPaperMode, setToolOption, ]) useEffect(() => { setDrawType() }, [editMode, setDrawType]) const { setShift, focusInput } = useKeyEventStore() const handleShortcut = useCallback( (key: string, ctrl: boolean) => { let checkOperation = getOperationSchema(key) const mode = usePaperStore.getState().mode // 标注方案里面存在的非文本类型标注才能切换 if (checkOperation) { // 辅助标注模式下非多边形类型不能切换 if (mode === "support" && checkOperation.label_type !== 102) return if (ctrl) { setActiveOperation((+key + 10).toString()) } else { setActiveOperation(key) } } }, [getOperationSchema, setActiveOperation] ) useEffect(() => { const down = (e: KeyboardEvent) => { // console.log(e, e.key); if (focusInput || isView) return console.log(e.key) const mode = usePaperStore.getState().mode if (e.key === "Alt" && mode !== "support") { e.preventDefault() // console.log(e.key); setEditMode(!editMode) if (!editMode) setPressA(false) } if (e.key === "Shift") { e.preventDefault() setShift(true) useKeyboardStore.getState().setShift(true) } if (mode !== "support") if (Number(e.key)) { handleShortcut(e.key, e.ctrlKey) } else if (Number(e.key) === 0) { handleShortcut("10", e.ctrlKey) } if (e.key === "s" && (e.metaKey || e.ctrlKey)) { e.preventDefault() setIsConfirmSave(true) } if (e.key === "g" && (e.metaKey || e.ctrlKey)) { e.preventDefault() paperContainerRef.current.handleCreatePathGroup() } if (e.key === "a") { e.preventDefault() setPressA(true) } if (e.key === "Control") { e.preventDefault() useKeyboardStore.getState().setCtrl(true) } // 辅助标注 分割 if (e.key === "F1") { // 判断类型 e.preventDefault() const schema = getOperationSchema( useTopToolsStore.getState().activeOperation ) console.log(schema) if (schema?.label_type !== 102) { showNotification({ title: "错误", message: "当前类型不支持辅助标注!", color: "red", }) } else { if (mode === "support") { // 辅助标注生成对应标注结果 paperContainerRef.current.saveSupportAnnotationData() } else { usePaperStore.getState().setPaperMode("support") } } } // 追踪 if (e.key === "i") { e.preventDefault() paperContainerRef.current.renderTrackingAnnotation() } // 复制 if (e.key === "c" && e.ctrlKey) { e.preventDefault() useKeyboardStore .getState() .setCopyDataIds( useObjectStore.getState().selectedPath[ useBottomToolsStore.getState().activeImage ] ) } // 粘贴 if (e.key === "v" && e.ctrlKey) { e.preventDefault() paperContainerRef.current.copyAnnotationDataToMultiFrame() } // 撤回 if (e.key === "z" && e.ctrlKey) { e.preventDefault() // 清除当前页数据 const clear = () => { usePaperStore .getState() .group!.getItems({}) .filter((item) => item.data.id) .forEach((item) => { item.remove() }) } const data = useLabelStore.getState().popStateStack() console.log("弹出栈的数据", data) if (data) { clear() const imageId = useBottomToolsStore.getState().activeImage const pathGroupMap = new Map() for (const [imageKey, categories] of data) { const groupMap = new Map() for (const [key, value] of categories) { value.forEach((item) => { const vId = item[2].parentGroupId if (vId) if (groupMap.has(vId)) groupMap.get(vId).push(item[0]) else groupMap.set(vId, [item[0]]) }) if (imageId === imageKey) paperContainerRef.current.renderPolygons(key, categories) } pathGroupMap.set(imageKey, groupMap) } useRightToolsStore.getState().setPathGroupMap(pathGroupMap) console.log(pathGroupMap) // if (data.size) { // if (data.has(imageId)) { // const map = data.get(imageId); // if (map) { // for (const [key, value] of map) { // console.log(key, value); // paperContainerRef.current.renderPolygons(key, map); // } // } // } // } // 更新当前数据 useLabelStore.getState().setLabel(structuredClone(data)) } } if (e.key === "p") { e.preventDefault() if (projectDetail?.label_type === 6 && qaToolContainerRef.current) qaToolContainerRef.current.checkWrongWords() } // if (e.key === "=" && (e.metaKey || e.ctrlKey)) { // e.preventDefault(); // setScale((num) => { // let newScale = num + 0.1; // return newScale; // }); // } else if (e.key === "-" && (e.metaKey || e.ctrlKey)) { // e.preventDefault(); // setScale((num) => { // if (num <= 1) { // let newScale = num * 0.9; // return newScale; // } else { // let newScale = num - 0.1; // return newScale; // } // }); // } else if (e.key === "3") { // e.preventDefault(); // } else if (e.key === "4") { // e.preventDefault(); // } else if (e.key === "5") { // e.preventDefault(); // } else if (e.key === "a" && (e.metaKey || e.ctrlKey)) { // e.preventDefault(); // } } const up = (e: KeyboardEvent) => { // console.log("键盘抬起", e); if (e.key === "Shift") { setShift(false) useKeyboardStore.getState().setShift(false) } if (e.key === "Control") { useKeyboardStore.getState().setCtrl(false) } } const wheel = (e: WheelEvent) => { e.preventDefault() const currentScale = useTopToolsStore.getState().scale let newScale = e.deltaY < 0 ? currentScale * 1.1 : currentScale * 0.9 if (newScale >= 100) return setScale(newScale) setGroupScale(newScale) } const mouseclick = (_e: MouseEvent) => { useTopToolsStore.getState().setSubAttrPresetShow(false) } const mousemove = (event: { clientX: number; clientY: number }) => { paperContainerRef.current.handleCrosshairMove(event) } let element = document.getElementById("resize-container") document.addEventListener("keydown", down) document.addEventListener("keyup", up) element?.addEventListener("wheel", wheel) element?.addEventListener("mousedown", mouseclick) element?.addEventListener("mousemove", mousemove) return () => { document.removeEventListener("keydown", down) document.removeEventListener("keyup", up) element?.removeEventListener("wheel", wheel) element?.removeEventListener("mousedown", mouseclick) element?.removeEventListener("mousemove", mousemove) } }, [ editMode, focusInput, getOperationSchema, handleShortcut, isView, projectDetail?.label_type, setEditMode, setGroupScale, setPressA, setScale, setShift, ]) const setGlobalLoading = useCallback((flag: boolean) => { setLoading(flag) }, []) // 切换图片时清空复制保存的数组 useEffect(() => { useKeyboardStore.getState().setCopyDataIds([]) }, [activeImage]) const handleBeforeUnload = (e: any) => { e.preventDefault() // 备份 topRef.current.handleBackup("auto_backup") // 清空操作栏状态 useObjectStore.getState().resetPathAndOperationStatus() } // 更新Splitter状态 const updateSplitterSizes = useCallback(() => { console.log( "document.documentElement.clientWidth", document.documentElement.clientWidth ) const len = document.documentElement.clientWidth - 4 let arr = [len, 0, 0, 0, 0, 0] const flags = [ showTaskList, showGroupList, showDescList && projectDetail && projectDetail.label_type === 5, showDescList && projectDetail && projectDetail.label_type === 6, showDescList && projectDetail && projectDetail.label_type === 7, showObjectList, ] let count = 0 flags.forEach((flag) => { if (flag) count++ }) if (count === 1 || count === 2) { arr[0] = count === 1 ? len * 0.8 : len * 0.6 flags.forEach((flag, index) => { if (flag) { arr[index + 1] = len * 0.2 } }) } else if (count === 3) { arr[0] = len * 0.55 flags.forEach((flag, index) => { if (flag) { arr[index + 1] = len * 0.15 } }) } else if (count === 4) { arr[0] = len * 0.6 flags.forEach((flag, index) => { if (flag) { arr[index + 1] = len * 0.1 } }) } setSizes(arr) }, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList]) useEffect(() => { updateSplitterSizes() }, [updateSplitterSizes]) useEffect(() => { if (!isView) { const delay = 10 * 60000 // 编辑模式下,修改labelData时启动或更新定时器 useTimerStore.getState().startTimer(() => { useTopToolsStore.getState().setIsView(true) }, delay) } else { useTimerStore.getState().clearTimer() } // 组件卸载时清理定时器 return () => { useTimerStore.getState().clearTimer() } }, [isView, labelData]) // auto save useEffect(() => { if (isAutoSave && !isView) { const minute = 60 * 1000 useIntervalStore.getState().startTimer(() => { topRef.current.handleSave() }, autoSaveGap * minute) } else { useIntervalStore.getState().clearTimer() } return () => { useIntervalStore.getState().clearTimer() } }, [autoSaveGap, isAutoSave, isView]) useEffect(() => { window.addEventListener("beforeunload", handleBeforeUnload) return () => { window.removeEventListener("beforeunload", handleBeforeUnload) } }, []) const mainBoxHeight = useMemo(() => { if (projectDetail?.label_type === 5 && metaOperation) { let h = `calc(100% - ${headerHeight}px - 198px)` return h } else if (showMultiFrame) { let h = `calc(100% - ${headerHeight}px - 150px)` return h } let h = `calc(100% - ${headerHeight}px - 70px)` return h }, [projectDetail?.label_type, metaOperation, showMultiFrame, headerHeight]) const fullscreenRef = useRef(null) return ( {/* */} {showTaskList && ( )} {showGroupList && ( )} {showDescList && projectDetail && projectDetail.label_type === 5 && ( )} {showDescList && projectDetail && projectDetail.label_type === 6 && ( )} {showObjectList && ( )} {/* */} {isConfirmSave && ( { try { await topRef.current?.handleSave() setIsConfirmSave(false) } catch {} }} onCancel={() => { setIsConfirmSave(false) }} /> )} ) } export default LabelPage