687 lines
26 KiB
TypeScript
687 lines
26 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
Accordion,
|
||
ActionIcon,
|
||
Box,
|
||
Button,
|
||
Flex,
|
||
Group,
|
||
NumberInput,
|
||
Radio,
|
||
Stack,
|
||
Text,
|
||
TextInput,
|
||
Tooltip,
|
||
} from "@mantine/core"
|
||
import { useForm } from "@mantine/form"
|
||
import { IconEdit } from "@tabler/icons-react"
|
||
import dayjs from "dayjs"
|
||
import parse from "html-react-parser"
|
||
import { ArrowUpIcon, ArrowUpToLineIcon } from "lucide-react"
|
||
import {
|
||
forwardRef,
|
||
useCallback,
|
||
useEffect,
|
||
useImperativeHandle,
|
||
useMemo,
|
||
useState,
|
||
} from "react"
|
||
import { getWrongWordList } from "../api/scheme"
|
||
import { Task } from "../api/task/typing"
|
||
import { useKeyEventStore } from "../store"
|
||
import { usePermissionStore } from "../store/auth"
|
||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||
import { useDescToolsStore } from "../useDescToolsStore"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
import CustomModal from "./CustomModal"
|
||
import EditorContainer, { splitWord } from "./EditorContainer"
|
||
|
||
interface ComponentProps {
|
||
taskDetail?: Task.DataProps
|
||
// projectDetail?: Project.DetailResponse;
|
||
}
|
||
|
||
interface EditFormProps {
|
||
question_name: string
|
||
value?: string
|
||
round_id: number
|
||
}
|
||
|
||
// 处理转义字符
|
||
const escapeRegExp = (str: string) => {
|
||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||
}
|
||
|
||
const RightQAContent = forwardRef(
|
||
({ taskDetail }: ComponentProps, ref: React.Ref<unknown>) => {
|
||
const { isView } = useTopToolsStore()
|
||
const { activeImage } = useBottomToolsStore()
|
||
const {
|
||
qaData,
|
||
setQaData,
|
||
countAllTurnsAndQaNumber,
|
||
updateDescDataFlag,
|
||
updateFlag,
|
||
} = useDescToolsStore()
|
||
const currentQaData = qaData.get(activeImage) ?? qaData.get("init") ?? {}
|
||
const isReview = [4, 6].includes(taskDetail?.label_status || 0)
|
||
const [selectedQuestion, setSelectedQuestion] = useState<any>(null)
|
||
|
||
const form = useForm({
|
||
initialValues: {
|
||
value: "",
|
||
round: 1,
|
||
tag: 0,
|
||
},
|
||
})
|
||
|
||
const textForm = useForm({
|
||
initialValues: {
|
||
question_name: "",
|
||
value: "",
|
||
round_id: 1,
|
||
},
|
||
})
|
||
|
||
const counts = useMemo(() => {
|
||
return countAllTurnsAndQaNumber(activeImage)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [activeImage, qaData, updateDescDataFlag, countAllTurnsAndQaNumber])
|
||
|
||
const [open, setOpen] = useState({
|
||
status: false,
|
||
label: "",
|
||
idx: -1,
|
||
isEdit: false,
|
||
})
|
||
|
||
// 新增问题
|
||
const onClickAdd = (label: string, index: number) => {
|
||
setOpen({ status: true, label, idx: index, isEdit: false })
|
||
}
|
||
|
||
// 编辑问题名称
|
||
const onClickEdit = (label: string, index: number, val: EditFormProps) => {
|
||
textForm.setValues({
|
||
question_name: val.question_name,
|
||
value: val.value || "",
|
||
round_id: val.round_id,
|
||
})
|
||
setOpen({ status: true, label, idx: index, isEdit: true })
|
||
}
|
||
|
||
type QaObj = Record<string, any>
|
||
|
||
const handleFormUpdate = (changeValues: any) => {
|
||
if (!selectedQuestion) return
|
||
const keys = Object.keys(changeValues)
|
||
const hasTagChange = keys.includes("tag")
|
||
const formData = form.values
|
||
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 = structuredClone(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
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}
|
||
|
||
const onFieldChange = (field: string, value: any) => {
|
||
form.setFieldValue(field, value)
|
||
handleFormUpdate({ [field]: value })
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!selectedQuestion) return
|
||
if (
|
||
selectedQuestion.type === "value" ||
|
||
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 obj = {
|
||
value: htmlText,
|
||
round: v.id,
|
||
tag: v.tag,
|
||
}
|
||
form.setValues(obj)
|
||
}
|
||
}, [selectedQuestion, form])
|
||
|
||
useEffect(() => {
|
||
if (updateDescDataFlag) {
|
||
updateFlag(false)
|
||
}
|
||
}, [updateDescDataFlag, updateFlag])
|
||
|
||
// 处理问题上移
|
||
const handleQuestionUp = (category: string, index: number) => {
|
||
const finalData = structuredClone(currentQaData)
|
||
let arr = finalData[category]
|
||
let temp = arr[index]
|
||
arr[index] = arr[index - 1]
|
||
arr[index - 1] = temp
|
||
finalData[category] = arr
|
||
qaData.set(activeImage, finalData)
|
||
setQaData(new Map(qaData))
|
||
updateFlag(true)
|
||
}
|
||
|
||
// 处理问题置顶
|
||
const handleQuestionUpToTop = (category: string, index: number) => {
|
||
const finalData = structuredClone(currentQaData)
|
||
let arr = finalData[category]
|
||
const element = arr.splice(index, 1)[0]
|
||
arr.unshift(element)
|
||
finalData[category] = arr
|
||
qaData.set(activeImage, finalData)
|
||
setQaData(new Map(qaData))
|
||
updateFlag(true)
|
||
}
|
||
|
||
// 错词检测
|
||
const checkWrongWords = useCallback(async () => {
|
||
if (useTopToolsStore.getState().isView) return
|
||
const res = await getWrongWordList()
|
||
const dictionary = res.data
|
||
const wrongWords = dictionary.map((item) => item.error)
|
||
const rightWords = dictionary.map((item) => item.correct)
|
||
// currentQaData
|
||
const currentImg = useBottomToolsStore.getState().activeImage
|
||
const qaData = useDescToolsStore.getState().qaData
|
||
const currentQaData = qaData.get(currentImg) ?? qaData.get("init") ?? {}
|
||
const newQaData = structuredClone(currentQaData)
|
||
|
||
const handleText = (v: string) => {
|
||
// 提取文本内容
|
||
const text = v.split(splitWord)[1]
|
||
let textContent = text.replace(/<[^>]+>/g, "")
|
||
// 替换错词
|
||
for (let i = 0; i < wrongWords.length; i++) {
|
||
const wrong = escapeRegExp(wrongWords[i])
|
||
const right = rightWords[i]
|
||
const regex = new RegExp(wrong, "g")
|
||
|
||
if (textContent.match(regex)) {
|
||
textContent = textContent.replace(
|
||
regex,
|
||
`<u style="color: red;" title="${right}">${wrong}</u>`
|
||
)
|
||
return textContent
|
||
}
|
||
}
|
||
return text
|
||
}
|
||
type QaObj = Record<string, any>
|
||
Object.values(newQaData).forEach((questionArr: QaObj[]) => {
|
||
questionArr.forEach((questionObj: QaObj) => {
|
||
const [name, questionData] = Object.entries(questionObj)[0]
|
||
const { value, comment } = questionData
|
||
|
||
const update = (v: string, key: "value" | "comment") => {
|
||
const checkValue = v.split(splitWord)[1]
|
||
const updatedValue = handleText(v)
|
||
|
||
if (checkValue !== updatedValue) {
|
||
let newValue = `<p>${updatedValue}</p>${splitWord}${updatedValue}`
|
||
questionData[key] = newValue
|
||
if (
|
||
selectedQuestion?.label === name &&
|
||
selectedQuestion?.type === key
|
||
)
|
||
form.setFieldValue("value", `<p>${updatedValue}</p>`)
|
||
}
|
||
}
|
||
|
||
if (value) update(value, "value")
|
||
if (comment) update(comment, "comment")
|
||
})
|
||
})
|
||
qaData.set(currentImg, newQaData)
|
||
setQaData(new Map(qaData)) // Force update
|
||
updateFlag(true)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [selectedQuestion, qaData, setQaData, updateFlag])
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
checkWrongWords,
|
||
}))
|
||
|
||
return (
|
||
<Box h="100%">
|
||
<Text
|
||
size="sm"
|
||
fw={600}
|
||
px="xs"
|
||
mb="xs">{`总轮数:${counts.turns}, 总问题数:${counts.question_size},新增单轮数:${counts.new_single_turns},新增多轮数:${counts.new_multiple_turns}`}</Text>
|
||
<Box
|
||
className="qa-tools-container"
|
||
h="calc(100% - 300px)"
|
||
style={{ overflowY: "auto" }}>
|
||
<Accordion multiple>
|
||
{Object.entries(currentQaData).map(([qaCategory, qaDataArr]) => {
|
||
const counts = useDescToolsStore
|
||
.getState()
|
||
.countTurnsAndQaNumber(activeImage, qaCategory)
|
||
return (
|
||
<Accordion.Item key={qaCategory} value={qaCategory}>
|
||
<Accordion.Control>
|
||
<Text
|
||
fw={
|
||
600
|
||
}>{`${qaCategory} 轮数:${counts.turns},问题数:${counts.question_size}`}</Text>
|
||
</Accordion.Control>
|
||
<Accordion.Panel>
|
||
<Stack gap="xs">
|
||
{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]
|
||
: ""
|
||
|
||
return (
|
||
<Box
|
||
key={`${qaCategory}${question_name}`}
|
||
p="xs"
|
||
style={{
|
||
borderRadius: "var(--mantine-radius-md)",
|
||
border:
|
||
selectedQuestion?.label === question_name
|
||
? "1px solid var(--mantine-color-blue-filled)"
|
||
: "1px solid transparent",
|
||
}}>
|
||
<Flex gap="xs" mb="xs">
|
||
<Flex align="center">
|
||
<Text>{question_name}</Text>
|
||
{!detail.is_pre && !isView ? (
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
ml="xs"
|
||
onClick={() => {
|
||
onClickEdit(qaCategory, index, {
|
||
question_name,
|
||
value: htmlText,
|
||
round_id: detail.id,
|
||
})
|
||
}}>
|
||
<IconEdit size={16} />
|
||
</ActionIcon>
|
||
) : null}
|
||
</Flex>
|
||
<Box>
|
||
{detail.tag === 2 ? (
|
||
<Text c="red">错误</Text>
|
||
) : detail.tag === 1 ? (
|
||
<Text c="green">正确</Text>
|
||
) : null}
|
||
</Box>
|
||
</Flex>
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
mb="xs">
|
||
<Text size="sm">轮次:{detail.id}</Text>
|
||
{!isView ? (
|
||
<Flex gap="xs">
|
||
{!detail.is_pre ? (
|
||
<Button
|
||
variant="subtle"
|
||
color="red"
|
||
size="compact-xs"
|
||
onClick={() => {
|
||
const finalData =
|
||
structuredClone(currentQaData)
|
||
const updateData =
|
||
structuredClone(qaDataArr)
|
||
updateData.splice(index, 1)
|
||
finalData[qaCategory] = updateData
|
||
qaData.set(activeImage, finalData)
|
||
setQaData(new Map(qaData))
|
||
updateFlag(true)
|
||
}}>
|
||
删除
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
onClick={() => {
|
||
onClickAdd(qaCategory, index)
|
||
}}>
|
||
添加
|
||
</Button>
|
||
{isReview && !detail.comment ? (
|
||
<Button
|
||
variant="subtle"
|
||
size="compact-xs"
|
||
onClick={() => {
|
||
const finalData =
|
||
structuredClone(currentQaData)
|
||
const updateData =
|
||
structuredClone(qaDataArr)
|
||
updateData[index] = {
|
||
[question_name]: {
|
||
...detail,
|
||
comment: "<p></p>",
|
||
},
|
||
}
|
||
finalData[qaCategory] = updateData
|
||
qaData.set(activeImage, finalData)
|
||
setQaData(new Map(qaData))
|
||
updateFlag(true)
|
||
// 更新下方选中
|
||
setSelectedQuestion({
|
||
label: question_name,
|
||
value: detail,
|
||
type: "comment",
|
||
category: qaCategory,
|
||
})
|
||
form.setValues({
|
||
value: "<p></p>",
|
||
round: detail.id,
|
||
tag: detail.tag,
|
||
})
|
||
}}>
|
||
批注
|
||
</Button>
|
||
) : null}
|
||
{index !== 0 ? (
|
||
<>
|
||
<Tooltip label="上移">
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
onClick={() => {
|
||
handleQuestionUp(qaCategory, index)
|
||
}}>
|
||
<ArrowUpIcon size={14} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
<Tooltip label="置顶">
|
||
<ActionIcon
|
||
variant="subtle"
|
||
size="sm"
|
||
onClick={() => {
|
||
handleQuestionUpToTop(
|
||
qaCategory,
|
||
index
|
||
)
|
||
}}>
|
||
<ArrowUpToLineIcon size={14} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
</>
|
||
) : null}
|
||
</Flex>
|
||
) : null}
|
||
</Flex>
|
||
<Box>
|
||
<Box
|
||
p="xs"
|
||
mb="xs"
|
||
style={{
|
||
border: !detail.is_pre
|
||
? "1px solid var(--mantine-color-green-filled)"
|
||
: "1px solid var(--mantine-color-gray-3)",
|
||
borderRadius: "var(--mantine-radius-md)",
|
||
cursor: "pointer",
|
||
}}
|
||
onClick={() => {
|
||
setSelectedQuestion({
|
||
label: question_name,
|
||
value: detail,
|
||
type: "value",
|
||
category: qaCategory,
|
||
})
|
||
console.log("点击批注", htmlText)
|
||
form.setValues({
|
||
value: htmlText,
|
||
round: detail.id,
|
||
tag: detail.tag,
|
||
})
|
||
}}>
|
||
{parse(htmlText)}
|
||
</Box>
|
||
{commentHtmlText ? (
|
||
<Box
|
||
p="xs"
|
||
style={{
|
||
border:
|
||
"1px solid var(--mantine-color-red-filled)",
|
||
borderRadius: "var(--mantine-radius-md)",
|
||
cursor: "pointer",
|
||
}}
|
||
onClick={() => {
|
||
setSelectedQuestion({
|
||
label: question_name,
|
||
value: detail,
|
||
type: "comment",
|
||
category: qaCategory,
|
||
})
|
||
form.setValues({
|
||
value: commentHtmlText,
|
||
round: detail.id,
|
||
tag: detail.tag,
|
||
})
|
||
}}>
|
||
{parse(commentHtmlText)}
|
||
</Box>
|
||
) : null}
|
||
</Box>
|
||
</Box>
|
||
)
|
||
})}
|
||
</Stack>
|
||
</Accordion.Panel>
|
||
</Accordion.Item>
|
||
)
|
||
})}
|
||
</Accordion>
|
||
</Box>
|
||
<Box
|
||
style={{ borderTop: "2px solid var(--mantine-color-gray-3)" }}
|
||
pt="sm"
|
||
px="sm">
|
||
<Stack
|
||
gap="xs"
|
||
opacity={!isView ? 1 : 0.5}
|
||
style={{ pointerEvents: !isView ? "auto" : "none" }}>
|
||
<Flex align="center" gap="xs">
|
||
<Text fw={600}>问题</Text>
|
||
<Text>{selectedQuestion?.label || ""}</Text>
|
||
</Flex>
|
||
<Flex align="center" gap="xs">
|
||
<Text>轮次</Text>
|
||
<NumberInput
|
||
size="xs"
|
||
min={1}
|
||
{...form.getInputProps("round")}
|
||
onChange={(val) => onFieldChange("round", val)}
|
||
disabled={
|
||
!selectedQuestion ||
|
||
(selectedQuestion && selectedQuestion.value?.is_pre)
|
||
}
|
||
/>
|
||
<Radio.Group
|
||
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>
|
||
<EditorContainer
|
||
value={form.values.value}
|
||
onChange={(val: any) => onFieldChange("value", val)}
|
||
disabled={!selectedQuestion}
|
||
/>
|
||
</Stack>
|
||
</Box>
|
||
<CustomModal
|
||
title={open.isEdit ? "编辑问题" : "添加问题"}
|
||
centered
|
||
open={open.status}
|
||
onOk={async () => {
|
||
const currentOperation = currentQaData[open.label]
|
||
// Validate
|
||
const { hasErrors } = textForm.validate()
|
||
if (hasErrors) return
|
||
|
||
// Duplicate check
|
||
const formData = textForm.values
|
||
const arr = useDescToolsStore
|
||
.getState()
|
||
.getQuestionList(activeImage)
|
||
// If adding new, check duplicates
|
||
if (!open.isEdit && arr.includes(formData.question_name)) {
|
||
textForm.setFieldError("question_name", "问题名称不能重复!")
|
||
return
|
||
}
|
||
// If editing, if name changed and new name exists, error
|
||
if (open.isEdit) {
|
||
const originalName = Object.keys(currentOperation[open.idx])[0]
|
||
if (
|
||
originalName !== formData.question_name &&
|
||
arr.includes(formData.question_name)
|
||
) {
|
||
textForm.setFieldError("question_name", "问题名称不能重复!")
|
||
return
|
||
}
|
||
}
|
||
|
||
const finalData = structuredClone(currentQaData)
|
||
const updateData = structuredClone(currentOperation)
|
||
let obj = {
|
||
value: formData.value ?? "<p></p>",
|
||
id: formData.round_id,
|
||
is_pre: false,
|
||
tag: 0,
|
||
uid: 0,
|
||
create_timestamp: 0,
|
||
modify_uid: 0,
|
||
modify_timestamp: 0,
|
||
comment: "",
|
||
flag: 0,
|
||
}
|
||
if (open.isEdit) {
|
||
const prevData = Object.values(currentOperation[open.idx])[0]
|
||
obj.uid = prevData.uid
|
||
obj.create_timestamp = prevData.create_timestamp
|
||
obj.modify_uid = usePermissionStore.getState().user_id
|
||
obj.modify_timestamp = dayjs().unix()
|
||
obj.comment = prevData.comment
|
||
obj.flag = prevData.flag
|
||
updateData[open.idx] = { [formData.question_name]: obj }
|
||
} else {
|
||
obj.uid = usePermissionStore.getState().user_id
|
||
obj.create_timestamp = dayjs().unix()
|
||
updateData.splice(open.idx + 1, 0, {
|
||
[formData.question_name]: obj,
|
||
})
|
||
}
|
||
finalData[open.label] = updateData
|
||
qaData.set(activeImage, finalData)
|
||
setQaData(new Map(qaData))
|
||
updateFlag(true)
|
||
setOpen({ status: false, label: "", idx: -1, isEdit: false })
|
||
textForm.reset()
|
||
}}
|
||
onCancel={() => {
|
||
textForm.reset()
|
||
setOpen({ status: false, label: "", idx: -1, isEdit: false })
|
||
}}>
|
||
<Stack gap="sm">
|
||
<TextInput
|
||
label="问题名称"
|
||
required
|
||
{...textForm.getInputProps("question_name")}
|
||
onFocus={() => {
|
||
useKeyEventStore.getState().setFocusInput(true)
|
||
}}
|
||
onBlur={() => {
|
||
useKeyEventStore.getState().setFocusInput(false)
|
||
}}
|
||
/>
|
||
<Box>
|
||
<Text size="sm" fw={500}>
|
||
值
|
||
</Text>
|
||
<EditorContainer
|
||
value={textForm.values.value}
|
||
onChange={(val: any) => textForm.setFieldValue("value", val)}
|
||
/>
|
||
</Box>
|
||
<NumberInput
|
||
label="轮次"
|
||
required
|
||
min={1}
|
||
disabled={open.isEdit}
|
||
{...textForm.getInputProps("round_id")}
|
||
/>
|
||
</Stack>
|
||
</CustomModal>
|
||
</Box>
|
||
)
|
||
}
|
||
)
|
||
|
||
const RightQATools = (
|
||
{ taskDetail }: ComponentProps,
|
||
ref: React.Ref<unknown>
|
||
) => {
|
||
const { activeImage } = useBottomToolsStore()
|
||
return <RightQAContent key={activeImage} taskDetail={taskDetail} ref={ref} />
|
||
}
|
||
|
||
export default forwardRef(RightQATools)
|