Merge branch 'zh' into 'main'

Zh

See merge request prism/lable/labelimage!2
This commit is contained in:
刘耀勇
2026-04-01 17:13:43 +08:00
22 changed files with 682 additions and 286 deletions

View File

@@ -5,10 +5,10 @@ NEXT_PUBLIC_ENV=development
# redis # redis
# NEXT_PUBLIC_REDIS_URL="172.16.112.128" # NEXT_PUBLIC_REDIS_URL="172.16.115.128"
NEXT_PUBLIC_REDIS_URL="localhost" NEXT_PUBLIC_REDIS_URL="172.30.21.213"
NEXT_PUBLIC_REDIS_PORT=9765 NEXT_PUBLIC_REDIS_PORT=9765
NEXT_PUBLIC_REDIS_PASSWORD=password NEXT_PUBLIC_REDIS_PASSWORD=password
NEXT_PUBLIC_REDIS_USERNAME=default NEXT_PUBLIC_REDIS_USERNAME=default
NEXT_PUBLIC_REDIS_DB=0 NEXT_PUBLIC_REDIS_DB=1

View File

@@ -3,8 +3,8 @@ NEXT_PUBLIC_BASE_PATH="/image"
NEXT_PUBLIC_ENV=production NEXT_PUBLIC_ENV=production
# redis # redis
NEXT_PUBLIC_REDIS_URL="172.16.105.154" NEXT_PUBLIC_REDIS_URL="172.16.104.13"
NEXT_PUBLIC_REDIS_PORT=8015 NEXT_PUBLIC_REDIS_PORT=6379
NEXT_PUBLIC_REDIS_PASSWORD=aAXJM2uMzPu76L93 NEXT_PUBLIC_REDIS_PASSWORD=123456
NEXT_PUBLIC_REDIS_USERNAME=default NEXT_PUBLIC_REDIS_USERNAME=default
NEXT_PUBLIC_REDIS_DB=10 NEXT_PUBLIC_REDIS_DB=10

View File

@@ -3,7 +3,7 @@ NEXT_PUBLIC_BASE_PATH="/image"
NEXT_PUBLIC_ENV=staging NEXT_PUBLIC_ENV=staging
# redis # 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_PORT=9765
NEXT_PUBLIC_REDIS_PASSWORD=password NEXT_PUBLIC_REDIS_PASSWORD=password
NEXT_PUBLIC_REDIS_USERNAME=default NEXT_PUBLIC_REDIS_USERNAME=default

28
.gitlab-ci.yml Normal file
View File

@@ -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"

View File

@@ -25,3 +25,42 @@ body {
:-webkit-full-screen::backdrop { :-webkit-full-screen::backdrop {
background: var(--mantine-color-body); 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;
}

View File

@@ -188,7 +188,7 @@ export default function OwnTaskTableContainer(props: {
const backUrl = type const backUrl = type
? `/project/detail?id=${projectId}&type=${type}` ? `/project/detail?id=${projectId}&type=${type}`
: `/project/detail?id=${projectId}` : `/project/detail?id=${projectId}`
setBackProps(backUrl, "own") setBackProps(backUrl, "own", "/image")
if (info && [4, 5, 6, 7].includes(info.label_type)) { if (info && [4, 5, 6, 7].includes(info.label_type)) {
router.push( router.push(

View File

@@ -456,7 +456,7 @@ export default function TaskTableContainer(props: {
const backUrl = type const backUrl = type
? `/project/detail?id=${projectId}&type=${type}` ? `/project/detail?id=${projectId}&type=${type}`
: `/project/detail?id=${projectId}` : `/project/detail?id=${projectId}`
setBackProps(backUrl, "all") setBackProps(backUrl, "all", "/image")
if (info && [4, 5, 6, 7].includes(info.label_type)) { if (info && [4, 5, 6, 7].includes(info.label_type)) {
router.push( router.push(

View File

@@ -167,7 +167,7 @@ export default function CollectionDetailPage() {
const backUrl = type const backUrl = type
? `/project/detail?id=${projectId}&type=${type}` ? `/project/detail?id=${projectId}&type=${type}`
: `/project/detail?id=${projectId}` : `/project/detail?id=${projectId}`
setBackProps(backUrl, next) setBackProps(backUrl, next, "/image")
}} }}
style={{ style={{
paddingLeft: 14, paddingLeft: 14,

View File

@@ -1,17 +1,28 @@
"use client"
import { usePermissionStore } from "@/components/label/store/auth"
import AppLayout from "@/components/layout/AppLayout" import AppLayout from "@/components/layout/AppLayout"
import { PathnameRecorder } from "@/components/layout/PathnameRecorder" import { PathnameRecorder } from "@/components/layout/PathnameRecorder"
import { componentList, showList } from "@/components/layout/common" import { componentList, showList } from "@/components/layout/common"
import { useMemo } from "react"
async function getMenu() { // async function getMenu() {
return [...componentList, ...showList] // 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 ( return (
<AppLayout menu={menu}> <AppLayout menu={menu}>
{children} {children}

View File

@@ -51,11 +51,11 @@ FLAG_PROD="${ENV}_latest"
cd $SHELL_FOLDER cd $SHELL_FOLDER
# 检查git状态 # # 检查git状态
echo -e "${GREEN}Checking git status...${NC}" # echo -e "${GREEN}Checking git status...${NC}"
git fetch # git fetch
git merge # git merge
if [[ $? != 0 ]]; then echo -e "${RED}git pull error${NC}"; exit 100; fi # if [[ $? != 0 ]]; then echo -e "${RED}git pull error${NC}"; exit 100; fi
BRANCH_NAME=`git branch | grep "*"` BRANCH_NAME=`git branch | grep "*"`
echo -e "${GREEN}Current branch: ${BRANCH_NAME/* /}${NC}" 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 if [[ $? != 0 ]]; then echo -e "${RED}build error${NC}"; exit 100; fi
# Docker构建和推送 # Docker构建和推送
MAIN_NAME=digitalspace_labelimage MAIN_NAME=web_digitalspace_labelimage
DOCKER_FILE=$SHELL_FOLDER/Dockerfile DOCKER_FILE=$SHELL_FOLDER/Dockerfile
HARBOR_REPO=softrepo
if [[ $ENV == prod ]]; then
HARBOR_REPO=softpro
fi
if [[ $OUTPUT_TYPE == export ]]; then if [[ $OUTPUT_TYPE == export ]]; then
DOCKER_FILE=$SHELL_FOLDER/docker/nginx.dockerfile DOCKER_FILE=$SHELL_FOLDER/docker/nginx.dockerfile
MAIN_NAME=digitalspace_labelimage_static MAIN_NAME=web_digitalspace_labelimage_static
elif [[ $OUTPUT_TYPE == standalone ]]; then elif [[ $OUTPUT_TYPE == standalone ]]; then
DOCKER_FILE=$SHELL_FOLDER/docker/node.dockerfile DOCKER_FILE=$SHELL_FOLDER/docker/node.dockerfile
MAIN_NAME=digitalspace_labelimage_ssr MAIN_NAME=web_digitalspace_labelimage_ssr
fi fi
echo -e "${GREEN}Building docker image...${NC}" 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 if [[ $? != 0 ]]; then echo -e "${RED}build docker with error${NC}"; exit 100; fi
echo -e "${GREEN}Tagging and pushing docker image...${NC}" echo -e "${GREEN}Tagging and pushing docker image...${NC}"
sudo docker tag $MAIN_NAME 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/gopro/$MAIN_NAME:$FLAG_PROD docker push harbor.cowarobot.cn/$HARBOR_REPO/$MAIN_NAME:$FLAG_PROD
echo -e "${GREEN}Build completed successfully${NC}" echo -e "${GREEN}Build completed successfully${NC}"

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"
? "https://label.cowarobot.com" ? "https://label.soft.cowarobot.com"
: process.env.NEXT_PUBLIC_ENV === "staging" : process.env.NEXT_PUBLIC_ENV === "staging"
? "https://label.softtest.cowarobot.com" ? "http://172.16.115.128:9110"
: "https://label.softtest.cowarobot.com" : "https://label.softtest.cowarobot.com"

View File

@@ -122,7 +122,7 @@ export const getAuxiliaryAnnotation = (data: any) => {
export const getAuxiliaryAnnotation2 = (data: { export const getAuxiliaryAnnotation2 = (data: {
project_id: string project_id: string
image_name: string image: string
prompt: { prompt: {
foreground_points?: Array<[number, number]> foreground_points?: Array<[number, number]>
background_points?: Array<[number, number]> background_points?: Array<[number, number]>

View File

@@ -1,21 +1,51 @@
"use client" "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 { useEditor } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit" import StarterKit from "@tiptap/starter-kit"
import Underline from "@tiptap/extension-underline" import { useEffect, useRef, type ChangeEvent } from "react"
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 { useKeyEventStore } from "../store" import { useKeyEventStore } from "../store"
import { useEffect, useRef } from "react" import { useTopToolsStore } from "../useTopToolsStore"
import { Box } from "@mantine/core"
import { IconPhoto } from "@tabler/icons-react"
import "@mantine/tiptap/styles.css"
export const splitWord = "@1024-" 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 { interface EditorContainerProps {
value: string value: string
onChange: (value: string) => void onChange: (value: string) => void
@@ -33,6 +63,7 @@ const EditorContainer = ({
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const editor = useEditor({ const editor = useEditor({
immediatelyRender: false,
extensions: [ extensions: [
StarterKit, StarterKit,
Underline, Underline,
@@ -40,11 +71,11 @@ const EditorContainer = ({
Image, Image,
Placeholder.configure({ placeholder: "请输入内容..." }), Placeholder.configure({ placeholder: "请输入内容..." }),
], ],
content: value, content: getEditorHtml(value),
editable: !disabled && !isView, editable: !disabled && !isView,
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
// Preserve splitWord format const html = getEditorHtml(editor.getHTML())
onChange(`${editor.getHTML()}${splitWord}${editor.getText()}`) onChange(`${html}${splitWord}${editor.getText()}`)
}, },
onFocus: () => { onFocus: () => {
useKeyEventStore.getState().setFocusInput(true) useKeyEventStore.getState().setFocusInput(true)
@@ -61,18 +92,15 @@ const EditorContainer = ({
}, [disabled, isView, editor]) }, [disabled, isView, editor])
useEffect(() => { useEffect(() => {
// Sync external value changes to editor if (!editor) return
// Check if editor content differs from value to avoid cursor jump and loops const incomingHtml = getEditorHtml(value) || "<p></p>"
if (editor && value && editor.getHTML() !== value) { if (editor.getHTML() !== incomingHtml) {
// Only set content if it's substantially different. // Keep editor content in sync with external state, including empty value switch.
// Note: editor.getHTML() might return different string than value (e.g. attribute order). editor.commands.setContent(incomingHtml, { emitUpdate: false })
// Ideally we should compare content, but string comparison is a basic check.
// If value is empty string, we should clear.
editor.commands.setContent(value)
} }
}, [value, editor]) }, [value, editor])
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
if (file && editor) { if (file && editor) {
const reader = new FileReader() const reader = new FileReader()
@@ -92,8 +120,11 @@ const EditorContainer = ({
borderRadius: "8px", borderRadius: "8px",
padding: "4px", padding: "4px",
}}> }}>
<RichTextEditor editor={editor} style={{ border: 0 }}> <RichTextEditor
<RichTextEditor.Toolbar sticky stickyOffset={60}> editor={editor}
labels={editorLabels}
style={{ border: 0 }}>
<RichTextEditor.Toolbar>
<RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup>
<RichTextEditor.Bold /> <RichTextEditor.Bold />
<RichTextEditor.Italic /> <RichTextEditor.Italic />
@@ -125,8 +156,8 @@ const EditorContainer = ({
<RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup>
<RichTextEditor.Control <RichTextEditor.Control
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
aria-label="Upload image" aria-label="上传图片"
title="Upload image"> title="上传图片">
<IconPhoto stroke={1.5} size="1rem" /> <IconPhoto stroke={1.5} size="1rem" />
</RichTextEditor.Control> </RichTextEditor.Control>
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup>

View File

@@ -514,7 +514,7 @@ const PaperContainer = (
try { try {
const data = await getAuxiliaryAnnotation2({ const data = await getAuxiliaryAnnotation2({
project_id: projectId, project_id: projectId,
image_name: activeImage, image: activeImage,
// project_id: "image_test_bk", // project_id: "image_test_bk",
prompt: { prompt: {
foreground_points: promptPoints, foreground_points: promptPoints,
@@ -1311,7 +1311,7 @@ const PaperContainer = (
}, },
hollow.map((item: any) => hollow.map((item: any) =>
item.map((p: any) => { item.map((p: any) => {
if (paper instanceof paper.Segment) { if (p instanceof paper.Segment) {
return [ return [
p.point.x / activeScale, p.point.x / activeScale,
p.point.y / activeScale, p.point.y / activeScale,
@@ -1376,8 +1376,10 @@ const PaperContainer = (
let findIndexInArr = arr.findIndex((v) => v[0] === id) let findIndexInArr = arr.findIndex((v) => v[0] === id)
let oldData = arr.find((a) => a[0] === id)! let oldData = arr.find((a) => a[0] === id)!
let arr2 = needCopyMap.get(needCopyCategoryIds[index])! let arr2 = needCopyMap.get(needCopyCategoryIds[index])!
let copiedItem = arr2.find((b: any) => b[0] === id)
if (!copiedItem) return
let newData = { let newData = {
...arr2.find((b: any) => b[0] === id), ...copiedItem[2],
uid: oldData[2].uid, uid: oldData[2].uid,
create_timestamp: oldData[2].create_timestamp, create_timestamp: oldData[2].create_timestamp,
first_modified_timestamp: first_modified_timestamp:
@@ -1444,7 +1446,21 @@ const PaperContainer = (
data.set(imageId, newCategoryMap) data.set(imageId, newCategoryMap)
} }
} else { } 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" }) notifications.show({ message: "复制成功", color: "green" })

View File

@@ -18,6 +18,7 @@ import { Task } from "../api/task/typing"
import { useBottomToolsStore } from "../useBottomToolsStore" import { useBottomToolsStore } from "../useBottomToolsStore"
import { useDescToolsStore } from "../useDescToolsStore" import { useDescToolsStore } from "../useDescToolsStore"
import { useTopToolsStore } from "../useTopToolsStore" import { useTopToolsStore } from "../useTopToolsStore"
import { safeClone } from "../utils/clone"
import EditorContainer from "./EditorContainer" import EditorContainer from "./EditorContainer"
interface ComponentProps { interface ComponentProps {
@@ -40,46 +41,67 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
}) })
const isUpdatingFromStore = useRef(false) const isUpdatingFromStore = useRef(false)
const syncTimerRef = useRef<ReturnType<typeof setTimeout> | 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 if (isUpdatingFromStore.current) return
const data = form.values as any const currentDescData = safeClone(useDescToolsStore.getState().descData)
let dataMap = new Map() const updateMap = new Map()
taskDetail?.data_name.forEach((detail_name) => { Object.entries(values).forEach(([label_class, val]: any) => {
if (detail_name.name[0] !== activeImage) { let value = { ...val }
if (useDescToolsStore.getState().descData.has(detail_name.name[0])) { if (value.is_correct !== undefined) {
dataMap.set( value.is_correct = value.is_correct === "true"
detail_name.name[0], }
useDescToolsStore.getState().descData.get(detail_name.name[0]) const keys = Object.keys(value)
) if (keys.includes("value")) {
} updateMap.set(label_class, value)
} else { } else {
const updateMap = new Map() updateMap.set(label_class, { value: value })
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)
} }
}) })
setDescData(dataMap) currentDescData.set(activeImage, updateMap)
setDescData(currentDescData)
} }
useEffect(() => { 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.values]) }, [form.values])
useEffect(() => { useEffect(() => {
if (syncTimerRef.current) {
clearTimeout(syncTimerRef.current)
syncTimerRef.current = null
}
isUpdatingFromStore.current = true isUpdatingFromStore.current = true
form.reset() form.reset()
if (activeImage) { if (activeImage) {
@@ -109,6 +131,10 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
useEffect(() => { useEffect(() => {
if (updateDescDataFlag) { if (updateDescDataFlag) {
if (syncTimerRef.current) {
clearTimeout(syncTimerRef.current)
syncTimerRef.current = null
}
isUpdatingFromStore.current = true isUpdatingFromStore.current = true
const formData = useDescToolsStore const formData = useDescToolsStore
.getState() .getState()
@@ -133,6 +159,15 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateDescDataFlag]) }, [updateDescDataFlag])
useEffect(() => {
return () => {
if (syncTimerRef.current) {
clearTimeout(syncTimerRef.current)
syncTimerRef.current = null
}
}
}, [])
return ( return (
<Box h="100%"> <Box h="100%">
<Box <Box
@@ -156,7 +191,9 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
activeImage, activeImage,
operation.label_class operation.label_class
) )
const hasComment = currentData?.comment const hasComment =
!!(form.values as any)?.[operation.label_class]?.comment ||
!!currentData?.comment
return ( return (
<Box key={operation.label_class}> <Box key={operation.label_class}>
@@ -171,9 +208,17 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
{operation.label_class} {operation.label_class}
</Text> </Text>
<Radio.Group <Radio.Group
{...form.getInputProps( value={
`${operation.label_class}.is_correct` (form.values as any)[operation.label_class]
)} ?.is_correct || ""
}
onChange={(val) => {
setOperationField(
operation.label_class,
"is_correct",
val
)
}}
size="xs"> size="xs">
<Group gap="xs"> <Group gap="xs">
<Radio <Radio
@@ -195,8 +240,9 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
variant="subtle" variant="subtle"
size="xs" size="xs"
onClick={() => { onClick={() => {
form.setFieldValue( setOperationField(
`${operation.label_class}.comment`, operation.label_class,
"comment",
"<p></p>" "<p></p>"
) )
}}> }}>
@@ -215,9 +261,18 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
placeholder={`请输入${subDesc.chinese_name}`} placeholder={`请输入${subDesc.chinese_name}`}
size="xs" size="xs"
required={!!subDesc.isrequired} required={!!subDesc.isrequired}
{...form.getInputProps( value={
`${operation.label_class}.${subDesc.chinese_name}` (form.values as any)[operation.label_class]?.[
)} subDesc.chinese_name
] || ""
}
onChange={(event) => {
setOperationField(
operation.label_class,
subDesc.chinese_name,
event.currentTarget.value
)
}}
disabled={!!isView} disabled={!!isView}
/> />
) : subDesc.select_mode === 1 ? ( ) : subDesc.select_mode === 1 ? (
@@ -225,9 +280,18 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
label={subDesc.chinese_name} label={subDesc.chinese_name}
size="xs" size="xs"
required={!!subDesc.isrequired} required={!!subDesc.isrequired}
{...form.getInputProps( value={
`${operation.label_class}.${subDesc.chinese_name}` (form.values as any)[operation.label_class]?.[
)}> subDesc.chinese_name
] || ""
}
onChange={(val) => {
setOperationField(
operation.label_class,
subDesc.chinese_name,
val
)
}}>
<Group gap="xs" mt={4}> <Group gap="xs" mt={4}>
{subDesc.optional_item.map((option) => ( {subDesc.optional_item.map((option) => (
<Radio <Radio
@@ -244,9 +308,18 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
label={subDesc.chinese_name} label={subDesc.chinese_name}
size="xs" size="xs"
required={!!subDesc.isrequired} required={!!subDesc.isrequired}
{...form.getInputProps( value={
`${operation.label_class}.${subDesc.chinese_name}` (form.values as any)[operation.label_class]?.[
)}> subDesc.chinese_name
] || []
}
onChange={(val) => {
setOperationField(
operation.label_class,
subDesc.chinese_name,
val
)
}}>
<Group gap="xs" mt={4}> <Group gap="xs" mt={4}>
{subDesc.optional_item.map((option) => ( {subDesc.optional_item.map((option) => (
<Checkbox <Checkbox
@@ -266,13 +339,11 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
<Box mb="xs"> <Box mb="xs">
<EditorContainer <EditorContainer
value={ value={
(form.values as any)[operation.label_class]?.value (form.values as any)[operation.label_class]?.value ||
""
} }
onChange={(val: any) => onChange={(val: any) =>
form.setFieldValue( setOperationField(operation.label_class, "value", val)
`${operation.label_class}.value`,
val
)
} }
/> />
</Box> </Box>
@@ -282,14 +353,16 @@ const RightDescTools = ({ taskDetail }: ComponentProps) => {
<EditorContainer <EditorContainer
color="border-danger" color="border-danger"
value={ value={
(form.values as any)[operation.label_class]?.comment (form.values as any)[operation.label_class]
?.comment || ""
} }
onChange={(val: any) => onChange={(val: any) => {
form.setFieldValue( setOperationField(
`${operation.label_class}.comment`, operation.label_class,
"comment",
val val
) )
} }}
/> />
</Box> </Box>
</> </>

View File

@@ -379,6 +379,33 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (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( const operationHide = useCallback(
(operationId: string, hide: boolean) => { (operationId: string, hide: boolean) => {
updateOperationStatus(activeImage + operationId, "HIDE", hide) updateOperationStatus(activeImage + operationId, "HIDE", hide)
@@ -388,19 +415,15 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
?.get(operationId) ?.get(operationId)
?.map((child) => { ?.map((child) => {
updatePathStatus(activeImage + child[0], "HIDE", hide) updatePathStatus(activeImage + child[0], "HIDE", hide)
if (getItemsById(child[0]) && getItemsById(child[0]).length) { syncObjectItemsVisibility(child[0], hide)
getItemsById(child[0]).forEach((item) => { // 隐藏时清空选中状态
item.visible = !hide if (hide) setSelectedItemFalse(child[0])
})
// 隐藏时清空选中状态
if (hide) setSelectedItemFalse(child[0])
}
}) })
}, },
[ [
activeImage, activeImage,
getItemsById,
storeLabel, storeLabel,
syncObjectItemsVisibility,
updateOperationStatus, updateOperationStatus,
updatePathStatus, updatePathStatus,
] ]
@@ -410,11 +433,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
(operationId: string, objectId: number, hide: boolean) => { (operationId: string, objectId: number, hide: boolean) => {
if (hide) { if (hide) {
updatePathStatus(activeImage + objectId, "HIDE", hide) updatePathStatus(activeImage + objectId, "HIDE", hide)
if (getItemsById(objectId) && getItemsById(objectId).length) { syncObjectItemsVisibility(objectId, hide)
getItemsById(objectId).forEach((item) => (item.visible = !hide)) // 隐藏时清空选中状态
// 隐藏时清空选中状态 setSelectedItemFalse(objectId)
setSelectedItemFalse(objectId)
}
let childrenHideStatus = storeLabel let childrenHideStatus = storeLabel
.get(activeImage) .get(activeImage)
?.get(operationId) ?.get(operationId)
@@ -427,28 +448,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
} else { } else {
updateOperationStatus(activeImage + operationId, "HIDE", hide) updateOperationStatus(activeImage + operationId, "HIDE", hide)
updatePathStatus(activeImage + objectId, "HIDE", hide) updatePathStatus(activeImage + objectId, "HIDE", hide)
const configs = useTopToolsStore.getState().showTagsConfigs syncObjectItemsVisibility(objectId, hide)
if (getItemsById(objectId) && getItemsById(objectId).length)
getItemsById(objectId).forEach((item) => {
let visible = !hide
if (item.data.type === "tag") {
if (item.data.tag_category === "mask") {
visible = configs.includes("外框")
} else if (item.data.tag_category === "point_text") {
visible = configs.includes("点ID")
} else {
visible = true
}
}
item.visible = visible
})
} }
}, },
[ [
activeImage, activeImage,
getItemById, getItemById,
getItemsById,
storeLabel, storeLabel,
syncObjectItemsVisibility,
updateOperationStatus, updateOperationStatus,
updatePathStatus, updatePathStatus,
] ]

View File

@@ -9,6 +9,7 @@ import {
Group, Group,
NumberInput, NumberInput,
Radio, Radio,
ScrollArea,
Stack, Stack,
Text, Text,
TextInput, TextInput,
@@ -19,12 +20,15 @@ import { IconEdit } from "@tabler/icons-react"
import dayjs from "dayjs" import dayjs from "dayjs"
import parse from "html-react-parser" import parse from "html-react-parser"
import { ArrowUpIcon, ArrowUpToLineIcon } from "lucide-react" import { ArrowUpIcon, ArrowUpToLineIcon } from "lucide-react"
import dynamic from "next/dynamic"
import { import {
forwardRef, forwardRef,
memo,
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef,
useState, useState,
} from "react" } from "react"
import { getWrongWordList } from "../api/scheme" import { getWrongWordList } from "../api/scheme"
@@ -36,7 +40,11 @@ import { useDescToolsStore } from "../useDescToolsStore"
import { useTopToolsStore } from "../useTopToolsStore" import { useTopToolsStore } from "../useTopToolsStore"
import { safeClone } from "../utils/clone" import { safeClone } from "../utils/clone"
import CustomModal from "./CustomModal" import CustomModal from "./CustomModal"
import EditorContainer, { splitWord } from "./EditorContainer" import { splitWord } from "./EditorContainer"
const EditorContainer = dynamic(() => import("./EditorContainer"), {
ssr: false,
})
interface ComponentProps { interface ComponentProps {
taskDetail?: Task.DataProps taskDetail?: Task.DataProps
@@ -54,6 +62,65 @@ const escapeRegExp = (str: string) => {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") 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(/&nbsp;/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(/&nbsp;/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 (
<Text size="xs" c="dimmed" className="qa-preview-empty">
{emptyText}
</Text>
)
}
return <Box className="qa-preview-content">{parsedContent}</Box>
}
)
QaPreviewContent.displayName = "QaPreviewContent"
const RightQAContent = forwardRef( const RightQAContent = forwardRef(
({ taskDetail }: ComponentProps, ref: React.Ref<unknown>) => { ({ taskDetail }: ComponentProps, ref: React.Ref<unknown>) => {
const { isView } = useTopToolsStore() const { isView } = useTopToolsStore()
@@ -88,7 +155,7 @@ const RightQAContent = forwardRef(
const counts = useMemo(() => { const counts = useMemo(() => {
return countAllTurnsAndQaNumber(activeImage) return countAllTurnsAndQaNumber(activeImage)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeImage, qaData, updateDescDataFlag, countAllTurnsAndQaNumber]) }, [activeImage, qaData, countAllTurnsAndQaNumber])
const [open, setOpen] = useState({ const [open, setOpen] = useState({
status: false, status: false,
@@ -97,13 +164,17 @@ const RightQAContent = forwardRef(
isEdit: false, isEdit: false,
}) })
const valueSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// 新增问题 // 新增问题
const onClickAdd = (label: string, index: number) => { const onClickAdd = (label: string, index: number) => {
flushPendingValueSync()
setOpen({ status: true, label, idx: index, isEdit: false }) setOpen({ status: true, label, idx: index, isEdit: false })
} }
// 编辑问题名称 // 编辑问题名称
const onClickEdit = (label: string, index: number, val: EditFormProps) => { const onClickEdit = (label: string, index: number, val: EditFormProps) => {
flushPendingValueSync()
textForm.setValues({ textForm.setValues({
question_name: val.question_name, question_name: val.question_name,
value: val.value || "", value: val.value || "",
@@ -114,55 +185,101 @@ const RightQAContent = forwardRef(
type QaObj = Record<string, any> type QaObj = Record<string, any>
const handleFormUpdate = (changeValues: any) => { const handleFormUpdate = (
changeValues: Record<string, any>,
rawEditorValue?: string,
options?: { notifyFlag?: boolean }
) => {
if (!selectedQuestion) return if (!selectedQuestion) return
const keys = Object.keys(changeValues) const keys = Object.keys(changeValues)
const hasTagChange = keys.includes("tag") 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 { 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 { try {
const finalData = safeClone(currentQaData) const latestQaData = useDescToolsStore.getState().qaData
finalData[category] = newArr const latestCurrentQaData =
qaData.set(activeImage, finalData) latestQaData.get(activeImage) ?? latestQaData.get("init") ?? {}
setQaData(new Map(qaData)) // Force update const latestQaDataArr = latestCurrentQaData[category] ?? []
updateFlag(true) // Ensure flag is updated to trigger recalculations if needed 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) { } catch (error) {
console.log(error) console.log(error)
} }
} }
const onFieldChange = (field: string, value: any) => { const flushPendingValueSync = () => {
form.setFieldValue(field, value) if (!valueSyncTimerRef.current) return
handleFormUpdate({ [field]: value }) 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(() => { useEffect(() => {
if (!selectedQuestion) return if (!selectedQuestion) return
if ( if (
@@ -170,12 +287,10 @@ const RightQAContent = forwardRef(
selectedQuestion.type === "comment" selectedQuestion.type === "comment"
) { ) {
const { value: v } = selectedQuestion const { value: v } = selectedQuestion
let htmlText = "" const htmlText =
if (selectedQuestion.type === "value") { selectedQuestion.type === "value"
htmlText = v.value ? v.value.split(splitWord)[0] : "" ? getHtmlFromStoredValue(v.value)
} else { : getHtmlFromStoredValue(v.comment)
htmlText = v.comment ? v.comment.split(splitWord)[0] : ""
}
const obj = { const obj = {
value: htmlText, value: htmlText,
round: v.id, round: v.id,
@@ -183,7 +298,8 @@ const RightQAContent = forwardRef(
} }
form.setValues(obj) form.setValues(obj)
} }
}, [selectedQuestion, form]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedQuestion])
useEffect(() => { useEffect(() => {
if (updateDescDataFlag) { if (updateDescDataFlag) {
@@ -230,9 +346,10 @@ const RightQAContent = forwardRef(
const newQaData = safeClone(currentQaData) const newQaData = safeClone(currentQaData)
const handleText = (v: string) => { const handleText = (v: string) => {
// 提取文本内容 // 优先使用存储中的纯文本部分;若历史数据不规范则兜底从 HTML 中提取
const text = v.split(splitWord)[1] const text =
let textContent = text.replace(/<[^>]+>/g, "") v.split(splitWord)[1] ?? getTextFromHtml(getHtmlFromStoredValue(v))
let textContent = text
// 替换错词 // 替换错词
for (let i = 0; i < wrongWords.length; i++) { for (let i = 0; i < wrongWords.length; i++) {
const wrong = escapeRegExp(wrongWords[i]) const wrong = escapeRegExp(wrongWords[i])
@@ -256,17 +373,20 @@ const RightQAContent = forwardRef(
const { value, comment } = questionData const { value, comment } = questionData
const update = (v: string, key: "value" | "comment") => { 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) const updatedValue = handleText(v)
if (checkValue !== updatedValue) { if (checkValue !== updatedValue) {
let newValue = `<p>${updatedValue}</p>${splitWord}${updatedValue}` const htmlValue = `<p>${updatedValue}</p>`
let newValue = `${htmlValue}${splitWord}${updatedValue}`
questionData[key] = newValue questionData[key] = newValue
if ( if (
selectedQuestion?.label === name && selectedQuestion?.label === name &&
selectedQuestion?.type === key selectedQuestion?.type === key
) )
form.setFieldValue("value", `<p>${updatedValue}</p>`) form.setFieldValue("value", htmlValue)
} }
} }
@@ -285,16 +405,19 @@ const RightQAContent = forwardRef(
})) }))
return ( return (
<Box h="100%"> <Box
<Text h="100%"
size="sm" style={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
fw={600} <Box px="xs" pt="xs" pb={6}>
px="xs" <Text
mb="xs">{`总轮数:${counts.turns} 总问题数:${counts.question_size},新增单轮数:${counts.new_single_turns},新增多轮数:${counts.new_multiple_turns}`}</Text> size="xs"
<Box fw={500}
c="dimmed">{`总轮数:${counts.turns} 总问题数:${counts.question_size},新增单轮数:${counts.new_single_turns},新增多轮数:${counts.new_multiple_turns}`}</Text>
</Box>
<ScrollArea
className="qa-tools-container" className="qa-tools-container"
h="calc(100% - 300px)" style={{ flex: 1, minHeight: 0 }}
style={{ overflowY: "auto" }}> px="xs">
<Accordion multiple> <Accordion multiple>
{Object.entries(currentQaData).map(([qaCategory, qaDataArr]) => { {Object.entries(currentQaData).map(([qaCategory, qaDataArr]) => {
const counts = useDescToolsStore const counts = useDescToolsStore
@@ -304,20 +427,22 @@ const RightQAContent = forwardRef(
<Accordion.Item key={qaCategory} value={qaCategory}> <Accordion.Item key={qaCategory} value={qaCategory}>
<Accordion.Control> <Accordion.Control>
<Text <Text
size="sm"
fw={ fw={
600 500
}>{`${qaCategory} 轮数:${counts.turns},问题数:${counts.question_size}`}</Text> }>{`${qaCategory} 轮数:${counts.turns},问题数:${counts.question_size}`}</Text>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
<Stack gap="xs"> <Stack gap="xs">
{qaDataArr.map((qaObj, index) => { {qaDataArr.map((qaObj, index) => {
const [question_name, detail] = Object.entries(qaObj)[0] const [question_name, detail] = Object.entries(qaObj)[0]
const htmlText = detail.value const htmlText = getHtmlFromStoredValue(detail.value)
? detail.value.split(splitWord)[0] const commentHtmlText = getHtmlFromStoredValue(
: "" detail.comment
const commentHtmlText = detail.comment )
? detail.comment.split(splitWord)[0] const hasValueContent = hasPreviewContent(htmlText)
: "" const hasCommentContent =
hasPreviewContent(commentHtmlText)
return ( return (
<Box <Box
@@ -332,7 +457,9 @@ const RightQAContent = forwardRef(
}}> }}>
<Flex gap="xs" mb="xs"> <Flex gap="xs" mb="xs">
<Flex align="center"> <Flex align="center">
<Text>{question_name}</Text> <Text size="sm" fw={500}>
{question_name}
</Text>
{!detail.is_pre && !isView ? ( {!detail.is_pre && !isView ? (
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
@@ -351,9 +478,13 @@ const RightQAContent = forwardRef(
</Flex> </Flex>
<Box> <Box>
{detail.tag === 2 ? ( {detail.tag === 2 ? (
<Text c="red"></Text> <Text size="xs" fw={500} c="red">
</Text>
) : detail.tag === 1 ? ( ) : detail.tag === 1 ? (
<Text c="green"></Text> <Text size="xs" fw={500} c="green">
</Text>
) : null} ) : null}
</Box> </Box>
</Flex> </Flex>
@@ -361,7 +492,7 @@ const RightQAContent = forwardRef(
justify="space-between" justify="space-between"
align="center" align="center"
mb="xs"> mb="xs">
<Text size="sm">{detail.id}</Text> <Text size="xs">{detail.id}</Text>
{!isView ? ( {!isView ? (
<Flex gap="xs"> <Flex gap="xs">
{!detail.is_pre ? ( {!detail.is_pre ? (
@@ -464,22 +595,28 @@ const RightQAContent = forwardRef(
: "1px solid var(--mantine-color-gray-3)", : "1px solid var(--mantine-color-gray-3)",
borderRadius: "var(--mantine-radius-md)", borderRadius: "var(--mantine-radius-md)",
cursor: "pointer", cursor: "pointer",
minHeight: "40px",
}} }}
onClick={() => { onClick={() => {
flushPendingValueSync()
setSelectedQuestion({ setSelectedQuestion({
label: question_name, label: question_name,
value: detail, value: detail,
type: "value", type: "value",
category: qaCategory, category: qaCategory,
}) })
console.log("点击批注", htmlText) // console.log("点击批注", htmlText)
form.setValues({ form.setValues({
value: htmlText, value: htmlText,
round: detail.id, round: detail.id,
tag: detail.tag, tag: detail.tag,
}) })
}}> }}>
{parse(htmlText)} <QaPreviewContent
html={htmlText}
hasContent={hasValueContent}
emptyText="暂无内容"
/>
</Box> </Box>
{commentHtmlText ? ( {commentHtmlText ? (
<Box <Box
@@ -489,8 +626,10 @@ const RightQAContent = forwardRef(
"1px solid var(--mantine-color-red-filled)", "1px solid var(--mantine-color-red-filled)",
borderRadius: "var(--mantine-radius-md)", borderRadius: "var(--mantine-radius-md)",
cursor: "pointer", cursor: "pointer",
minHeight: "40px",
}} }}
onClick={() => { onClick={() => {
flushPendingValueSync()
setSelectedQuestion({ setSelectedQuestion({
label: question_name, label: question_name,
value: detail, value: detail,
@@ -503,7 +642,11 @@ const RightQAContent = forwardRef(
tag: detail.tag, tag: detail.tag,
}) })
}}> }}>
{parse(commentHtmlText)} <QaPreviewContent
html={commentHtmlText}
hasContent={hasCommentContent}
emptyText="暂无批注内容"
/>
</Box> </Box>
) : null} ) : null}
</Box> </Box>
@@ -516,52 +659,89 @@ const RightQAContent = forwardRef(
) )
})} })}
</Accordion> </Accordion>
</Box> </ScrollArea>
<Box <Box
style={{ borderTop: "2px solid var(--mantine-color-gray-3)" }} style={{
borderTop:
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
flexShrink: 0,
}}
pt="sm" pt="sm"
px="sm"> px="sm"
pb="sm">
<Stack <Stack
gap="xs" gap="xs"
opacity={!isView ? 1 : 0.5} opacity={!isView ? 1 : 0.5}
style={{ pointerEvents: !isView ? "auto" : "none" }}> style={{ pointerEvents: !isView ? "auto" : "none" }}>
<Flex align="center" gap="xs"> <Box
<Text fw={600}></Text> p="xs"
<Text>{selectedQuestion?.label || ""}</Text> style={{
</Flex> border:
<Flex align="center" gap="xs"> "1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))",
<Text></Text> borderRadius: "var(--mantine-radius-md)",
<NumberInput background:
size="xs" "light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))",
min={1} }}>
{...form.getInputProps("round")} <Flex
onChange={(val) => onFieldChange("round", val)} align="flex-end"
disabled={ justify="space-between"
!selectedQuestion || gap="sm"
(selectedQuestion && selectedQuestion.value?.is_pre) wrap="wrap">
} <Box style={{ flex: 1, minWidth: 140 }}>
/> <Text size="xs" c="dimmed">
<Radio.Group
size="xs" </Text>
{...form.getInputProps("tag")} <Text size="sm" fw={500} truncate="end">
onChange={(val) => onFieldChange("tag", Number(val))}> {selectedQuestion?.label || "未选择问题"}
<Group gap="xs"> </Text>
<Radio </Box>
label="正确" <Box w={100}>
value={1} <Text size="xs" c="dimmed" mb={4}>
disabled={!isReview || isView}
</Text>
<NumberInput
size="xs"
min={1}
{...form.getInputProps("round")}
onChange={(val) => onFieldChange("round", val)}
disabled={
!selectedQuestion ||
(selectedQuestion && selectedQuestion.value?.is_pre)
}
/> />
<Radio </Box>
label="错误" </Flex>
value={2} <Flex align="center" gap="xs" mt="xs">
disabled={!isReview || isView} <Text size="xs" c="dimmed">
/>
</Group> </Text>
</Radio.Group> <Radio.Group
</Flex> size="xs"
{...form.getInputProps("tag")}
onChange={(val) => onFieldChange("tag", Number(val))}>
<Group gap="xs">
<Radio
label="正确"
value={1}
disabled={!isReview || isView}
/>
<Radio
label="错误"
value={2}
disabled={!isReview || isView}
/>
</Group>
</Radio.Group>
</Flex>
</Box>
<Text size="xs" c="dimmed">
</Text>
<EditorContainer <EditorContainer
value={form.values.value} value={form.values.value}
onChange={(val: any) => onFieldChange("value", val)} onChange={(val: string) =>
onFieldChange("value", getHtmlFromStoredValue(val), val)
}
disabled={!selectedQuestion} disabled={!selectedQuestion}
/> />
</Stack> </Stack>
@@ -601,7 +781,7 @@ const RightQAContent = forwardRef(
const finalData = safeClone(currentQaData) const finalData = safeClone(currentQaData)
const updateData = safeClone(currentOperation) const updateData = safeClone(currentOperation)
let obj = { let obj = {
value: formData.value ?? "<p></p>", value: ensureStoredValue(formData.value ?? "<p></p>"),
id: formData.round_id, id: formData.round_id,
is_pre: false, is_pre: false,
tag: 0, tag: 0,
@@ -657,7 +837,9 @@ const RightQAContent = forwardRef(
</Text> </Text>
<EditorContainer <EditorContainer
value={textForm.values.value} value={textForm.values.value}
onChange={(val: any) => textForm.setFieldValue("value", val)} onChange={(val: string) =>
textForm.setFieldValue("value", getHtmlFromStoredValue(val))
}
/> />
</Box> </Box>
<NumberInput <NumberInput

View File

@@ -156,7 +156,7 @@ const TopTools = (
isLabelTask, isLabelTask,
renderPolygons, renderPolygons,
} = props } = props
const { backUrl } = useBackUrlStore() const { backUrl, basePath } = useBackUrlStore()
const { mode, loadingData } = usePaperStore() const { mode, loadingData } = usePaperStore()
const router = useRouter() const router = useRouter()
const user_id = usePermissionStore.getState().user_id const user_id = usePermissionStore.getState().user_id
@@ -1091,9 +1091,9 @@ const TopTools = (
useObjectStore.getState().resetPathAndOperationStatus() useObjectStore.getState().resetPathAndOperationStatus()
} else { } else {
// 无id返回时跳转回任务列表页 // 无id返回时跳转回任务列表页
const url = backUrl || "/" const url = backUrl ? basePath + backUrl : "/"
router.push(url) // router.push(url)
// window.location.href = url window.location.href = url
// 重置图片比例及选中标注对象 // 重置图片比例及选中标注对象
usePaperStore.getState().resetRasterScale() usePaperStore.getState().resetRasterScale()
useObjectStore.getState().resetPathAndOperationStatus() useObjectStore.getState().resetPathAndOperationStatus()
@@ -1757,9 +1757,9 @@ const TopTools = (
c="dark" c="dark"
onClick={() => { onClick={() => {
if (isView) { if (isView) {
const url = backUrl || "/" const url = backUrl ? basePath + backUrl : "/"
router.push(url) // router.push(url)
// window.location.href = url window.location.href = url
// handleBackup("auto_backup"); // handleBackup("auto_backup");
// 重置图片比例及选中标注对象 // 重置图片比例及选中标注对象
usePaperStore.getState().resetRasterScale() usePaperStore.getState().resetRasterScale()
@@ -1827,7 +1827,7 @@ const TopTools = (
</HoverCard.Dropdown> </HoverCard.Dropdown>
</HoverCard> </HoverCard>
<Box style={{ maxWidth: 300 }}> <Box style={{ maxWidth: 300 }}>
<Text truncate="end" size="sm"> <Text truncate="end" size="sm" title={activeImage}>
{activeImage} {activeImage}
</Text> </Text>
</Box> </Box>
@@ -2450,9 +2450,9 @@ const TopTools = (
return return
} }
setConfirmOpen(false) setConfirmOpen(false)
const url = backUrl || "/" const url = backUrl ? basePath + backUrl : "/"
router.push(url) // router.push(url)
// window.location.href = url window.location.href = url
// 重置图片比例及选中标注对象 // 重置图片比例及选中标注对象
usePaperStore.getState().resetRasterScale() usePaperStore.getState().resetRasterScale()
useObjectStore.getState().resetPathAndOperationStatus() useObjectStore.getState().resetPathAndOperationStatus()
@@ -2461,9 +2461,9 @@ const TopTools = (
try { try {
setConfirmOpen(false) setConfirmOpen(false)
handleBackup("auto_backup") handleBackup("auto_backup")
const url = backUrl || "/" const url = backUrl ? basePath + backUrl : "/"
router.push(url) // router.push(url)
// window.location.href = url window.location.href = url
// 重置图片比例及选中标注对象 // 重置图片比例及选中标注对象
usePaperStore.getState().resetRasterScale() usePaperStore.getState().resetRasterScale()
useObjectStore.getState().resetPathAndOperationStatus() useObjectStore.getState().resetPathAndOperationStatus()

View File

@@ -1,8 +1,8 @@
import { ALL_CODE } from "../utils/constants" import dayjs from "dayjs"
import { create } from "zustand" import { create } from "zustand"
import { persist } from "zustand/middleware" import { persist } from "zustand/middleware"
import { storage } from "../store" import { storage } from "../store"
import dayjs from "dayjs" import { ALL_CODE } from "../utils/constants"
const initUserInfo = { const initUserInfo = {
user_id: null, user_id: null,
@@ -263,8 +263,13 @@ export const useBackUrlStore = create(
(set) => ({ (set) => ({
backUrl: null, backUrl: null,
backTitle: null, backTitle: null,
setBackProps: (url: string, title?: string) => basePath: "",
set({ backUrl: url, backTitle: title || null }), setBackProps: (url: string, title?: string, basePath?: string) =>
set({
backUrl: url,
backTitle: title || null,
basePath: basePath || "",
}),
}), }),
{ {
name: "back_url", name: "back_url",

View File

@@ -35,11 +35,11 @@ export const componentList: MenuItem[] = [
url: "video", url: "video",
icon: "SystemIcon", icon: "SystemIcon",
}, },
{ // {
title: "点云标注", // title: "点云标注",
url: "lidar", // url: "lidar",
icon: "SystemIcon", // icon: "SystemIcon",
}, // },
{ {
title: "管理中心", title: "管理中心",
url: "mgt", url: "mgt",

View File

@@ -4,9 +4,9 @@ import { Login } from "./types"
const BASE_PATH = const BASE_PATH =
process.env.NEXT_PUBLIC_ENV === "production" process.env.NEXT_PUBLIC_ENV === "production"
? "https://label.cowarobot.com" ? "https://label.soft.cowarobot.com"
: process.env.NEXT_PUBLIC_ENV === "staging" : process.env.NEXT_PUBLIC_ENV === "staging"
? "https://label.softtest.cowarobot.com" ? "http://172.16.115.128:9110"
: "https://label.softtest.cowarobot.com" : "https://label.softtest.cowarobot.com"
// 获取验证码 // 获取验证码

View File

@@ -1,9 +1,9 @@
version: "3" version: "3"
services: services:
digitalspace_labelimage_ssr: web_digitalspace_labelimage_ssr:
container_name: digitalspace_labelimage_ssr container_name: web_digitalspace_labelimage_ssr
image: harbor.cowarobot.cn/gopro/digitalspace_labelimage_ssr:stage_latest image: harbor.cowarobot.cn/softrepo/web_digitalspace_labelimage_ssr:stage_latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- 5532:5532 - 5532:5532