feat: 增强连线题功能,添加错误反馈机制

- 在 MatchQuizPanel 组件中引入错误连线对的状态管理,提供红色高亮和抖动反馈。
- 更新匹配逻辑,确保用户在连线错误时能及时获得反馈信息。
- 修改类型定义,支持自定义错误提示信息的函数形式,提升灵活性。
- 优化用户体验,确保在错误状态下的匹配项能正确展示。
This commit is contained in:
zhongyidian committed 2026-03-03 13:27:25 +08:00
1 parent b91f2430c6
commit 5d00adc1a5
43 files changed
+1662 -54

No files matched your search

+326
View File
@@ -0,0 +1,326 @@
import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
Image,
Title,
MatchQuizPanel,
Video,
type VideoRef,
} from "@/components";
import { useNavigate, useParams } from "react-router-dom";
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useRef, useState } from "react";
import Cursor from "@/components/cursor";
import { useCursorController } from "@/hooks/useCursorController";
import SuccessAlert from "@/components/layout/alert/success-alert";
import ScaledPortal from "@/components/layout/ScaledPortal";
// starName 固定为 zhangleping
const starName = "zhangleping";
// 展品 ID 与资源路径映射
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
1: "zhangleping/place2/张乐平场景2展品1",
2: "zhangleping/place2/张乐平场景2展品2",
3: "zhangleping/place2/张乐平场景2展品3",
};
// 阶段二:抗战画家(3 个光标)
const ZhanglepingPlace2 = () => {
const navigate = useNavigate();
const { moduleName } = useParams<{
moduleName: string;
}>();
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(null);
const [visited, setVisited] = useState<Set<1 | 2 | 3>>(() => new Set());
const [guideVisible2, setGuideVisible2] = useState(false);
const [guideStep2, setGuideStep2] = useState(0);
const [guideVisible3, setGuideVisible3] = useState(false);
const [guideStep3, setGuideStep3] = useState(0);
// 展品3对话结束后的连线题
const [quizVisible3, setQuizVisible3] = useState(false);
const [quizPassed3, setQuizPassed3] = useState(false);
const [successAlert3Visible, setSuccessAlert3Visible] = useState(false);
// 展品1:关闭后播放的视频
const [showVideo1, setShowVideo1] = useState(false);
const video1Ref = useRef<VideoRef>(null);
// Cursor 顺序控制:先学习展品3,再依次解锁展品2、展品1
const { isEnabled } = useCursorController({
initialEnabled: [3],
dependencies: {
2: [3],
1: [2],
},
visited,
});
if (!moduleName) return null;
const total = 3;
const progressCount = visited.size;
const completed = progressCount >= total;
const subtitle = completed
? "搜索该阶段相关资料(已完成)"
: `搜索该阶段相关资料(${progressCount}/${total})`;
const handleOpenExhibit = (id: 1 | 2 | 3) => {
setActiveExhibitId(id);
if (id === 1) {
// 展品1仅展示大图,无引导对话
} else if (id === 2) {
setGuideVisible2(true);
setGuideStep2(0);
} else {
setGuideVisible3(true);
setGuideStep3(0);
// 重新进入展品3时重置题目完成状态
setQuizPassed3(false);
}
};
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/place2/张乐平场景2`),
}}
>
{/* 标题 */}
<Title
title="抗战画家"
subtitle={subtitle}
className="absolute top-10 left-1/2 -translate-x-1/2"
/>
<Image
className="absolute right-5 top-5"
src={getStaticImage("components/离开")}
button
onClick={() => {
if (moduleName && starName) {
navigate(`/${moduleName}/${starName}/home`);
}
}}
/>
{/* 光标1 */}
<Cursor
className="absolute left-135 bottom-90"
size={0.6}
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute right-177 bottom-170"
size={0.6}
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
<Cursor
className="absolute right-135 bottom-168"
size={0.6}
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{/* 继续前进 */}
{completed && (
<Image
className="absolute bottom-0 right-0"
src={getStaticImage("components/place/继续前进")}
button
onClick={() => {
navigate(`/${moduleName}/${starName}/place3`);
}}
/>
)}
{/* 展品1:仅图片,无对话,引导通过关闭后的视频讲解 */}
{/* 展品2:代表作品与历史背景 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={setGuideVisible2}
currentStep={guideStep2}
onStepChange={setGuideStep2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
张先生还在街头做过画,这样的画作能直面民众,唤起国人的抗战热情,应该就是你说的画笔如“刀枪”吧!
</GuideStepsItem>
<GuideStepsItem speaker="guide">
不错。你看到的正是抗战时期的场景,也是张乐平先生艺术生涯中最具历史使命感和时代感的时期。1937年震惊中外的“七七事变”爆发,日本侵略行径激起了全中国人民的强烈愤慨。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
淞沪会战当月,张先生以抗日救亡漫画宣传队副领队身份北上宣传抗日,后任队长,他带领宣传队足迹遍及沪、苏、皖、鄂、浙、湘、桂、赣、闽、粤等地,配合抗战形势创作了大量的抗日漫画。
</GuideStepsItem>
</GuideSteps>
{/* 展品3:张乐平画笔在不同历史时期的力量 */}
<GuideSteps
visible={guideVisible3}
onVisibleChange={(visible) => {
setGuideVisible3(visible);
// 对话结束后,弹出连线题(如未通过)
if (!visible && !quizPassed3) {
setQuizVisible3(true);
}
}}
currentStep={guideStep3}
onStepChange={setGuideStep3}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
这幅“穷凶极恶的结果”的漫画太形象了,小沪,聂耳先生用音乐唤醒大家的爱国情怀,那张乐平先生的画笔在不同年代里,是不是也为大家带来了不一样的力量呀?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
这是个很好的问题!张乐平先生的画笔在不同的历史时期发挥着不同作用,比如抗日战争时是“刀枪”,揭露日寇暴行;解放战争时是“镜子”,映照社会现实;新中国成立后是“园丁”,为万千儿童种下真善美的种子。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
六十年笔墨不辍,他笔下不只是漫画故事,更是陪着一代代中国人长大的心灵成长书。张先生用画笔为我们带来了对社会深刻的洞察和对普通民众深切的关怀。
</GuideStepsItem>
</GuideSteps>
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onClose={() => {
if (activeExhibitId !== null) {
// 展品1:关闭后播放一段视频,视频结束再标记已访问
if (activeExhibitId === 1) {
setActiveExhibitId(null);
setShowVideo1(true);
return;
}
// 展品3:需在完成连线题后才标记为已访问
if (activeExhibitId === 3) {
if (quizPassed3) {
setVisited((prev) => {
const next = new Set(prev);
next.add(3);
return next;
});
}
} else {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
}
}
setActiveExhibitId(null);
}}
/>
{/* 展品1关闭后的讲解视频 */}
{showVideo1 && (
<Video
ref={video1Ref}
src={getPublicPath("video/sunzhongshan/sunzhongshan_03.mp4")}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo1(false);
setVisited((prev) => {
const next = new Set(prev);
next.add(1);
return next;
});
}}
onSkip={() => {
setShowVideo1(false);
}}
/>
)}
{/* 展品3对话结束后的连线题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible3 || successAlert3Visible} zIndex={10050}>
<>
<MatchQuizPanel
visible={quizVisible3}
title="【连线】张乐平先生的画笔在不同历史时期发挥着不同作用,请你把对应的历史时期与当时先生画笔发挥的主要作用匹配起来吧!"
leftItems={[
{ id: "period1", label: "抗日战争" },
{ id: "period2", label: "解放战争" },
{ id: "period3", label: "新中国成立后" },
]}
rightItems={[
{ id: "role2", label: "镜子" },
{ id: "role3", label: "园丁" },
{ id: "role1", label: "刀枪" },
]}
correctMatches={[
{ leftId: "period1", rightId: "role1" },
{ leftId: "period2", rightId: "role2" },
{ leftId: "period3", rightId: "role3" },
]}
errorMessage={(matches) => {
const getMatchByLeft = (leftId: string) =>
matches.find((m) => m.leftId === leftId);
const match1 = getMatchByLeft("period1");
if (match1 && match1.rightId !== "role1") {
return "抗日战争时期,张乐平先生的画笔主要是揭露日寇暴行,似“刀枪”。请你再想想吧!";
}
const match2 = getMatchByLeft("period2");
if (match2 && match2.rightId !== "role2") {
return "解放战争时期,张乐平先生的画笔主要是映照社会现实,似“镜子”。请你再想想吧!";
}
const match3 = getMatchByLeft("period3");
if (match3 && match3.rightId !== "role3") {
return "新中国成立后,张乐平先生的画笔主要为万千儿童种下真善美的种子,似“园丁”。请你再想想吧!";
}
return "连线不太对,请你再仔细想一想吧!";
}}
onSubmit={(_matches, isCorrect) => {
if (isCorrect) {
setQuizVisible3(false);
setQuizPassed3(true);
setSuccessAlert3Visible(true);
// 答题成功后标记展品3为已访问
setVisited((prev) => {
const next = new Set(prev);
next.add(3);
return next;
});
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlert3Visible}
onContinue={() => {
setSuccessAlert3Visible(false);
}}
/>
</>
</ScaledPortal>
</div>
);
};
export default ZhanglepingPlace2;