fix(style): fix

This commit is contained in:
zhangheng
2026-03-28 15:23:23 +08:00
parent 28660de4ed
commit 27d47b5692
3 changed files with 271 additions and 96 deletions

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

@@ -62,14 +62,11 @@ 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 = value ? 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])

View File

@@ -9,6 +9,7 @@ import {
Group, Group,
NumberInput, NumberInput,
Radio, Radio,
ScrollArea,
Stack, Stack,
Text, Text,
TextInput, TextInput,
@@ -22,10 +23,12 @@ import { ArrowUpIcon, ArrowUpToLineIcon } from "lucide-react"
import dynamic from "next/dynamic" 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"
@@ -78,6 +81,46 @@ const ensureStoredValue = (value?: string) => {
return `${value}${splitWord}${getTextFromHtml(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()
@@ -112,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,
@@ -121,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 || "",
@@ -140,7 +187,8 @@ const RightQAContent = forwardRef(
const handleFormUpdate = ( const handleFormUpdate = (
changeValues: Record<string, any>, changeValues: Record<string, any>,
rawEditorValue?: string rawEditorValue?: string,
options?: { notifyFlag?: boolean }
) => { ) => {
if (!selectedQuestion) return if (!selectedQuestion) return
const keys = Object.keys(changeValues) const keys = Object.keys(changeValues)
@@ -154,53 +202,84 @@ const RightQAContent = forwardRef(
? ensureStoredValue(rawEditorValue) ? ensureStoredValue(rawEditorValue)
: ensureStoredValue(formData.value) : 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" ? 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 {
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 flushPendingValueSync = () => {
if (!valueSyncTimerRef.current) return
clearTimeout(valueSyncTimerRef.current)
valueSyncTimerRef.current = null
handleFormUpdate({ value: form.values.value }, form.values.value)
}
const onFieldChange = ( const onFieldChange = (
field: string, field: string,
value: any, value: any,
rawEditorValue?: string rawEditorValue?: string
) => { ) => {
form.setFieldValue(field, value) form.setFieldValue(field, value)
handleFormUpdate({ [field]: value }, rawEditorValue) 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 (
@@ -326,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
@@ -345,8 +427,9 @@ 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>
@@ -357,6 +440,9 @@ const RightQAContent = forwardRef(
const commentHtmlText = getHtmlFromStoredValue( const commentHtmlText = getHtmlFromStoredValue(
detail.comment detail.comment
) )
const hasValueContent = hasPreviewContent(htmlText)
const hasCommentContent =
hasPreviewContent(commentHtmlText)
return ( return (
<Box <Box
@@ -371,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"
@@ -390,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>
@@ -400,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 ? (
@@ -503,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
@@ -528,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,
@@ -542,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>
@@ -555,49 +659,84 @@ 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: string) => onChange={(val: string) =>