feat: 增强连线题功能,添加错误反馈机制
- 在 MatchQuizPanel 组件中引入错误连线对的状态管理,提供红色高亮和抖动反馈。 - 更新匹配逻辑,确保用户在连线错误时能及时获得反馈信息。 - 修改类型定义,支持自定义错误提示信息的函数形式,提升灵活性。 - 优化用户体验,确保在错误状态下的匹配项能正确展示。
This commit is contained in:
1 parent
b91f2430c6
commit
5d00adc1a5
43 files changed
+1662
-54
No files matched your search
@@ -45,11 +45,16 @@ const MatchQuizPanel = ({
|
||||
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 }>
|
||||
>([]);
|
||||
|
||||
// 当窗口关闭时,重置状态
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setMatches([]);
|
||||
setErrorPairs([]);
|
||||
setSelectedLeftId(null);
|
||||
setError("");
|
||||
}
|
||||
@@ -99,47 +104,67 @@ const MatchQuizPanel = ({
|
||||
// 创建新的匹配
|
||||
const newMatch = { leftId: selectedLeftId, rightId };
|
||||
const newMatches = [...matches, newMatch];
|
||||
|
||||
// 每次连线后立即校验当前所有连线是否存在错误
|
||||
const wrongMatches =
|
||||
correctMatches?.filter(
|
||||
() => true
|
||||
) && // 保证 correctMatches 存在时才校验
|
||||
newMatches.filter(
|
||||
(match) =>
|
||||
!correctMatches?.some(
|
||||
(cm) => cm.leftId === match.leftId && cm.rightId === match.rightId
|
||||
)
|
||||
);
|
||||
|
||||
const hasError = !!wrongMatches && wrongMatches.length > 0;
|
||||
|
||||
if (hasError && wrongMatches) {
|
||||
// 记录错误连线对,用于红色高亮+抖动反馈
|
||||
setErrorPairs(wrongMatches);
|
||||
setMatches(newMatches);
|
||||
setSelectedLeftId(null);
|
||||
setError("");
|
||||
|
||||
let errorMsg: string;
|
||||
if (typeof errorMessage === "function") {
|
||||
errorMsg = errorMessage(newMatches);
|
||||
} else if (errorMessage) {
|
||||
errorMsg = errorMessage;
|
||||
} else {
|
||||
errorMsg = "连线不正确,请重新尝试!";
|
||||
}
|
||||
setError(errorMsg);
|
||||
onError?.(errorMsg);
|
||||
// 短暂展示错误状态(变红+抖动),随后只清空错误连线,保留已答对的连线
|
||||
setTimeout(() => {
|
||||
setMatches((prev) =>
|
||||
prev.filter(
|
||||
(m) =>
|
||||
!wrongMatches.some(
|
||||
(w) => w.leftId === m.leftId && w.rightId === m.rightId
|
||||
)
|
||||
)
|
||||
);
|
||||
setErrorPairs([]);
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有错误时,正常记录匹配
|
||||
setMatches(newMatches);
|
||||
setSelectedLeftId(null);
|
||||
setError("");
|
||||
|
||||
// 检查是否所有项都已匹配
|
||||
if (newMatches.length === leftItems.length && newMatches.length === rightItems.length) {
|
||||
// 验证答案
|
||||
const isCorrect = validateMatches(newMatches);
|
||||
if (isCorrect) {
|
||||
onSubmit?.(newMatches, true);
|
||||
} else {
|
||||
const errorMsg = errorMessage || "连线不正确,请重新尝试!";
|
||||
setError(errorMsg);
|
||||
onError?.(errorMsg);
|
||||
// 清空匹配,允许重新尝试
|
||||
setTimeout(() => {
|
||||
setMatches([]);
|
||||
}, 2000);
|
||||
}
|
||||
// 如果没有错误且所有项都已连完,则认为答题成功
|
||||
if (
|
||||
newMatches.length === leftItems.length &&
|
||||
newMatches.length === rightItems.length
|
||||
) {
|
||||
onSubmit?.(newMatches, true);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证匹配是否正确
|
||||
const validateMatches = (matchesToValidate: Array<{ leftId: string; rightId: string }>) => {
|
||||
if (!correctMatches || matchesToValidate.length !== correctMatches.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查每个匹配是否正确
|
||||
for (const match of matchesToValidate) {
|
||||
const isCorrect = correctMatches.some(
|
||||
(cm) => cm.leftId === match.leftId && cm.rightId === match.rightId
|
||||
);
|
||||
if (!isCorrect) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 获取左项是否已匹配
|
||||
const isLeftItemMatched = (leftId: string) => {
|
||||
return matches.some((m) => m.leftId === leftId);
|
||||
@@ -192,7 +217,7 @@ const MatchQuizPanel = ({
|
||||
{leftItems.map((item) => {
|
||||
const isMatched = isLeftItemMatched(item.id);
|
||||
const isSelected = selectedLeftId === item.id;
|
||||
const matchedRightId = getMatchedRightId(item.id);
|
||||
const isError = errorPairs.some((p) => p.leftId === item.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -204,7 +229,9 @@ const MatchQuizPanel = ({
|
||||
disabled || isMatched
|
||||
? "opacity-60 cursor-not-allowed"
|
||||
: "cursor-pointer",
|
||||
isSelected
|
||||
isError
|
||||
? "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]"
|
||||
@@ -220,7 +247,12 @@ const MatchQuizPanel = ({
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
{isMatched && (
|
||||
{isError && (
|
||||
<div className="absolute -right-2 md:-right-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-red-400 rounded-full flex items-center justify-center shadow-[0_0_10px_rgba(248,113,113,0.8)]">
|
||||
<span className="text-white text-[10px] md:text-xs">✕</span>
|
||||
</div>
|
||||
)}
|
||||
{!isError && isMatched && (
|
||||
<div className="absolute -right-2 md:-right-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xs md:text-sm">✓</span>
|
||||
</div>
|
||||
@@ -282,7 +314,7 @@ 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 matchedLeftId = getMatchedLeftId(item.id);
|
||||
const isError = errorPairs.some((p) => p.rightId === item.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -296,7 +328,9 @@ const MatchQuizPanel = ({
|
||||
: selectedLeftId
|
||||
? "cursor-pointer"
|
||||
: "cursor-default",
|
||||
isMatched
|
||||
isError
|
||||
? "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]"
|
||||
@@ -313,7 +347,12 @@ const MatchQuizPanel = ({
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
{isMatched && (
|
||||
{isError && (
|
||||
<div className="absolute -left-2 md:-left-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-red-400 rounded-full flex items-center justify-center shadow-[0_0_10px_rgba(248,113,113,0.8)]">
|
||||
<span className="text-white text-[10px] md:text-xs">✕</span>
|
||||
</div>
|
||||
)}
|
||||
{!isError && isMatched && (
|
||||
<div className="absolute -left-2 md:-left-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xs md:text-sm">✓</span>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user