feat: 更新 ESLint 和 Prettier 配置,增强代码质量管理

- 在 .gitignore 中添加 public/audio/* 以忽略音频文件。
- 更新 bun.lock,添加 eslint-config-prettier 和 eslint-plugin-prettier 依赖,确保 ESLint 与 Prettier 的兼容性。
- 修改 eslint.config.ts,整合 Prettier 规则,优化代码风格一致性。
- 在 package.json 中添加 lint:fix 和 format 脚本,简化代码格式化和修复流程。
- 更新 tailwind.config.ts 和其他组件文件,统一代码风格,提升可读性。
This commit is contained in:
zhongyidian committed 2026-03-05 13:50:47 +08:00
1 parent 5d00adc1a5
commit 76c67b492e
216 files changed
+12454 -13976

No files matched your search

+31 -44
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react";
import { DragFill } from "@/components";
import { cn } from "@/lib/utils";
import type { DragQuizPanelProps } from "./types";
import { useEffect, useRef } from 'react'
import { DragFill } from '@/components'
import { cn } from '@/lib/utils'
import type { DragQuizPanelProps } from './types'
/** 拖拽题面板:使用 DragFill 组件 */
const DragQuizPanel = ({
@@ -18,25 +18,22 @@ const DragQuizPanel = ({
onChange,
}: DragQuizPanelProps) => {
// 跟踪之前的 visible 状态
const prevVisibleRef = useRef(visible);
const prevVisibleRef = useRef(visible)
// 当窗口关闭时,清除拖拽状态
useEffect(() => {
// 如果从可见变为不可见,清除拖拽状态
if (prevVisibleRef.current && !visible && value.length > 0) {
onChange([]);
onChange([])
}
prevVisibleRef.current = visible;
prevVisibleRef.current = visible
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
}, [visible])
if (!visible) return null;
if (!visible) return null
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
<div className="relative w-[1728px] h-[972px] flex flex-col items-center pt-12 pb-14">
{/* 题干卡片 */}
<div className="w-[1700px] rounded-[32px] bg-black/90 px-20 py-12 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
@@ -46,18 +43,18 @@ const DragQuizPanel = ({
<div>
{questionPrefix && <span>{questionPrefix}</span>}
{blanks.map((blank, index) => {
const filledResult = value.find((r) => r.blankId === blank.id);
const filledValue = filledResult?.optionValue || null;
const filledResult = value.find((r) => r.blankId === blank.id)
const filledValue = filledResult?.optionValue || null
return (
<span key={blank.id}>
<span
data-blank-id={blank.id}
className={cn(
"inline-flex items-center justify-center min-w-[140px] h-[56px] px-6 mx-3",
"rounded-xl border-2 transition-all duration-300 shadow-[0_4px_16px_rgba(0,0,0,0.2)]",
'inline-flex items-center justify-center min-w-[140px] h-[56px] px-6 mx-3',
'rounded-xl border-2 transition-all duration-300 shadow-[0_4px_16px_rgba(0,0,0,0.2)]',
filledValue
? "border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] backdrop-blur-sm"
: "border-[#d4c5a9]/60 bg-white/10 backdrop-blur-sm hover:border-[#d4c5a9]"
? 'border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] backdrop-blur-sm'
: 'border-[#d4c5a9]/60 bg-white/10 backdrop-blur-sm hover:border-[#d4c5a9]'
)}
>
{filledValue ? (
@@ -67,38 +64,28 @@ const DragQuizPanel = ({
type="button"
className="w-7 h-7 flex items-center justify-center rounded-full bg-[#8b4513]/80 hover:bg-[#a0522d] text-white text-sm transition-all hover:scale-110 shadow-lg"
onClick={(e) => {
e.stopPropagation();
onChange(
value.filter((r) => r.blankId !== blank.id)
);
e.stopPropagation()
onChange(value.filter((r) => r.blankId !== blank.id))
}}
>
×
</button>
</span>
) : (
<span className="text-white/40 text-[24px] font-light">
____
</span>
<span className="text-white/40 text-[24px] font-light">____</span>
)}
</span>
{index < blanks.length - 1 && (
<span className="text-white/80 mx-1">
{blankSeparators && blankSeparators[index]
? blankSeparators[index]
: "、"}
{blankSeparators && blankSeparators[index] ? blankSeparators[index] : '、'}
</span>
)}
</span>
);
)
})}
{questionSuffix && <span>{questionSuffix}</span>}
{/* 答案反馈 */}
{error && (
<div className="mt-5 text-center text-[26px] text-[#fcbe53]">
{error}
</div>
)}
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
</div>
</div>
</div>
@@ -117,12 +104,12 @@ const DragQuizPanel = ({
renderOption={(option, isUsed) => (
<div
className={cn(
"relative h-[110px] px-10 flex items-center justify-center",
"rounded-[32px] transition-all duration-300",
"text-[32px] font-bold border-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)]",
'relative h-[110px] px-10 flex items-center justify-center',
'rounded-[32px] transition-all duration-300',
'text-[32px] font-bold border-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)]',
isUsed
? "border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50"
: "border-[#d4c5a9] bg-gradient-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3] cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)] hover:scale-[1.02]"
? 'border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50'
: 'border-[#d4c5a9] bg-gradient-to-br from-[#4a3f35] to-[#3a2f25] text-[#f5e6d3] cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)] hover:scale-[1.02]'
)}
>
{!isUsed && (
@@ -136,13 +123,13 @@ const DragQuizPanel = ({
renderBlank={(_blank) => {
// 返回一个隐藏元素,实际的空白区域在题干中
// 但需要通过 data-blank-id 让 DragFill 能找到它
return <div className="hidden" />;
return <div className="hidden" />
}}
/>
</div>
</div>
</div>
);
};
)
}
export default DragQuizPanel;
export default DragQuizPanel
+115 -130
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import type { MatchQuizPanelProps } from "./types";
import { useState, useEffect, useRef } from 'react'
import { cn } from '@/lib/utils'
import type { MatchQuizPanelProps } from './types'
/**
* 连线题面板组件
@@ -41,153 +41,141 @@ const MatchQuizPanel = ({
onError,
disabled = false,
}: MatchQuizPanelProps) => {
const [matches, setMatches] = useState<Array<{ leftId: string; rightId: string }>>([]);
const [selectedLeftId, setSelectedLeftId] = useState<string | null>(null);
const [error, setError] = useState<string>("");
const containerRef = useRef<HTMLDivElement>(null);
const [matches, setMatches] = useState<Array<{ leftId: string; rightId: string }>>([])
const [selectedLeftId, setSelectedLeftId] = useState<string | null>(null)
const [error, setError] = useState<string>('')
const containerRef = useRef<HTMLDivElement>(null)
// 当前错误的连线对,用于做红色高亮+抖动反馈
const [errorPairs, setErrorPairs] = useState<
Array<{ leftId: string; rightId: string }>
>([]);
const [errorPairs, setErrorPairs] = useState<Array<{ leftId: string; rightId: string }>>([])
// 当窗口关闭时,重置状态
useEffect(() => {
if (!visible) {
setMatches([]);
setErrorPairs([]);
setSelectedLeftId(null);
setError("");
setMatches([])
setErrorPairs([])
setSelectedLeftId(null)
setError('')
}
}, [visible]);
}, [visible])
// 处理左列项点击
const handleLeftItemClick = (leftId: string) => {
if (disabled) return;
if (disabled) return
// 如果已经匹配过,先取消匹配
const existingMatch = matches.find((m) => m.leftId === leftId);
const existingMatch = matches.find((m) => m.leftId === leftId)
if (existingMatch) {
setMatches(matches.filter((m) => m.leftId !== leftId));
setSelectedLeftId(null);
setError("");
return;
setMatches(matches.filter((m) => m.leftId !== leftId))
setSelectedLeftId(null)
setError('')
return
}
// 如果已经选中了另一个左项,取消之前的选中
if (selectedLeftId && selectedLeftId !== leftId) {
setSelectedLeftId(leftId);
setError("");
return;
setSelectedLeftId(leftId)
setError('')
return
}
// 切换选中状态
setSelectedLeftId(selectedLeftId === leftId ? null : leftId);
setError("");
};
setSelectedLeftId(selectedLeftId === leftId ? null : leftId)
setError('')
}
// 处理右列项点击
const handleRightItemClick = (rightId: string) => {
if (disabled) return;
if (disabled) return
// 如果没有选中左项,直接返回
if (!selectedLeftId) return;
if (!selectedLeftId) return
// 如果已经匹配过,先取消匹配
const existingMatch = matches.find((m) => m.rightId === rightId);
const existingMatch = matches.find((m) => m.rightId === rightId)
if (existingMatch) {
setMatches(matches.filter((m) => m.rightId !== rightId));
setSelectedLeftId(null);
setError("");
return;
setMatches(matches.filter((m) => m.rightId !== rightId))
setSelectedLeftId(null)
setError('')
return
}
// 创建新的匹配
const newMatch = { leftId: selectedLeftId, rightId };
const newMatches = [...matches, newMatch];
const newMatch = { leftId: selectedLeftId, rightId }
const newMatches = [...matches, newMatch]
// 每次连线后立即校验当前所有连线是否存在错误
const wrongMatches =
correctMatches?.filter(
() => true
) && // 保证 correctMatches 存在时才校验
correctMatches?.filter(() => true) && // 保证 correctMatches 存在时才校验
newMatches.filter(
(match) =>
!correctMatches?.some(
(cm) => cm.leftId === match.leftId && cm.rightId === match.rightId
)
);
!correctMatches?.some((cm) => cm.leftId === match.leftId && cm.rightId === match.rightId)
)
const hasError = !!wrongMatches && wrongMatches.length > 0;
const hasError = !!wrongMatches && wrongMatches.length > 0
if (hasError && wrongMatches) {
// 记录错误连线对,用于红色高亮+抖动反馈
setErrorPairs(wrongMatches);
setMatches(newMatches);
setSelectedLeftId(null);
setError("");
setErrorPairs(wrongMatches)
setMatches(newMatches)
setSelectedLeftId(null)
setError('')
let errorMsg: string;
if (typeof errorMessage === "function") {
errorMsg = errorMessage(newMatches);
let errorMsg: string
if (typeof errorMessage === 'function') {
errorMsg = errorMessage(newMatches)
} else if (errorMessage) {
errorMsg = errorMessage;
errorMsg = errorMessage
} else {
errorMsg = "连线不正确,请重新尝试!";
errorMsg = '连线不正确,请重新尝试!'
}
setError(errorMsg);
onError?.(errorMsg);
setError(errorMsg)
onError?.(errorMsg)
// 短暂展示错误状态(变红+抖动),随后只清空错误连线,保留已答对的连线
setTimeout(() => {
setMatches((prev) =>
prev.filter(
(m) =>
!wrongMatches.some(
(w) => w.leftId === m.leftId && w.rightId === m.rightId
)
(m) => !wrongMatches.some((w) => w.leftId === m.leftId && w.rightId === m.rightId)
)
);
setErrorPairs([]);
}, 600);
return;
)
setErrorPairs([])
}, 600)
return
}
// 没有错误时,正常记录匹配
setMatches(newMatches);
setSelectedLeftId(null);
setError("");
setMatches(newMatches)
setSelectedLeftId(null)
setError('')
// 如果没有错误且所有项都已连完,则认为答题成功
if (
newMatches.length === leftItems.length &&
newMatches.length === rightItems.length
) {
onSubmit?.(newMatches, true);
if (newMatches.length === leftItems.length && newMatches.length === rightItems.length) {
onSubmit?.(newMatches, true)
}
};
}
// 获取左项是否已匹配
const isLeftItemMatched = (leftId: string) => {
return matches.some((m) => m.leftId === leftId);
};
return matches.some((m) => m.leftId === leftId)
}
// 获取右项是否已匹配
const isRightItemMatched = (rightId: string) => {
return matches.some((m) => m.rightId === rightId);
};
return matches.some((m) => m.rightId === rightId)
}
// 获取左项对应的右项ID
const getMatchedRightId = (leftId: string) => {
const match = matches.find((m) => m.leftId === leftId);
return match?.rightId || null;
};
const match = matches.find((m) => m.leftId === leftId)
return match?.rightId || null
}
// 获取右项对应的左项ID
const getMatchedLeftId = (rightId: string) => {
const match = matches.find((m) => m.rightId === rightId);
return match?.leftId || null;
};
const match = matches.find((m) => m.rightId === rightId)
return match?.leftId || null
}
if (!visible) return null;
if (!visible) return null
return (
<div
@@ -215,35 +203,33 @@ const MatchQuizPanel = ({
{/* 左列:日期 */}
<div className="w-full md:w-1/2 flex flex-col gap-3 md:gap-4">
{leftItems.map((item) => {
const isMatched = isLeftItemMatched(item.id);
const isSelected = selectedLeftId === item.id;
const isError = errorPairs.some((p) => p.leftId === item.id);
const isMatched = isLeftItemMatched(item.id)
const isSelected = selectedLeftId === item.id
const isError = errorPairs.some((p) => p.leftId === item.id)
return (
<div
key={item.id}
className={cn(
"relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200",
"text-[20px] md:text-[28px] leading-relaxed cursor-pointer",
"touch-manipulation", // 优化移动端触摸
disabled || isMatched
? "opacity-60 cursor-not-allowed"
: "cursor-pointer",
'relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200',
'text-[20px] md:text-[28px] leading-relaxed cursor-pointer',
'touch-manipulation', // 优化移动端触摸
disabled || isMatched ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
isError
? "bg-[#fbe9e9] text-[#b91c1c] border-[#fca5a5] shadow-[0_0_15px_rgba(248,113,113,0.7)] scale-105 animate-pulse"
? 'bg-[#fbe9e9] text-[#b91c1c] border-[#fca5a5] shadow-[0_0_15px_rgba(248,113,113,0.7)] scale-105 animate-pulse'
: isSelected
? "bg-[#d4a574] text-[#5c3e2e] border-[#f0be75] shadow-lg scale-105"
: isMatched
? "bg-[#3a3020] text-[#d4c5a9] border-[#8b7355]"
: "bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]"
? 'bg-[#d4a574] text-[#5c3e2e] border-[#f0be75] shadow-lg scale-105'
: isMatched
? 'bg-[#3a3020] text-[#d4c5a9] border-[#8b7355]'
: 'bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
)}
onClick={() => handleLeftItemClick(item.id)}
onTouchStart={(e) => {
// 移动端触摸优化
e.currentTarget.classList.add("scale-105");
e.currentTarget.classList.add('scale-105')
}}
onTouchEnd={(e) => {
e.currentTarget.classList.remove("scale-105");
e.currentTarget.classList.remove('scale-105')
}}
>
{item.label}
@@ -258,7 +244,7 @@ const MatchQuizPanel = ({
</div>
)}
</div>
);
)
})}
</div>
@@ -267,18 +253,18 @@ const MatchQuizPanel = ({
{/* 绘制连线 */}
<svg className="absolute inset-0 w-full h-full pointer-events-none overflow-visible">
{matches.map((match, index) => {
const leftItem = leftItems.find((item) => item.id === match.leftId);
const rightItem = rightItems.find((item) => item.id === match.rightId);
if (!leftItem || !rightItem) return null;
const leftItem = leftItems.find((item) => item.id === match.leftId)
const rightItem = rightItems.find((item) => item.id === match.rightId)
if (!leftItem || !rightItem) return null
const leftIndex = leftItems.findIndex((item) => item.id === match.leftId);
const rightIndex = rightItems.findIndex((item) => item.id === match.rightId);
const leftIndex = leftItems.findIndex((item) => item.id === match.leftId)
const rightIndex = rightItems.findIndex((item) => item.id === match.rightId)
// 计算连线起点和终点(基于项目高度和间距)
const itemHeight = 80; // 每个项目的大概高度
const gap = 16; // 项目之间的间距
const startY = leftIndex * (itemHeight + gap) + itemHeight / 2;
const endY = rightIndex * (itemHeight + gap) + itemHeight / 2;
const itemHeight = 80 // 每个项目的大概高度
const gap = 16 // 项目之间的间距
const startY = leftIndex * (itemHeight + gap) + itemHeight / 2
const endY = rightIndex * (itemHeight + gap) + itemHeight / 2
return (
<line
@@ -292,7 +278,7 @@ const MatchQuizPanel = ({
strokeDasharray="5,5"
markerEnd="url(#arrowhead)"
/>
);
)
})}
{/* 箭头标记定义 */}
<defs>
@@ -313,37 +299,37 @@ const MatchQuizPanel = ({
{/* 右列:事件 */}
<div className="w-full md:w-1/2 flex flex-col gap-3 md:gap-4">
{rightItems.map((item) => {
const isMatched = isRightItemMatched(item.id);
const isError = errorPairs.some((p) => p.rightId === item.id);
const isMatched = isRightItemMatched(item.id)
const isError = errorPairs.some((p) => p.rightId === item.id)
return (
<div
key={item.id}
className={cn(
"relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200",
"text-[20px] md:text-[28px] leading-relaxed",
"touch-manipulation", // 优化移动端触摸
'relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200',
'text-[20px] md:text-[28px] leading-relaxed',
'touch-manipulation', // 优化移动端触摸
disabled || isMatched
? "opacity-60 cursor-not-allowed"
? 'opacity-60 cursor-not-allowed'
: selectedLeftId
? "cursor-pointer"
: "cursor-default",
? 'cursor-pointer'
: 'cursor-default',
isError
? "bg-[#fbe9e9] text-[#b91c1c] border-[#fca5a5] shadow-[0_0_15px_rgba(248,113,113,0.7)] scale-105 animate-pulse"
? 'bg-[#fbe9e9] text-[#b91c1c] border-[#fca5a5] shadow-[0_0_15px_rgba(248,113,113,0.7)] scale-105 animate-pulse'
: isMatched
? "bg-[#3a3020] text-[#d4c5a9] border-[#8b7355]"
: selectedLeftId
? "bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]"
: "bg-[#d7b888]/60 text-[#5c3e2e]/60 border-[#d4c5a9]/60"
? 'bg-[#3a3020] text-[#d4c5a9] border-[#8b7355]'
: selectedLeftId
? 'bg-[#d7b888] text-[#5c3e2e] border-[#d4c5a9] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
: 'bg-[#d7b888]/60 text-[#5c3e2e]/60 border-[#d4c5a9]/60'
)}
onClick={() => handleRightItemClick(item.id)}
onTouchStart={(e) => {
if (selectedLeftId) {
e.currentTarget.classList.add("scale-105");
e.currentTarget.classList.add('scale-105')
}
}}
onTouchEnd={(e) => {
e.currentTarget.classList.remove("scale-105");
e.currentTarget.classList.remove('scale-105')
}}
>
{item.label}
@@ -358,7 +344,7 @@ const MatchQuizPanel = ({
</div>
)}
</div>
);
)
})}
</div>
</div>
@@ -369,8 +355,7 @@ const MatchQuizPanel = ({
</div>
</div>
</div>
);
};
export default MatchQuizPanel;
)
}
export default MatchQuizPanel
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { MultipleChoice } from "@/components";
import type { MultipleChoiceQuizPanelProps } from "./types";
import { useState, useEffect } from 'react'
import { MultipleChoice } from '@/components'
import type { MultipleChoiceQuizPanelProps } from './types'
/**
* 多选题面板组件
@@ -44,94 +44,85 @@ const MultipleChoiceQuizPanel = ({
disabled = false,
minSelections,
maxSelections,
submitText = "提交",
submitText = '提交',
}: MultipleChoiceQuizPanelProps) => {
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [error, setError] = useState<string>("");
const [selectedValues, setSelectedValues] = useState<string[]>([])
const [error, setError] = useState<string>('')
// 当窗口关闭时,清除选中状态和错误状态
useEffect(() => {
if (!visible) {
setSelectedValues([]);
setError("");
setSelectedValues([])
setError('')
}
}, [visible]);
}, [visible])
if (!visible) return null;
if (!visible) return null
// 处理选项点击
const handleOptionChange = (values: string[]) => {
if (disabled) return;
setSelectedValues(values);
setError("");
};
if (disabled) return
setSelectedValues(values)
setError('')
}
// 处理提交
const handleSubmit = () => {
if (disabled) return;
if (disabled) return
// 检查最小选择数量
if (minSelections !== undefined && selectedValues.length < minSelections) {
const errorMsg =
minSelectionsErrorMessage || `请至少选择 ${minSelections} 项`;
setError(errorMsg);
onError?.(errorMsg);
return;
const errorMsg = minSelectionsErrorMessage || `请至少选择 ${minSelections} 项`
setError(errorMsg)
onError?.(errorMsg)
return
}
// 验证答案
let isCorrect: boolean | undefined = undefined;
let isCorrect: boolean | undefined = undefined
if (correctAnswer !== undefined) {
// 比较两个数组是否相等(不考虑顺序)
const correctSet = new Set(correctAnswer);
const selectedSet = new Set(selectedValues);
const correctSet = new Set(correctAnswer)
const selectedSet = new Set(selectedValues)
isCorrect =
correctSet.size === selectedSet.size &&
[...correctSet].every((val) => selectedSet.has(val));
correctSet.size === selectedSet.size && [...correctSet].every((val) => selectedSet.has(val))
if (isCorrect) {
// 答题成功:清空状态,执行 onSubmit(外部控制关闭窗口)
const submittedValues = [...selectedValues];
setSelectedValues([]);
setError("");
onSubmit?.(submittedValues, true);
const submittedValues = [...selectedValues]
setSelectedValues([])
setError('')
onSubmit?.(submittedValues, true)
} else {
// 答题失败:清空状态,执行 onError,允许重新选择
const submittedValues = [...selectedValues];
setSelectedValues([]);
const submittedValues = [...selectedValues]
setSelectedValues([])
const errorMsg =
typeof errorMessage === "function"
typeof errorMessage === 'function'
? errorMessage(submittedValues)
: errorMessage || "很抱歉,你选错了!请你再思考一下吧!";
setError(errorMsg);
onError?.(errorMsg);
: errorMessage || '很抱歉,你选错了!请你再思考一下吧!'
setError(errorMsg)
onError?.(errorMsg)
}
} else {
// 没有提供正确答案,直接提交
const submittedValues = [...selectedValues];
setSelectedValues([]);
setError("");
onSubmit?.(submittedValues, true);
const submittedValues = [...selectedValues]
setSelectedValues([])
setError('')
onSubmit?.(submittedValues, true)
}
};
}
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
<div className="relative w-[1728px] flex flex-col items-center py-6">
{/* 题干卡片 */}
<div className="w-[1700px] rounded-[32px] bg-black px-20 py-12 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9] flex-shrink-0">
<div className="text-[34px] leading-relaxed text-white">{title}</div>
{/* 答案反馈 */}
{error && (
<div className="mt-5 text-center text-[26px] text-[#fcbe53]">
{error}
</div>
)}
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
</div>
{/* 选项区域 - 可滚动 */}
@@ -149,56 +140,48 @@ const MultipleChoiceQuizPanel = ({
disabled ||
(!isSelected &&
maxSelections !== undefined &&
selectedValues.length >= maxSelections);
selectedValues.length >= maxSelections)
// 处理选项点击
const handleClick = () => {
if (isDisabled) return;
if (isDisabled) return
let newValues: string[];
let newValues: string[]
if (isSelected) {
// 取消选择
newValues = selectedValues.filter((v) => v !== option.value);
newValues = selectedValues.filter((v) => v !== option.value)
// 检查最小选择数量限制
if (
minSelections !== undefined &&
newValues.length < minSelections
) {
return;
if (minSelections !== undefined && newValues.length < minSelections) {
return
}
} else {
// 选择
// 检查最大选择数量限制
if (
maxSelections !== undefined &&
selectedValues.length >= maxSelections
) {
return;
if (maxSelections !== undefined && selectedValues.length >= maxSelections) {
return
}
newValues = [...selectedValues, option.value];
newValues = [...selectedValues, option.value]
}
handleOptionChange(newValues);
};
handleOptionChange(newValues)
}
return (
<div key={option.value} className="mb-4">
<div
className={`w-[1500px] flex items-center p-4 text-[28px] leading-relaxed rounded-tr-[50px] rounded-bl-4xl transition-all duration-150 ${
isDisabled
? "opacity-60 cursor-not-allowed"
: "cursor-pointer"
isDisabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'
} ${
isSelected
? "bg-[#d4a574] text-[#5c3e2e] bg-gradient-to-r from-[#ddf3e1] to-[#fbdd97]"
: "bg-[#d7b888] text-[#5c3e2e] hover:brightness-110"
? 'bg-[#d4a574] text-[#5c3e2e] bg-gradient-to-r from-[#ddf3e1] to-[#fbdd97]'
: 'bg-[#d7b888] text-[#5c3e2e] hover:brightness-110'
}`}
onClick={handleClick}
>
{option.label}
</div>
</div>
);
)
}}
/>
</div>
@@ -212,8 +195,7 @@ const MultipleChoiceQuizPanel = ({
disabled={
disabled ||
selectedValues.length === 0 ||
(minSelections !== undefined &&
selectedValues.length < minSelections)
(minSelections !== undefined && selectedValues.length < minSelections)
}
className="px-30 py-4 text-[28px] font-medium text-white bg-[#c2995f] rounded-[40px] transition-all duration-200 hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
>
@@ -223,7 +205,7 @@ const MultipleChoiceQuizPanel = ({
</div>
</div>
</div>
);
};
)
}
export default MultipleChoiceQuizPanel;
export default MultipleChoiceQuizPanel
+19 -29
View File
@@ -1,6 +1,6 @@
import { SingleChoice } from "@/components";
import { getBackgroundImage } from "@/lib/utils";
import type { QuizPanelProps } from "./types";
import { SingleChoice } from '@/components'
import { getBackgroundImage } from '@/lib/utils'
import type { QuizPanelProps } from './types'
/** 单选题展示面板:题干使用“底板”背景,点击即选,hover 高亮 */
const QuizPanel = ({
@@ -12,13 +12,10 @@ const QuizPanel = ({
error,
onChange,
}: QuizPanelProps) => {
if (!visible) return null;
if (!visible) return null
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
<div className="relative w-[1728px] h-[972px] flex flex-col items-center justify-center">
{/* 题干卡片 */}
<div className="w-[1600px] min-h-[260px] rounded-[32px] bg-black px-20 py-14 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
@@ -40,24 +37,21 @@ const QuizPanel = ({
className="w-[1431px] h-[156px] flex items-center px-16 text-[28px] leading-snug text-[#fdf5e6] bg-cover bg-center bg-no-repeat transition-all duration-150"
style={{
backgroundImage: getBackgroundImage(
isSelected
? "components/quiz/选中"
: "components/quiz/未选择"
isSelected ? 'components/quiz/选中' : 'components/quiz/未选择'
),
filter: isSelected ? undefined : "brightness(0.96)",
filter: isSelected ? undefined : 'brightness(0.96)',
}}
onMouseEnter={(e) => {
if (isSelected) return;
(e.currentTarget as HTMLDivElement).style.backgroundImage =
getBackgroundImage("components/quiz/选中");
(e.currentTarget as HTMLDivElement).style.filter = "";
if (isSelected) return
;(e.currentTarget as HTMLDivElement).style.backgroundImage =
getBackgroundImage('components/quiz/选中')
;(e.currentTarget as HTMLDivElement).style.filter = ''
}}
onMouseLeave={(e) => {
if (isSelected) return;
(e.currentTarget as HTMLDivElement).style.backgroundImage =
getBackgroundImage("components/quiz/未选择");
(e.currentTarget as HTMLDivElement).style.filter =
"brightness(0.96)";
if (isSelected) return
;(e.currentTarget as HTMLDivElement).style.backgroundImage =
getBackgroundImage('components/quiz/未选择')
;(e.currentTarget as HTMLDivElement).style.filter = 'brightness(0.96)'
}}
>
{option.label}
@@ -67,15 +61,11 @@ const QuizPanel = ({
/>
{/* 答案反馈 */}
{error && (
<div className="mt-6 text-center text-[26px] text-[#ff4d4f]">
{error}
</div>
)}
{error && <div className="mt-6 text-center text-[26px] text-[#ff4d4f]">{error}</div>}
</div>
</div>
</div>
);
};
)
}
export default QuizPanel;
export default QuizPanel
@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import type {
SelectFillQuizPanelProps,
SelectFillResult,
} from "./types";
import { useEffect, useState } from 'react'
import type { SelectFillQuizPanelProps, SelectFillResult } from './types'
/**
* 选择填空题面板组件
@@ -21,131 +18,124 @@ const SelectFillQuizPanel = ({
onSubmit,
onError,
disabled = false,
submitText = "提交",
submitText = '提交',
}: SelectFillQuizPanelProps) => {
const [results, setResults] = useState<SelectFillResult[]>([]);
const [error, setError] = useState<string>("");
const [results, setResults] = useState<SelectFillResult[]>([])
const [error, setError] = useState<string>('')
// 当前正在填的空的下标(从 0 开始),默认为第一个空
const [activeBlankIndex, setActiveBlankIndex] = useState(0);
const [activeBlankIndex, setActiveBlankIndex] = useState(0)
// 关闭时重置内部状态
useEffect(() => {
if (!visible) {
setResults([]);
setError("");
setActiveBlankIndex(0);
setResults([])
setError('')
setActiveBlankIndex(0)
}
}, [visible]);
}, [visible])
if (!visible) return null;
if (!visible) return null
const isAllFilled = blanks.every((blank) =>
results.some((r) => r.blankId === blank.id && r.value !== "")
);
results.some((r) => r.blankId === blank.id && r.value !== '')
)
// 点击下方选项:填入当前激活的空
const handleOptionClick = (value: string) => {
if (disabled) return;
if (disabled) return
const currentBlank = blanks[activeBlankIndex];
if (!currentBlank) return;
const currentBlank = blanks[activeBlankIndex]
if (!currentBlank) return
const newResults: SelectFillResult[] = (() => {
const others = results.filter((r) => r.blankId !== currentBlank.id);
return [...others, { blankId: currentBlank.id, value }];
})();
const others = results.filter((r) => r.blankId !== currentBlank.id)
return [...others, { blankId: currentBlank.id, value }]
})()
setResults(newResults);
setError("");
setResults(newResults)
setError('')
// 自动跳到下一个空
const nextIndex = (() => {
// 优先跳到还未填写的下一个空
for (let i = 0; i < blanks.length; i++) {
const blankItem = blanks[i]!;
const hasValue = newResults.some(
(r) => r.blankId === blankItem.id && r.value !== ""
);
if (!hasValue) return i;
const blankItem = blanks[i]!
const hasValue = newResults.some((r) => r.blankId === blankItem.id && r.value !== '')
if (!hasValue) return i
}
return activeBlankIndex;
})();
setActiveBlankIndex(nextIndex);
};
return activeBlankIndex
})()
setActiveBlankIndex(nextIndex)
}
// 清空某个空
const handleClearBlank = (blankId: string) => {
if (disabled) return;
setResults(results.filter((r) => r.blankId !== blankId));
setError("");
const index = blanks.findIndex((b) => b.id === blankId);
if (index >= 0) setActiveBlankIndex(index);
};
if (disabled) return
setResults(results.filter((r) => r.blankId !== blankId))
setError('')
const index = blanks.findIndex((b) => b.id === blankId)
if (index >= 0) setActiveBlankIndex(index)
}
// 提交答案
const handleSubmit = () => {
if (disabled) return;
if (disabled) return
// 有空未填
if (!isAllFilled) {
const msg = "请先把所有空都填完再提交哦~";
setError(msg);
onError?.(msg);
return;
const msg = '请先把所有空都填完再提交哦~'
setError(msg)
onError?.(msg)
return
}
const filledResults = blanks.map((blank) => {
const r = results.find((res) => res.blankId === blank.id);
return { blankId: blank.id, value: r?.value ?? "" };
});
const r = results.find((res) => res.blankId === blank.id)
return { blankId: blank.id, value: r?.value ?? '' }
})
let isCorrect = true;
let isCorrect = true
if (correctAnswers) {
for (const r of filledResults) {
const expected = correctAnswers[r.blankId];
const expected = correctAnswers[r.blankId]
if (expected !== undefined && expected !== r.value) {
isCorrect = false;
break;
isCorrect = false
break
}
}
}
if (!correctAnswers) {
onSubmit?.(filledResults, true);
return;
onSubmit?.(filledResults, true)
return
}
if (isCorrect) {
setError("");
onSubmit?.(filledResults, true);
setError('')
onSubmit?.(filledResults, true)
} else {
const msg =
errorMessage || "还不太对,再仔细对照题干和提示想一想吧~";
setError(msg);
const msg = errorMessage || '还不太对,再仔细对照题干和提示想一想吧~'
setError(msg)
// 错误后清空已填写内容,重新作答
setResults([]);
setActiveBlankIndex(0);
onError?.(msg);
setResults([])
setActiveBlankIndex(0)
onError?.(msg)
}
};
}
const getBlankValue = (blankId: string) =>
results.find((r) => r.blankId === blankId)?.value ?? "";
const getBlankValue = (blankId: string) => results.find((r) => r.blankId === blankId)?.value ?? ''
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
<div className="relative w-[1728px] h-[972px] flex flex-col items-center justify-center">
{/* 题干 + 空格区域 */}
<div className="w-[1700px] rounded-[32px] bg-black px-20 py-12 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
<div className="text-[32px] leading-relaxed text-white">
{titlePrefix && <span>{titlePrefix}</span>}
{blanks.map((blank, index) => {
const value = getBlankValue(blank.id);
const isActive = index === activeBlankIndex;
const value = getBlankValue(blank.id)
const isActive = index === activeBlankIndex
return (
<span key={blank.id}>
{blank.prefix && <span>{blank.prefix}</span>}
@@ -153,29 +143,29 @@ const SelectFillQuizPanel = ({
type="button"
className={`inline-flex items-center justify-center min-w-[120px] h-[52px] px-4 mx-2 rounded-xl border-2 text-[28px] transition-all ${
disabled
? "opacity-60 cursor-not-allowed border-[#d4c5a9]/40"
: "cursor-pointer"
? 'opacity-60 cursor-not-allowed border-[#d4c5a9]/40'
: 'cursor-pointer'
} ${
value
? "border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] text-[#f0be75]"
: "border-[#d4c5a9]/60 bg-white/10 text-white/40"
? 'border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] text-[#f0be75]'
: 'border-[#d4c5a9]/60 bg-white/10 text-white/40'
} ${
isActive && !disabled
? "ring-2 ring-[#f0be75] ring-offset-2 ring-offset-black"
: ""
? 'ring-2 ring-[#f0be75] ring-offset-2 ring-offset-black'
: ''
}`}
onClick={() => {
if (disabled) return;
setActiveBlankIndex(index);
if (disabled) return
setActiveBlankIndex(index)
}}
>
{value || "____"}
{value || '____'}
{value && !disabled && (
<span
className="ml-3 text-[22px] text-[#f0be75]/80"
onClick={(e) => {
e.stopPropagation();
handleClearBlank(blank.id);
e.stopPropagation()
handleClearBlank(blank.id)
}}
>
×
@@ -184,17 +174,13 @@ const SelectFillQuizPanel = ({
</button>
{blank.suffix && <span>{blank.suffix}</span>}
</span>
);
)
})}
{titleSuffix && <span>{titleSuffix}</span>}
</div>
{/* 答案反馈 */}
{error && (
<div className="mt-6 text-center text-[26px] text-[#fcbe53]">
{error}
</div>
)}
{error && <div className="mt-6 text-center text-[26px] text-[#fcbe53]">{error}</div>}
</div>
{/* 选项区域 */}
@@ -207,8 +193,8 @@ const SelectFillQuizPanel = ({
disabled={disabled}
className={`min-w-[90px] h-[72px] px-6 rounded-2xl text-[28px] font-medium border-2 transition-all ${
disabled
? "opacity-60 cursor-not-allowed bg-[#d7b888]/60 border-[#d4c5a9]/60 text-[#5c3e2e]/60"
: "cursor-pointer bg-[#d7b888] border-[#d4c5a9] text-[#5c3e2e] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]"
? 'opacity-60 cursor-not-allowed bg-[#d7b888]/60 border-[#d4c5a9]/60 text-[#5c3e2e]/60'
: 'cursor-pointer bg-[#d7b888] border-[#d4c5a9] text-[#5c3e2e] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]'
}`}
onClick={() => handleOptionClick(option.value)}
>
@@ -231,9 +217,7 @@ const SelectFillQuizPanel = ({
</div>
</div>
</div>
);
};
export default SelectFillQuizPanel;
)
}
export default SelectFillQuizPanel
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { SingleChoice } from "@/components";
import type { SingleChoiceQuizPanelProps } from "./types";
import { useEffect, useState } from 'react'
import { SingleChoice } from '@/components'
import type { SingleChoiceQuizPanelProps } from './types'
/**
* 单选题面板组件
@@ -39,70 +39,61 @@ const SingleChoiceQuizPanel = ({
onError,
disabled = false,
}: SingleChoiceQuizPanelProps) => {
const [selectedValue, setSelectedValue] = useState<string>("");
const [error, setError] = useState<string>("");
const [selectedValue, setSelectedValue] = useState<string>('')
const [error, setError] = useState<string>('')
// 当窗口关闭时,重置内部状态
useEffect(() => {
if (!visible) {
setSelectedValue("");
setError("");
setSelectedValue('')
setError('')
}
}, [visible]);
}, [visible])
if (!visible) return null;
if (!visible) return null
// 处理选项点击
const handleOptionClick = (value: string) => {
if (disabled) return;
if (disabled) return
setSelectedValue(value);
setError("");
setSelectedValue(value)
setError('')
// 验证答案
const isCorrect =
correctAnswer !== undefined ? value === correctAnswer : undefined;
const isCorrect = correctAnswer !== undefined ? value === correctAnswer : undefined
// 如果提供了正确答案,进行验证
if (correctAnswer !== undefined) {
if (isCorrect) {
// 答题成功:清空状态,执行 onSubmit(外部控制关闭窗口)
setSelectedValue("");
setError("");
onSubmit?.(value, true);
setSelectedValue('')
setError('')
onSubmit?.(value, true)
} else {
// 答题失败:清空状态,执行 onError,允许重新选择
setSelectedValue("");
const customError = onWrongSelect?.(value);
const errorMsg =
customError || errorMessage || "很抱歉,你选错了!请你再思考一下吧!";
setError(errorMsg);
onError?.(errorMsg);
setSelectedValue('')
const customError = onWrongSelect?.(value)
const errorMsg = customError || errorMessage || '很抱歉,你选错了!请你再思考一下吧!'
setError(errorMsg)
onError?.(errorMsg)
}
} else {
// 没有提供正确答案,直接提交
setSelectedValue("");
setError("");
onSubmit?.(value, true);
setSelectedValue('')
setError('')
onSubmit?.(value, true)
}
};
}
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="fixed inset-0 flex items-center justify-center bg-black/40" style={{ zIndex }}>
<div className="relative w-[1728px] h-[972px] flex flex-col items-center justify-center">
{/* 题干卡片 */}
<div className="w-[1700px] rounded-[32px] bg-black px-20 py-8 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
<div className="text-[34px] leading-relaxed text-white">{title}</div>
{/* 答案反馈 */}
{error && (
<div className="mt-5 text-center text-[26px] text-[#fcbe53]">
{error}
</div>
)}
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
</div>
{/* 选项区域 */}
@@ -119,13 +110,11 @@ const SingleChoiceQuizPanel = ({
>
<div
className={`w-[1500px] flex items-center p-4 text-[28px] leading-relaxed rounded-tr-[50px] rounded-bl-4xl transition-all duration-150 ${
disabled
? "opacity-60 cursor-not-allowed"
: "cursor-pointer"
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'
} ${
isSelected
? "bg-[#d4a574] text-[#5c3e2e]"
: "bg-[#d7b888] text-[#5c3e2e] hover:bg-gradient-to-r from-[#ddf3e1] to-[#fbdd97]"
? 'bg-[#d4a574] text-[#5c3e2e]'
: 'bg-[#d7b888] text-[#5c3e2e] hover:bg-gradient-to-r from-[#ddf3e1] to-[#fbdd97]'
}`}
>
{option.label}
@@ -136,7 +125,7 @@ const SingleChoiceQuizPanel = ({
</div>
</div>
</div>
);
};
)
}
export default SingleChoiceQuizPanel;
export default SingleChoiceQuizPanel
+8 -8
View File
@@ -1,9 +1,9 @@
// 新组件
export { default as SingleChoiceQuizPanel } from "./SingleChoiceQuizPanel";
export { default as MultipleChoiceQuizPanel } from "./MultipleChoiceQuizPanel";
export { default as DragQuizPanel } from "./DragQuizPanel";
export { default as MatchQuizPanel } from "./MatchQuizPanel";
export { default as SelectFillQuizPanel } from "./SelectFillQuizPanel";
export { default as SingleChoiceQuizPanel } from './SingleChoiceQuizPanel'
export { default as MultipleChoiceQuizPanel } from './MultipleChoiceQuizPanel'
export { default as DragQuizPanel } from './DragQuizPanel'
export { default as MatchQuizPanel } from './MatchQuizPanel'
export { default as SelectFillQuizPanel } from './SelectFillQuizPanel'
export type {
SingleChoiceQuizPanelProps,
MultipleChoiceQuizPanelProps,
@@ -14,8 +14,8 @@ export type {
SelectFillQuizPanelProps,
SelectFillBlank,
SelectFillResult,
} from "./types";
} from './types'
// 保留旧组件以保持向后兼容
export { default as QuizPanel } from "./QuizPanal";
export type { QuizPanelProps } from "./types";
export { default as QuizPanel } from './QuizPanal'
export type { QuizPanelProps } from './types'
+73 -73
View File
@@ -3,188 +3,188 @@ import {
MultipleChoiceOption,
DragFillResult,
DragFillBlank,
} from "@/components";
} from '@/components'
/** 拖拽题面板配置 */
export interface DragQuizPanelProps {
/** 是否可见 */
visible: boolean;
visible: boolean
/** z-index 层级 */
zIndex?: number;
zIndex?: number
/** 题目标题(可选) */
title?: string;
title?: string
/** 题干前缀文本(空白区域之前的文本) */
questionPrefix?: string;
questionPrefix?: string
/** 题干后缀文本(空白区域之后的文本) */
questionSuffix?: string;
questionSuffix?: string
/** 选项列表 */
options: { value: string; label: string }[];
options: { value: string; label: string }[]
/** 空白区域列表 */
blanks: DragFillBlank[];
blanks: DragFillBlank[]
/** 空白之间的分隔符数组(可选,如果不提供则使用默认的"、") */
blankSeparators?: string[];
blankSeparators?: string[]
/** 当前值 */
value: DragFillResult[];
value: DragFillResult[]
/** 错误提示信息 */
error?: string;
error?: string
/** 值变化回调 */
onChange: (value: DragFillResult[]) => void;
onChange: (value: DragFillResult[]) => void
}
/** 单选题面板配置 */
export interface SingleChoiceQuizPanelProps {
/** 是否可见 */
visible: boolean;
visible: boolean
/** z-index 层级 */
zIndex?: number;
zIndex?: number
/** 题目 */
title: string;
title: string
/** 选项列表 */
options: SingleChoiceOption[];
options: SingleChoiceOption[]
/** 正确答案(可选,用于验证) */
correctAnswer?: string;
correctAnswer?: string
/** 自定义错误提示信息(答案错误时显示) */
errorMessage?: string;
errorMessage?: string
/** 提交回调,返回用户选择和是否正确 */
onSubmit?: (value: string, isCorrect: boolean) => void;
onSubmit?: (value: string, isCorrect: boolean) => void
/** 答错时回调,可返回自定义解析文案覆盖默认 errorMessage */
onWrongSelect?: (value: string) => string | void;
onWrongSelect?: (value: string) => string | void
/** 错误回调(可选) */
onError?: (error: string) => void;
onError?: (error: string) => void
/** 是否禁用 */
disabled?: boolean;
disabled?: boolean
}
/** 多选题面板配置 */
export interface MultipleChoiceQuizPanelProps {
/** 是否可见 */
visible: boolean;
visible: boolean
/** z-index 层级 */
zIndex?: number;
zIndex?: number
/** 题目 */
title: string;
title: string
/** 选项列表 */
options: MultipleChoiceOption[];
options: MultipleChoiceOption[]
/** 正确答案数组(可选,用于验证) */
correctAnswer?: string[];
correctAnswer?: string[]
/** 自定义错误提示信息(答案错误时显示),可以是字符串或函数(根据用户选择返回不同提示) */
errorMessage?: string | ((selectedValues: string[]) => string);
errorMessage?: string | ((selectedValues: string[]) => string)
/** 自定义最小选择数量不足时的提示信息 */
minSelectionsErrorMessage?: string;
minSelectionsErrorMessage?: string
/** 提交回调,返回用户选择和是否正确 */
onSubmit?: (values: string[], isCorrect: boolean) => void;
onSubmit?: (values: string[], isCorrect: boolean) => void
/** 错误回调(可选) */
onError?: (error: string) => void;
onError?: (error: string) => void
/** 是否禁用 */
disabled?: boolean;
disabled?: boolean
/** 最小选择数量 */
minSelections?: number;
minSelections?: number
/** 最大选择数量 */
maxSelections?: number;
maxSelections?: number
/** 提交按钮文本 */
submitText?: string;
submitText?: string
}
/** 连线题项 */
export interface MatchQuizItem {
/** 唯一标识 */
id: string;
id: string
/** 显示文本 */
label: string;
label: string
}
/** 连线题匹配 */
export interface MatchQuizMatch {
/** 左列项ID */
leftId: string;
leftId: string
/** 右列项ID */
rightId: string;
rightId: string
}
/** 连线题面板配置 */
export interface MatchQuizPanelProps {
/** 是否可见 */
visible: boolean;
visible: boolean
/** z-index 层级 */
zIndex?: number;
zIndex?: number
/** 题目 */
title: string;
title: string
/** 左列项列表(日期) */
leftItems: MatchQuizItem[];
leftItems: MatchQuizItem[]
/** 右列项列表(事件) */
rightItems: MatchQuizItem[];
rightItems: MatchQuizItem[]
/** 正确答案匹配列表 */
correctMatches: MatchQuizMatch[];
correctMatches: MatchQuizMatch[]
/**
* 自定义错误提示信息(答案错误时显示)
* 可以是固定文案,或根据当前匹配结果返回不同提示
*/
errorMessage?: string | ((matches: MatchQuizMatch[]) => string);
errorMessage?: string | ((matches: MatchQuizMatch[]) => string)
/** 提交回调,返回用户匹配和是否正确 */
onSubmit?: (matches: MatchQuizMatch[], isCorrect: boolean) => void;
onSubmit?: (matches: MatchQuizMatch[], isCorrect: boolean) => void
/** 错误回调(可选) */
onError?: (error: string) => void;
onError?: (error: string) => void
/** 是否禁用 */
disabled?: boolean;
disabled?: boolean
}
/** 选择填空题中,每一个空的配置 */
export interface SelectFillBlank {
/** 唯一标识,用于和答案、结果关联 */
id: string;
id: string
/** 空前面的文本(可选) */
prefix?: string;
prefix?: string
/** 空后面的文本(可选) */
suffix?: string;
suffix?: string
}
/** 选择填空题的答题结果 */
export interface SelectFillResult {
blankId: string;
value: string;
blankId: string
value: string
}
/** 选择填空题面板配置 */
export interface SelectFillQuizPanelProps {
/** 是否可见 */
visible: boolean;
visible: boolean
/** z-index 层级 */
zIndex?: number;
zIndex?: number
/** 题目前缀整体文本(可选) */
titlePrefix?: string;
titlePrefix?: string
/** 题目后缀整体文本(可选) */
titleSuffix?: string;
titleSuffix?: string
/** 填空位配置(控制有几个空、空前后的说明文本) */
blanks: SelectFillBlank[];
blanks: SelectFillBlank[]
/** 可选项列表(如 0-9 的数字、若干短语等) */
options: { value: string; label: string }[];
options: { value: string; label: string }[]
/**
* 正确答案映射
* key 为 blankId,value 为该空对应的正确值
*/
correctAnswers?: Record<string, string>;
correctAnswers?: Record<string, string>
/** 自定义错误提示信息(答案错误时显示) */
errorMessage?: string;
errorMessage?: string
/**
* 提交回调,返回用户选择结果与是否全部正确
* 注意:如果未提供 correctAnswers,则 isCorrect 恒为 true
*/
onSubmit?: (results: SelectFillResult[], isCorrect: boolean) => void;
onSubmit?: (results: SelectFillResult[], isCorrect: boolean) => void
/** 错误回调(可选) */
onError?: (error: string) => void;
onError?: (error: string) => void
/** 是否禁用 */
disabled?: boolean;
disabled?: boolean
/** 提交按钮文本 */
submitText?: string;
submitText?: string
}
/** @deprecated 使用 SingleChoiceQuizPanelProps 替代 */
export interface QuizPanelProps {
visible: boolean;
zIndex?: number;
title: string;
options: SingleChoiceOption[];
value: string;
error?: string;
onChange?: (value: string) => void;
visible: boolean
zIndex?: number
title: string
options: SingleChoiceOption[]
value: string
error?: string
onChange?: (value: string) => void
}