diff --git a/.env.development b/.env.development index f0c4a53..c0db470 100644 --- a/.env.development +++ b/.env.development @@ -5,10 +5,10 @@ NEXT_PUBLIC_ENV=development # redis -# NEXT_PUBLIC_REDIS_URL="172.16.112.128" -NEXT_PUBLIC_REDIS_URL="localhost" +# NEXT_PUBLIC_REDIS_URL="172.16.115.128" +NEXT_PUBLIC_REDIS_URL="172.30.21.213" NEXT_PUBLIC_REDIS_PORT=9765 NEXT_PUBLIC_REDIS_PASSWORD=password NEXT_PUBLIC_REDIS_USERNAME=default -NEXT_PUBLIC_REDIS_DB=0 +NEXT_PUBLIC_REDIS_DB=1 diff --git a/.env.production b/.env.production index a68234f..63edf82 100644 --- a/.env.production +++ b/.env.production @@ -3,8 +3,8 @@ NEXT_PUBLIC_BASE_PATH="/image" NEXT_PUBLIC_ENV=production # redis -NEXT_PUBLIC_REDIS_URL="172.16.105.154" -NEXT_PUBLIC_REDIS_PORT=8015 -NEXT_PUBLIC_REDIS_PASSWORD=aAXJM2uMzPu76L93 +NEXT_PUBLIC_REDIS_URL="172.16.104.13" +NEXT_PUBLIC_REDIS_PORT=6379 +NEXT_PUBLIC_REDIS_PASSWORD=123456 NEXT_PUBLIC_REDIS_USERNAME=default NEXT_PUBLIC_REDIS_DB=10 \ No newline at end of file diff --git a/.env.staging b/.env.staging index 9dcef7b..f4fc254 100644 --- a/.env.staging +++ b/.env.staging @@ -3,7 +3,7 @@ NEXT_PUBLIC_BASE_PATH="/image" NEXT_PUBLIC_ENV=staging # redis -NEXT_PUBLIC_REDIS_URL="172.30.21.213" +NEXT_PUBLIC_REDIS_URL="172.16.115.128" NEXT_PUBLIC_REDIS_PORT=9765 NEXT_PUBLIC_REDIS_PASSWORD=password NEXT_PUBLIC_REDIS_USERNAME=default diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..4380e73 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,28 @@ +stages: + - sync_github + +sync_github: + stage: sync_github + tags: + - webbuild + image: harbor.cowarobot.cn/softrepo/rust_build:1.94 + script: + - echo "代码已合并到 main, 开始执行发布逻辑..." + - | + for i in {1..10}; do + if command -v docker >/dev/null 2>&1 && [ -S /var/run/docker.sock ]; then + echo "Docker is ready!" + break + fi + echo "Waiting for Docker..." + sleep 2 + done + - docker info + - echo ${CI_PROJECT_NAME} ${CI_PROJECT_PATH} ${CI_PROJECT_DIR} ${CI_PROJECT_URL} ${CI_COMMIT_SHA} + - echo ${HARBORKEY} | docker login harbor.cowarobot.cn -u 'robot$softpro+cibuild' --password-stdin + - rm -rf ${CI_PROJECT_NAME} + - git clone https://oauth2:${TOKEN}@git.cowarobot.com/${CI_PROJECT_PATH} + - cd ${CI_PROJECT_NAME} + - bash build.sh prod + rules: + - if: $CI_COMMIT_BRANCH == "main" diff --git a/app/globals.css b/app/globals.css index e76a859..ab29833 100644 --- a/app/globals.css +++ b/app/globals.css @@ -25,3 +25,42 @@ body { :-webkit-full-screen::backdrop { background: var(--mantine-color-body); } + +.qa-preview-content { + font-size: 12px; + line-height: 1.5; + color: var(--mantine-color-text); + word-break: break-word; +} + +.qa-preview-content p { + margin: 0; +} + +.qa-preview-content h1, +.qa-preview-content h2, +.qa-preview-content h3, +.qa-preview-content h4, +.qa-preview-content h5, +.qa-preview-content h6 { + margin: 0; + font-size: 12px; + font-weight: 500; + line-height: 1.5; +} + +.qa-preview-content ul, +.qa-preview-content ol { + margin: 0; + padding-left: 16px; +} + +.qa-preview-content pre, +.qa-preview-content code { + font-size: 12px; + white-space: pre-wrap; +} + +.qa-preview-empty { + line-height: 24px; +} diff --git a/app/project/detail/OwnTaskTableContainer.tsx b/app/project/detail/OwnTaskTableContainer.tsx index 39d4269..e552b97 100644 --- a/app/project/detail/OwnTaskTableContainer.tsx +++ b/app/project/detail/OwnTaskTableContainer.tsx @@ -188,7 +188,7 @@ export default function OwnTaskTableContainer(props: { const backUrl = type ? `/project/detail?id=${projectId}&type=${type}` : `/project/detail?id=${projectId}` - setBackProps(backUrl, "own") + setBackProps(backUrl, "own", "/image") if (info && [4, 5, 6, 7].includes(info.label_type)) { router.push( diff --git a/app/project/detail/TaskTableContainer.tsx b/app/project/detail/TaskTableContainer.tsx index c70a1cf..bed3b6a 100644 --- a/app/project/detail/TaskTableContainer.tsx +++ b/app/project/detail/TaskTableContainer.tsx @@ -456,7 +456,7 @@ export default function TaskTableContainer(props: { const backUrl = type ? `/project/detail?id=${projectId}&type=${type}` : `/project/detail?id=${projectId}` - setBackProps(backUrl, "all") + setBackProps(backUrl, "all", "/image") if (info && [4, 5, 6, 7].includes(info.label_type)) { router.push( diff --git a/app/project/detail/page.tsx b/app/project/detail/page.tsx index c58b729..712bbf6 100644 --- a/app/project/detail/page.tsx +++ b/app/project/detail/page.tsx @@ -167,7 +167,7 @@ export default function CollectionDetailPage() { const backUrl = type ? `/project/detail?id=${projectId}&type=${type}` : `/project/detail?id=${projectId}` - setBackProps(backUrl, next) + setBackProps(backUrl, next, "/image") }} style={{ paddingLeft: 14, diff --git a/app/project/layout.tsx b/app/project/layout.tsx index 11900c0..c78bb51 100644 --- a/app/project/layout.tsx +++ b/app/project/layout.tsx @@ -1,17 +1,28 @@ +"use client" + +import { usePermissionStore } from "@/components/label/store/auth" import AppLayout from "@/components/layout/AppLayout" import { PathnameRecorder } from "@/components/layout/PathnameRecorder" import { componentList, showList } from "@/components/layout/common" +import { useMemo } from "react" -async function getMenu() { - return [...componentList, ...showList] -} +// async function getMenu() { +// return [...componentList, ...showList] +// } + +export default function Layout({ children }: { children: React.ReactNode }) { + const detailInfo = usePermissionStore((s) => s.detailInfo) + + const menu = useMemo(() => { + if (detailInfo?.roles?.length) { + return [...componentList, ...showList] + } else { + return [...componentList, ...showList].filter( + (item) => item.title !== "管理中心" + ) + } + }, [detailInfo?.roles?.length]) -export default async function Layout({ - children, -}: { - children: React.ReactNode -}) { - const menu = await getMenu() return ( {children} diff --git a/build.sh b/build.sh index 9621c45..e588ca2 100755 --- a/build.sh +++ b/build.sh @@ -51,11 +51,11 @@ FLAG_PROD="${ENV}_latest" cd $SHELL_FOLDER -# 检查git状态 -echo -e "${GREEN}Checking git status...${NC}" -git fetch -git merge -if [[ $? != 0 ]]; then echo -e "${RED}git pull error${NC}"; exit 100; fi +# # 检查git状态 +# echo -e "${GREEN}Checking git status...${NC}" +# git fetch +# git merge +# if [[ $? != 0 ]]; then echo -e "${RED}git pull error${NC}"; exit 100; fi BRANCH_NAME=`git branch | grep "*"` echo -e "${GREEN}Current branch: ${BRANCH_NAME/* /}${NC}" @@ -71,23 +71,27 @@ pnpm run $BUILD_FLAG if [[ $? != 0 ]]; then echo -e "${RED}build error${NC}"; exit 100; fi # Docker构建和推送 -MAIN_NAME=digitalspace_labelimage +MAIN_NAME=web_digitalspace_labelimage DOCKER_FILE=$SHELL_FOLDER/Dockerfile +HARBOR_REPO=softrepo +if [[ $ENV == prod ]]; then + HARBOR_REPO=softpro +fi if [[ $OUTPUT_TYPE == export ]]; then DOCKER_FILE=$SHELL_FOLDER/docker/nginx.dockerfile - MAIN_NAME=digitalspace_labelimage_static + MAIN_NAME=web_digitalspace_labelimage_static elif [[ $OUTPUT_TYPE == standalone ]]; then DOCKER_FILE=$SHELL_FOLDER/docker/node.dockerfile - MAIN_NAME=digitalspace_labelimage_ssr + MAIN_NAME=web_digitalspace_labelimage_ssr fi echo -e "${GREEN}Building docker image...${NC}" -sudo docker build -f $DOCKER_FILE -t $MAIN_NAME $SHELL_FOLDER +docker build -f $DOCKER_FILE -t $MAIN_NAME $SHELL_FOLDER if [[ $? != 0 ]]; then echo -e "${RED}build docker with error${NC}"; exit 100; fi echo -e "${GREEN}Tagging and pushing docker image...${NC}" -sudo docker tag $MAIN_NAME harbor.cowarobot.cn/gopro/$MAIN_NAME:$FLAG_PROD -# docker push harbor.cowarobot.cn/gopro/$MAIN_NAME:$FLAG_PROD +docker tag $MAIN_NAME harbor.cowarobot.cn/$HARBOR_REPO/$MAIN_NAME:$FLAG_PROD +docker push harbor.cowarobot.cn/$HARBOR_REPO/$MAIN_NAME:$FLAG_PROD echo -e "${GREEN}Build completed successfully${NC}" diff --git a/components/label/api/const.ts b/components/label/api/const.ts index 8b520b4..5e1fb07 100644 --- a/components/label/api/const.ts +++ b/components/label/api/const.ts @@ -1,6 +1,6 @@ export const BASE_LABEL_API = process.env.NEXT_PUBLIC_ENV === "production" - ? "https://label.cowarobot.com" + ? "https://label.soft.cowarobot.com" : process.env.NEXT_PUBLIC_ENV === "staging" - ? "https://label.softtest.cowarobot.com" + ? "http://172.16.115.128:9110" : "https://label.softtest.cowarobot.com" diff --git a/components/label/api/label/index.ts b/components/label/api/label/index.ts index b4a3e1b..8762c52 100644 --- a/components/label/api/label/index.ts +++ b/components/label/api/label/index.ts @@ -122,7 +122,7 @@ export const getAuxiliaryAnnotation = (data: any) => { export const getAuxiliaryAnnotation2 = (data: { project_id: string - image_name: string + image: string prompt: { foreground_points?: Array<[number, number]> background_points?: Array<[number, number]> diff --git a/components/label/components/EditorContainer.tsx b/components/label/components/EditorContainer.tsx index b72d431..d0fe8f8 100644 --- a/components/label/components/EditorContainer.tsx +++ b/components/label/components/EditorContainer.tsx @@ -1,21 +1,51 @@ "use client" +import { Box } from "@mantine/core" +import { RichTextEditor } from "@mantine/tiptap" +import "@mantine/tiptap/styles.css" +import { IconPhoto } from "@tabler/icons-react" +import Image from "@tiptap/extension-image" +import Link from "@tiptap/extension-link" +import Placeholder from "@tiptap/extension-placeholder" +import Underline from "@tiptap/extension-underline" import { useEditor } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" -import Underline from "@tiptap/extension-underline" -import Link from "@tiptap/extension-link" -import Image from "@tiptap/extension-image" -import Placeholder from "@tiptap/extension-placeholder" -import { RichTextEditor } from "@mantine/tiptap" -import { useTopToolsStore } from "../useTopToolsStore" +import { useEffect, useRef, type ChangeEvent } from "react" import { useKeyEventStore } from "../store" -import { useEffect, useRef } from "react" -import { Box } from "@mantine/core" -import { IconPhoto } from "@tabler/icons-react" -import "@mantine/tiptap/styles.css" +import { useTopToolsStore } from "../useTopToolsStore" export const splitWord = "@1024-" +const getEditorHtml = (rawValue?: string) => { + if (!rawValue) return "" + const [html] = rawValue.split(splitWord) + return html ?? rawValue +} + +const editorLabels = { + boldControlLabel: "加粗", + italicControlLabel: "斜体", + underlineControlLabel: "下划线", + strikeControlLabel: "删除线", + clearFormattingControlLabel: "清除格式", + codeControlLabel: "行内代码", + h1ControlLabel: "一级标题", + h2ControlLabel: "二级标题", + h3ControlLabel: "三级标题", + h4ControlLabel: "四级标题", + bulletListControlLabel: "无序列表", + orderedListControlLabel: "有序列表", + blockquoteControlLabel: "引用", + hrControlLabel: "分割线", + linkControlLabel: "插入链接", + unlinkControlLabel: "移除链接", + linkEditorInputLabel: "输入链接地址", + linkEditorInputPlaceholder: "请输入链接,例如:https://example.com", + linkEditorExternalLink: "在新标签页打开", + linkEditorInternalLink: "在当前标签页打开", + linkEditorSave: "保存", +} + interface EditorContainerProps { value: string onChange: (value: string) => void @@ -33,6 +63,7 @@ const EditorContainer = ({ const fileInputRef = useRef(null) const editor = useEditor({ + immediatelyRender: false, extensions: [ StarterKit, Underline, @@ -40,11 +71,11 @@ const EditorContainer = ({ Image, Placeholder.configure({ placeholder: "请输入内容..." }), ], - content: value, + content: getEditorHtml(value), editable: !disabled && !isView, onUpdate: ({ editor }) => { - // Preserve splitWord format - onChange(`${editor.getHTML()}${splitWord}${editor.getText()}`) + const html = getEditorHtml(editor.getHTML()) + onChange(`${html}${splitWord}${editor.getText()}`) }, onFocus: () => { useKeyEventStore.getState().setFocusInput(true) @@ -61,18 +92,15 @@ const EditorContainer = ({ }, [disabled, isView, editor]) useEffect(() => { - // Sync external value changes to editor - // Check if editor content differs from value to avoid cursor jump and loops - if (editor && value && editor.getHTML() !== value) { - // Only set content if it's substantially different. - // Note: editor.getHTML() might return different string than value (e.g. attribute order). - // Ideally we should compare content, but string comparison is a basic check. - // If value is empty string, we should clear. - editor.commands.setContent(value) + if (!editor) return + const incomingHtml = getEditorHtml(value) || "

" + if (editor.getHTML() !== incomingHtml) { + // Keep editor content in sync with external state, including empty value switch. + editor.commands.setContent(incomingHtml, { emitUpdate: false }) } }, [value, editor]) - const handleImageUpload = (e: React.ChangeEvent) => { + const handleImageUpload = (e: ChangeEvent) => { const file = e.target.files?.[0] if (file && editor) { const reader = new FileReader() @@ -92,8 +120,11 @@ const EditorContainer = ({ borderRadius: "8px", padding: "4px", }}> - - + + @@ -125,8 +156,8 @@ const EditorContainer = ({ fileInputRef.current?.click()} - aria-label="Upload image" - title="Upload image"> + aria-label="上传图片" + title="上传图片"> diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 60af9e9..5ce17ae 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -514,7 +514,7 @@ const PaperContainer = ( try { const data = await getAuxiliaryAnnotation2({ project_id: projectId, - image_name: activeImage, + image: activeImage, // project_id: "image_test_bk", prompt: { foreground_points: promptPoints, @@ -1311,7 +1311,7 @@ const PaperContainer = ( }, hollow.map((item: any) => item.map((p: any) => { - if (paper instanceof paper.Segment) { + if (p instanceof paper.Segment) { return [ p.point.x / activeScale, p.point.y / activeScale, @@ -1376,8 +1376,10 @@ const PaperContainer = ( let findIndexInArr = arr.findIndex((v) => v[0] === id) let oldData = arr.find((a) => a[0] === id)! let arr2 = needCopyMap.get(needCopyCategoryIds[index])! + let copiedItem = arr2.find((b: any) => b[0] === id) + if (!copiedItem) return let newData = { - ...arr2.find((b: any) => b[0] === id), + ...copiedItem[2], uid: oldData[2].uid, create_timestamp: oldData[2].create_timestamp, first_modified_timestamp: @@ -1444,7 +1446,21 @@ const PaperContainer = ( data.set(imageId, newCategoryMap) } } else { - data.set(imageId, needCopyMap) + let newCategoryMap = new Map() + for (let [key, needCopyItemsArr] of needCopyMap) { + let arr = needCopyItemsArr.map((item: any) => [ + item[0], + item[1].map((path: any) => + path.map(([x, y]: any) => [x * picScale, y * picScale]) + ), + item[2], + item[3].map((path: any) => + path.map(([x, y]: any) => [x * picScale, y * picScale]) + ), + ]) + newCategoryMap.set(key, arr) + } + data.set(imageId, newCategoryMap) } }) notifications.show({ message: "复制成功", color: "green" }) diff --git a/components/label/components/RightDescTools.tsx b/components/label/components/RightDescTools.tsx index caa8de8..ede4b50 100644 --- a/components/label/components/RightDescTools.tsx +++ b/components/label/components/RightDescTools.tsx @@ -18,6 +18,7 @@ import { Task } from "../api/task/typing" import { useBottomToolsStore } from "../useBottomToolsStore" import { useDescToolsStore } from "../useDescToolsStore" import { useTopToolsStore } from "../useTopToolsStore" +import { safeClone } from "../utils/clone" import EditorContainer from "./EditorContainer" interface ComponentProps { @@ -40,46 +41,67 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { }) const isUpdatingFromStore = useRef(false) + const syncTimerRef = useRef | null>(null) - const handleFormUpdate = () => { + const setOperationField = ( + labelClass: string, + field: string, + value: string | string[] + ) => { + const current = ((form.values as any)[labelClass] ?? {}) as Record< + string, + any + > + form.setFieldValue(labelClass, { + ...current, + [field]: value, + }) + } + + const handleFormUpdate = (values: any) => { if (isUpdatingFromStore.current) return - const data = form.values as any - let dataMap = new Map() - taskDetail?.data_name.forEach((detail_name) => { - if (detail_name.name[0] !== activeImage) { - if (useDescToolsStore.getState().descData.has(detail_name.name[0])) { - dataMap.set( - detail_name.name[0], - useDescToolsStore.getState().descData.get(detail_name.name[0]) - ) - } + const currentDescData = safeClone(useDescToolsStore.getState().descData) + const updateMap = new Map() + Object.entries(values).forEach(([label_class, val]: any) => { + let value = { ...val } + if (value.is_correct !== undefined) { + value.is_correct = value.is_correct === "true" + } + const keys = Object.keys(value) + if (keys.includes("value")) { + updateMap.set(label_class, value) } else { - const updateMap = new Map() - Object.entries(data).forEach(([label_class, val]: any) => { - let value = { ...val } - if (value.is_correct !== undefined) { - value.is_correct = value.is_correct === "true" - } - const keys = Object.keys(value) - if (keys.includes("value")) { - updateMap.set(label_class, value) - } else { - updateMap.set(label_class, { value: value }) - } - }) - dataMap.set(activeImage, updateMap) + updateMap.set(label_class, { value: value }) } }) - setDescData(dataMap) + currentDescData.set(activeImage, updateMap) + setDescData(currentDescData) } useEffect(() => { - handleFormUpdate() + if (isUpdatingFromStore.current) return + if (syncTimerRef.current) { + clearTimeout(syncTimerRef.current) + } + syncTimerRef.current = setTimeout(() => { + handleFormUpdate(form.values) + syncTimerRef.current = null + }, 180) + return () => { + if (syncTimerRef.current) { + clearTimeout(syncTimerRef.current) + syncTimerRef.current = null + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [form.values]) useEffect(() => { + if (syncTimerRef.current) { + clearTimeout(syncTimerRef.current) + syncTimerRef.current = null + } isUpdatingFromStore.current = true form.reset() if (activeImage) { @@ -109,6 +131,10 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { useEffect(() => { if (updateDescDataFlag) { + if (syncTimerRef.current) { + clearTimeout(syncTimerRef.current) + syncTimerRef.current = null + } isUpdatingFromStore.current = true const formData = useDescToolsStore .getState() @@ -133,6 +159,15 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateDescDataFlag]) + useEffect(() => { + return () => { + if (syncTimerRef.current) { + clearTimeout(syncTimerRef.current) + syncTimerRef.current = null + } + } + }, []) + return ( { activeImage, operation.label_class ) - const hasComment = currentData?.comment + const hasComment = + !!(form.values as any)?.[operation.label_class]?.comment || + !!currentData?.comment return ( @@ -171,9 +208,17 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { {operation.label_class} { + setOperationField( + operation.label_class, + "is_correct", + val + ) + }} size="xs"> { variant="subtle" size="xs" onClick={() => { - form.setFieldValue( - `${operation.label_class}.comment`, + setOperationField( + operation.label_class, + "comment", "

" ) }}> @@ -215,9 +261,18 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { placeholder={`请输入${subDesc.chinese_name}`} size="xs" required={!!subDesc.isrequired} - {...form.getInputProps( - `${operation.label_class}.${subDesc.chinese_name}` - )} + value={ + (form.values as any)[operation.label_class]?.[ + subDesc.chinese_name + ] || "" + } + onChange={(event) => { + setOperationField( + operation.label_class, + subDesc.chinese_name, + event.currentTarget.value + ) + }} disabled={!!isView} /> ) : subDesc.select_mode === 1 ? ( @@ -225,9 +280,18 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { label={subDesc.chinese_name} size="xs" required={!!subDesc.isrequired} - {...form.getInputProps( - `${operation.label_class}.${subDesc.chinese_name}` - )}> + value={ + (form.values as any)[operation.label_class]?.[ + subDesc.chinese_name + ] || "" + } + onChange={(val) => { + setOperationField( + operation.label_class, + subDesc.chinese_name, + val + ) + }}> {subDesc.optional_item.map((option) => ( { label={subDesc.chinese_name} size="xs" required={!!subDesc.isrequired} - {...form.getInputProps( - `${operation.label_class}.${subDesc.chinese_name}` - )}> + value={ + (form.values as any)[operation.label_class]?.[ + subDesc.chinese_name + ] || [] + } + onChange={(val) => { + setOperationField( + operation.label_class, + subDesc.chinese_name, + val + ) + }}> {subDesc.optional_item.map((option) => ( { - form.setFieldValue( - `${operation.label_class}.value`, - val - ) + setOperationField(operation.label_class, "value", val) } /> @@ -282,14 +353,16 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => { - form.setFieldValue( - `${operation.label_class}.comment`, + onChange={(val: any) => { + setOperationField( + operation.label_class, + "comment", val ) - } + }} />
diff --git a/components/label/components/RightObjectTools.tsx b/components/label/components/RightObjectTools.tsx index 315101a..b51b728 100644 --- a/components/label/components/RightObjectTools.tsx +++ b/components/label/components/RightObjectTools.tsx @@ -379,6 +379,33 @@ const RightObjectTools: React.FC = (props) => { }) } + const getTagVisibility = (tagCategory?: string) => { + const { showTags, showTagsConfigs } = useTopToolsStore.getState() + if (!showTags) return false + if (tagCategory === "mask") return showTagsConfigs.includes("外框") + if (tagCategory === "point_text") return showTagsConfigs.includes("点ID") + return true + } + + const syncObjectItemsVisibility = useCallback( + (objectId: number, hide: boolean) => { + const sameIdItems = getItemsById(objectId) + if (!sameIdItems?.length) return + sameIdItems.forEach((item) => { + if (hide) { + item.visible = false + return + } + if (item.data?.type === "tag") { + item.visible = getTagVisibility(item.data?.tag_category) + return + } + item.visible = true + }) + }, + [getItemsById] + ) + const operationHide = useCallback( (operationId: string, hide: boolean) => { updateOperationStatus(activeImage + operationId, "HIDE", hide) @@ -388,19 +415,15 @@ const RightObjectTools: React.FC = (props) => { ?.get(operationId) ?.map((child) => { updatePathStatus(activeImage + child[0], "HIDE", hide) - if (getItemsById(child[0]) && getItemsById(child[0]).length) { - getItemsById(child[0]).forEach((item) => { - item.visible = !hide - }) - // 隐藏时清空选中状态 - if (hide) setSelectedItemFalse(child[0]) - } + syncObjectItemsVisibility(child[0], hide) + // 隐藏时清空选中状态 + if (hide) setSelectedItemFalse(child[0]) }) }, [ activeImage, - getItemsById, storeLabel, + syncObjectItemsVisibility, updateOperationStatus, updatePathStatus, ] @@ -410,11 +433,9 @@ const RightObjectTools: React.FC = (props) => { (operationId: string, objectId: number, hide: boolean) => { if (hide) { updatePathStatus(activeImage + objectId, "HIDE", hide) - if (getItemsById(objectId) && getItemsById(objectId).length) { - getItemsById(objectId).forEach((item) => (item.visible = !hide)) - // 隐藏时清空选中状态 - setSelectedItemFalse(objectId) - } + syncObjectItemsVisibility(objectId, hide) + // 隐藏时清空选中状态 + setSelectedItemFalse(objectId) let childrenHideStatus = storeLabel .get(activeImage) ?.get(operationId) @@ -427,28 +448,14 @@ const RightObjectTools: React.FC = (props) => { } else { updateOperationStatus(activeImage + operationId, "HIDE", hide) updatePathStatus(activeImage + objectId, "HIDE", hide) - const configs = useTopToolsStore.getState().showTagsConfigs - if (getItemsById(objectId) && getItemsById(objectId).length) - getItemsById(objectId).forEach((item) => { - let visible = !hide - if (item.data.type === "tag") { - if (item.data.tag_category === "mask") { - visible = configs.includes("外框") - } else if (item.data.tag_category === "point_text") { - visible = configs.includes("点ID") - } else { - visible = true - } - } - item.visible = visible - }) + syncObjectItemsVisibility(objectId, hide) } }, [ activeImage, getItemById, - getItemsById, storeLabel, + syncObjectItemsVisibility, updateOperationStatus, updatePathStatus, ] diff --git a/components/label/components/RightQATools.tsx b/components/label/components/RightQATools.tsx index 68f1de5..885d25a 100644 --- a/components/label/components/RightQATools.tsx +++ b/components/label/components/RightQATools.tsx @@ -9,6 +9,7 @@ import { Group, NumberInput, Radio, + ScrollArea, Stack, Text, TextInput, @@ -19,12 +20,15 @@ import { IconEdit } from "@tabler/icons-react" import dayjs from "dayjs" import parse from "html-react-parser" import { ArrowUpIcon, ArrowUpToLineIcon } from "lucide-react" +import dynamic from "next/dynamic" import { forwardRef, + memo, useCallback, useEffect, useImperativeHandle, useMemo, + useRef, useState, } from "react" import { getWrongWordList } from "../api/scheme" @@ -36,7 +40,11 @@ import { useDescToolsStore } from "../useDescToolsStore" import { useTopToolsStore } from "../useTopToolsStore" import { safeClone } from "../utils/clone" import CustomModal from "./CustomModal" -import EditorContainer, { splitWord } from "./EditorContainer" +import { splitWord } from "./EditorContainer" + +const EditorContainer = dynamic(() => import("./EditorContainer"), { + ssr: false, +}) interface ComponentProps { taskDetail?: Task.DataProps @@ -54,6 +62,65 @@ const escapeRegExp = (str: string) => { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") } +const getHtmlFromStoredValue = (value?: string) => { + if (!value) return "" + const [html] = value.split(splitWord) + return html ?? value +} + +const getTextFromHtml = (html: string) => { + return html + .replace(/<[^>]+>/g, "") + .replace(/ /g, " ") + .trim() +} + +const ensureStoredValue = (value?: string) => { + if (!value) return "" + if (value.includes(splitWord)) return value + return `${value}${splitWord}${getTextFromHtml(value)}` +} + +const hasPreviewContent = (html?: string) => { + if (!html) return false + if (/<(img|video|audio|iframe|hr)\b/i.test(html)) return true + return ( + html + .replace(/<[^>]+>/g, "") + .replace(/ /g, " ") + .trim().length > 0 + ) +} + +const QaPreviewContent = memo( + ({ + html, + hasContent, + emptyText, + }: { + html: string + hasContent: boolean + emptyText: string + }) => { + const parsedContent = useMemo(() => { + if (!hasContent) return null + return parse(html) + }, [hasContent, html]) + + if (!hasContent) { + return ( + + {emptyText} + + ) + } + + return {parsedContent} + } +) + +QaPreviewContent.displayName = "QaPreviewContent" + const RightQAContent = forwardRef( ({ taskDetail }: ComponentProps, ref: React.Ref) => { const { isView } = useTopToolsStore() @@ -88,7 +155,7 @@ const RightQAContent = forwardRef( const counts = useMemo(() => { return countAllTurnsAndQaNumber(activeImage) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeImage, qaData, updateDescDataFlag, countAllTurnsAndQaNumber]) + }, [activeImage, qaData, countAllTurnsAndQaNumber]) const [open, setOpen] = useState({ status: false, @@ -97,13 +164,17 @@ const RightQAContent = forwardRef( isEdit: false, }) + const valueSyncTimerRef = useRef | null>(null) + // 新增问题 const onClickAdd = (label: string, index: number) => { + flushPendingValueSync() setOpen({ status: true, label, idx: index, isEdit: false }) } // 编辑问题名称 const onClickEdit = (label: string, index: number, val: EditFormProps) => { + flushPendingValueSync() textForm.setValues({ question_name: val.question_name, value: val.value || "", @@ -114,55 +185,101 @@ const RightQAContent = forwardRef( type QaObj = Record - const handleFormUpdate = (changeValues: any) => { + const handleFormUpdate = ( + changeValues: Record, + rawEditorValue?: string, + options?: { notifyFlag?: boolean } + ) => { if (!selectedQuestion) return const keys = Object.keys(changeValues) const hasTagChange = keys.includes("tag") - const formData = form.values + const formData = { + ...form.values, + ...changeValues, + } + const nextStoredValue = + typeof rawEditorValue === "string" + ? ensureStoredValue(rawEditorValue) + : ensureStoredValue(formData.value) const { label, type, category } = selectedQuestion - const currentQaDataArr = currentQaData[category] ?? [] - let newArr: { [x: string]: any }[] = [] - currentQaDataArr.forEach((qaObj: QaObj) => { - const [question_name, detail] = Object.entries(qaObj)[0] - if (question_name === label) { - newArr.push({ - [question_name]: { - value: type === "value" ? formData.value : detail.value, - id: formData.round, - is_pre: detail.is_pre, - tag: formData.tag, - uid: detail.uid - ? detail.uid - : usePermissionStore.getState().user_id, - create_timestamp: detail.create_timestamp - ? detail.create_timestamp - : dayjs().unix(), - modify_uid: usePermissionStore.getState().user_id, - modify_timestamp: dayjs().unix(), - comment: type === "comment" ? formData.value : detail.comment, - flag: hasTagChange ? 0 : detail.tag === 2 ? 1 : 0, - }, - }) - } else { - newArr.push(qaObj) - } - }) try { - const finalData = safeClone(currentQaData) - finalData[category] = newArr - qaData.set(activeImage, finalData) - setQaData(new Map(qaData)) // Force update - updateFlag(true) // Ensure flag is updated to trigger recalculations if needed + const latestQaData = useDescToolsStore.getState().qaData + const latestCurrentQaData = + latestQaData.get(activeImage) ?? latestQaData.get("init") ?? {} + const latestQaDataArr = latestCurrentQaData[category] ?? [] + let latestNewArr: { [x: string]: any }[] = [] + latestQaDataArr.forEach((qaObj: QaObj) => { + const [question_name, detail] = Object.entries(qaObj)[0] + if (question_name === label) { + latestNewArr.push({ + [question_name]: { + value: type === "value" ? nextStoredValue : detail.value, + id: formData.round, + is_pre: detail.is_pre, + tag: formData.tag, + uid: detail.uid + ? detail.uid + : usePermissionStore.getState().user_id, + create_timestamp: detail.create_timestamp + ? detail.create_timestamp + : dayjs().unix(), + modify_uid: usePermissionStore.getState().user_id, + modify_timestamp: dayjs().unix(), + comment: type === "comment" ? nextStoredValue : detail.comment, + flag: hasTagChange ? 0 : detail.tag === 2 ? 1 : 0, + }, + }) + } else { + latestNewArr.push(qaObj) + } + }) + const finalData = safeClone(latestCurrentQaData) + finalData[category] = latestNewArr + latestQaData.set(activeImage, finalData) + setQaData(new Map(latestQaData)) // Force update + if (options?.notifyFlag) { + updateFlag(true) // Ensure flag is updated to trigger recalculations if needed + } } catch (error) { console.log(error) } } - const onFieldChange = (field: string, value: any) => { - form.setFieldValue(field, value) - handleFormUpdate({ [field]: value }) + const flushPendingValueSync = () => { + if (!valueSyncTimerRef.current) return + clearTimeout(valueSyncTimerRef.current) + valueSyncTimerRef.current = null + handleFormUpdate({ value: form.values.value }, form.values.value) } + const onFieldChange = ( + field: string, + value: any, + rawEditorValue?: string + ) => { + form.setFieldValue(field, value) + if (field === "value") { + if (valueSyncTimerRef.current) { + clearTimeout(valueSyncTimerRef.current) + } + valueSyncTimerRef.current = setTimeout(() => { + handleFormUpdate({ [field]: value }, rawEditorValue) + valueSyncTimerRef.current = null + }, 180) + return + } + handleFormUpdate({ [field]: value }, rawEditorValue, { notifyFlag: true }) + } + + useEffect(() => { + return () => { + if (valueSyncTimerRef.current) { + clearTimeout(valueSyncTimerRef.current) + valueSyncTimerRef.current = null + } + } + }, []) + useEffect(() => { if (!selectedQuestion) return if ( @@ -170,12 +287,10 @@ const RightQAContent = forwardRef( selectedQuestion.type === "comment" ) { const { value: v } = selectedQuestion - let htmlText = "" - if (selectedQuestion.type === "value") { - htmlText = v.value ? v.value.split(splitWord)[0] : "" - } else { - htmlText = v.comment ? v.comment.split(splitWord)[0] : "" - } + const htmlText = + selectedQuestion.type === "value" + ? getHtmlFromStoredValue(v.value) + : getHtmlFromStoredValue(v.comment) const obj = { value: htmlText, round: v.id, @@ -183,7 +298,8 @@ const RightQAContent = forwardRef( } form.setValues(obj) } - }, [selectedQuestion, form]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedQuestion]) useEffect(() => { if (updateDescDataFlag) { @@ -230,9 +346,10 @@ const RightQAContent = forwardRef( const newQaData = safeClone(currentQaData) const handleText = (v: string) => { - // 提取文本内容 - const text = v.split(splitWord)[1] - let textContent = text.replace(/<[^>]+>/g, "") + // 优先使用存储中的纯文本部分;若历史数据不规范则兜底从 HTML 中提取 + const text = + v.split(splitWord)[1] ?? getTextFromHtml(getHtmlFromStoredValue(v)) + let textContent = text // 替换错词 for (let i = 0; i < wrongWords.length; i++) { const wrong = escapeRegExp(wrongWords[i]) @@ -256,17 +373,20 @@ const RightQAContent = forwardRef( const { value, comment } = questionData const update = (v: string, key: "value" | "comment") => { - const checkValue = v.split(splitWord)[1] + const checkValue = + v.split(splitWord)[1] ?? + getTextFromHtml(getHtmlFromStoredValue(v)) const updatedValue = handleText(v) if (checkValue !== updatedValue) { - let newValue = `

${updatedValue}

${splitWord}${updatedValue}` + const htmlValue = `

${updatedValue}

` + let newValue = `${htmlValue}${splitWord}${updatedValue}` questionData[key] = newValue if ( selectedQuestion?.label === name && selectedQuestion?.type === key ) - form.setFieldValue("value", `

${updatedValue}

`) + form.setFieldValue("value", htmlValue) } } @@ -285,16 +405,19 @@ const RightQAContent = forwardRef( })) return ( - - {`总轮数:${counts.turns}, 总问题数:${counts.question_size},新增单轮数:${counts.new_single_turns},新增多轮数:${counts.new_multiple_turns}`} - + + {`总轮数:${counts.turns}, 总问题数:${counts.question_size},新增单轮数:${counts.new_single_turns},新增多轮数:${counts.new_multiple_turns}`} + + + style={{ flex: 1, minHeight: 0 }} + px="xs"> {Object.entries(currentQaData).map(([qaCategory, qaDataArr]) => { const counts = useDescToolsStore @@ -304,20 +427,22 @@ const RightQAContent = forwardRef( {`${qaCategory} 轮数:${counts.turns},问题数:${counts.question_size}`} {qaDataArr.map((qaObj, index) => { const [question_name, detail] = Object.entries(qaObj)[0] - const htmlText = detail.value - ? detail.value.split(splitWord)[0] - : "" - const commentHtmlText = detail.comment - ? detail.comment.split(splitWord)[0] - : "" + const htmlText = getHtmlFromStoredValue(detail.value) + const commentHtmlText = getHtmlFromStoredValue( + detail.comment + ) + const hasValueContent = hasPreviewContent(htmlText) + const hasCommentContent = + hasPreviewContent(commentHtmlText) return ( - {question_name} + + {question_name} + {!detail.is_pre && !isView ? ( {detail.tag === 2 ? ( - 错误 + + 错误 + ) : detail.tag === 1 ? ( - 正确 + + 正确 + ) : null} @@ -361,7 +492,7 @@ const RightQAContent = forwardRef( justify="space-between" align="center" mb="xs"> - 轮次:{detail.id} + 轮次:{detail.id} {!isView ? ( {!detail.is_pre ? ( @@ -464,22 +595,28 @@ const RightQAContent = forwardRef( : "1px solid var(--mantine-color-gray-3)", borderRadius: "var(--mantine-radius-md)", cursor: "pointer", + minHeight: "40px", }} onClick={() => { + flushPendingValueSync() setSelectedQuestion({ label: question_name, value: detail, type: "value", category: qaCategory, }) - console.log("点击批注", htmlText) + // console.log("点击批注", htmlText) form.setValues({ value: htmlText, round: detail.id, tag: detail.tag, }) }}> - {parse(htmlText)} + {commentHtmlText ? ( { + flushPendingValueSync() setSelectedQuestion({ label: question_name, value: detail, @@ -503,7 +642,11 @@ const RightQAContent = forwardRef( tag: detail.tag, }) }}> - {parse(commentHtmlText)} + ) : null} @@ -516,52 +659,89 @@ const RightQAContent = forwardRef( ) })} - + + px="sm" + pb="sm"> - - 问题 - {selectedQuestion?.label || ""} - - - 轮次 - onFieldChange("round", val)} - disabled={ - !selectedQuestion || - (selectedQuestion && selectedQuestion.value?.is_pre) - } - /> - onFieldChange("tag", Number(val))}> - - + + + + 当前问题 + + + {selectedQuestion?.label || "未选择问题"} + + + + + 轮次 + + onFieldChange("round", val)} + disabled={ + !selectedQuestion || + (selectedQuestion && selectedQuestion.value?.is_pre) + } /> - - - - + + + + + 审核结果 + + onFieldChange("tag", Number(val))}> + + + + + + +
+ + 内容编辑 + onFieldChange("value", val)} + onChange={(val: string) => + onFieldChange("value", getHtmlFromStoredValue(val), val) + } disabled={!selectedQuestion} /> @@ -601,7 +781,7 @@ const RightQAContent = forwardRef( const finalData = safeClone(currentQaData) const updateData = safeClone(currentOperation) let obj = { - value: formData.value ?? "

", + value: ensureStoredValue(formData.value ?? "

"), id: formData.round_id, is_pre: false, tag: 0, @@ -657,7 +837,9 @@ const RightQAContent = forwardRef( textForm.setFieldValue("value", val)} + onChange={(val: string) => + textForm.setFieldValue("value", getHtmlFromStoredValue(val)) + } />
{ if (isView) { - const url = backUrl || "/" - router.push(url) - // window.location.href = url + const url = backUrl ? basePath + backUrl : "/" + // router.push(url) + window.location.href = url // handleBackup("auto_backup"); // 重置图片比例及选中标注对象 usePaperStore.getState().resetRasterScale() @@ -1827,7 +1827,7 @@ const TopTools = ( - + {activeImage} @@ -2450,9 +2450,9 @@ const TopTools = ( return } setConfirmOpen(false) - const url = backUrl || "/" - router.push(url) - // window.location.href = url + const url = backUrl ? basePath + backUrl : "/" + // router.push(url) + window.location.href = url // 重置图片比例及选中标注对象 usePaperStore.getState().resetRasterScale() useObjectStore.getState().resetPathAndOperationStatus() @@ -2461,9 +2461,9 @@ const TopTools = ( try { setConfirmOpen(false) handleBackup("auto_backup") - const url = backUrl || "/" - router.push(url) - // window.location.href = url + const url = backUrl ? basePath + backUrl : "/" + // router.push(url) + window.location.href = url // 重置图片比例及选中标注对象 usePaperStore.getState().resetRasterScale() useObjectStore.getState().resetPathAndOperationStatus() diff --git a/components/label/store/auth.ts b/components/label/store/auth.ts index 55e668d..5ef8fc5 100644 --- a/components/label/store/auth.ts +++ b/components/label/store/auth.ts @@ -1,8 +1,8 @@ -import { ALL_CODE } from "../utils/constants" +import dayjs from "dayjs" import { create } from "zustand" import { persist } from "zustand/middleware" import { storage } from "../store" -import dayjs from "dayjs" +import { ALL_CODE } from "../utils/constants" const initUserInfo = { user_id: null, @@ -263,8 +263,13 @@ export const useBackUrlStore = create( (set) => ({ backUrl: null, backTitle: null, - setBackProps: (url: string, title?: string) => - set({ backUrl: url, backTitle: title || null }), + basePath: "", + setBackProps: (url: string, title?: string, basePath?: string) => + set({ + backUrl: url, + backTitle: title || null, + basePath: basePath || "", + }), }), { name: "back_url", diff --git a/components/layout/common.ts b/components/layout/common.ts index cad1a1e..d6f3276 100644 --- a/components/layout/common.ts +++ b/components/layout/common.ts @@ -35,11 +35,11 @@ export const componentList: MenuItem[] = [ url: "video", icon: "SystemIcon", }, - { - title: "点云标注", - url: "lidar", - icon: "SystemIcon", - }, + // { + // title: "点云标注", + // url: "lidar", + // icon: "SystemIcon", + // }, { title: "管理中心", url: "mgt", diff --git a/components/login/api/index.ts b/components/login/api/index.ts index a4d5890..2075b43 100644 --- a/components/login/api/index.ts +++ b/components/login/api/index.ts @@ -4,9 +4,9 @@ import { Login } from "./types" const BASE_PATH = process.env.NEXT_PUBLIC_ENV === "production" - ? "https://label.cowarobot.com" + ? "https://label.soft.cowarobot.com" : process.env.NEXT_PUBLIC_ENV === "staging" - ? "https://label.softtest.cowarobot.com" + ? "http://172.16.115.128:9110" : "https://label.softtest.cowarobot.com" // 获取验证码 diff --git a/docker-compose.yml b/docker-compose.yml index 975c08c..73dc848 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ version: "3" services: - digitalspace_labelimage_ssr: - container_name: digitalspace_labelimage_ssr - image: harbor.cowarobot.cn/gopro/digitalspace_labelimage_ssr:stage_latest + web_digitalspace_labelimage_ssr: + container_name: web_digitalspace_labelimage_ssr + image: harbor.cowarobot.cn/softrepo/web_digitalspace_labelimage_ssr:stage_latest restart: unless-stopped ports: - 5532:5532