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

+422
View File
@@ -0,0 +1,422 @@
import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
Image,
Title,
SingleChoiceQuizPanel,
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/place3/张乐平场景3展品1",
2: "zhangleping/place3/张乐平场景3展品2",
3: "zhangleping/place3/张乐平场景3展品3",
};
// 阶段三:三毛之父(3 个光标)
const ZhanglepingPlace3 = () => {
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 [guideVisible1, setGuideVisible1] = useState(false);
const [guideStep1, setGuideStep1] = useState(0);
const [guideVisible2, setGuideVisible2] = useState(false);
const [guideStep2, setGuideStep2] = useState(0);
const [guideVisible3, setGuideVisible3] = useState(false);
const [guideStep3, setGuideStep3] = useState(0);
// 展品2:关闭后播放的视频 + 单选题
const [showVideo2, setShowVideo2] = useState(false);
const video2Ref = useRef<VideoRef>(null);
const [quiz2Visible, setQuiz2Visible] = useState(false);
const [successAlert2Visible, setSuccessAlert2Visible] = useState(false);
const [successAlert2Content, setSuccessAlert2Content] = useState("");
// 展品3对话结束后的两道单选题
const [quiz3Visible, setQuiz3Visible] = useState(false);
const [quiz3Step, setQuiz3Step] = useState<1 | 2>(1);
const [quiz3Finished, setQuiz3Finished] = useState(false);
const [successAlert3Visible, setSuccessAlert3Visible] = useState(false);
const [successAlert3Content, setSuccessAlert3Content] = useState("");
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) {
setGuideVisible1(true);
setGuideStep1(0);
} else if (id === 2) {
setGuideVisible2(true);
setGuideStep2(0);
} else {
setGuideVisible3(true);
setGuideStep3(0);
}
};
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/place3/张乐平场景3`),
}}
>
{/* 标题 */}
<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-202 bottom-109"
size={0.8}
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute right-178 bottom-62"
size={0.8}
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
<Cursor
className="absolute right-58 bottom-68"
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}/place4`);
}}
/>
)}
{/* 展品1:三毛的诞生 */}
<GuideSteps
visible={guideVisible1}
onVisibleChange={setGuideVisible1}
currentStep={guideStep1}
onStepChange={setGuideStep1}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
那我明白了!这个戴上红领巾的三毛体现的应该是已经摆脱苦难的中国人民了。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
确实如此。这幅名叫《向日葵》的画选自于1950—1965年的《三毛日记》。像这样的“新三毛”形象再次发生重要转变是自1949年新中国成立之后,三毛戴上红领巾,成为新中国的少先队员。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
这一变化不仅标志着角色自身的成长,更是整个中国社会由战乱走向安定、由贫弱迈向新生的时代缩影。三毛那三根标志性的头发,仿佛穿越时光的见证者,记录下一个民族从山河破碎到国泰民安的巨大变迁。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
他的每一次蜕变,不仅是一个漫画人物的成长,更是一个国家与一代人的共同记忆。
</GuideStepsItem>
</GuideSteps>
{/* 展品2:三毛形象的特点 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={setGuideVisible2}
currentStep={guideStep2}
onStepChange={setGuideStep2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="guide">
早期三毛以一个滑稽幽默、调皮活泼的上海弄堂儿童形象出现。抗战胜利后,张先生重返上海开启了新的创作历程,自1950年6月起开始居住在五原路288弄3号,直至1992年9月辞世,张先生在这里度过了他艺术生涯中极为重要的创作时期。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
他先是根据抗战前线的经历见闻创作了《三毛从军记》,后又以自己耳闻目睹的社会现实,创作了《三毛流浪记》,三毛的形象也逐步转变为承载民族苦难与底层儿童命运的象征。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
三毛身上的每一处伤痕、每一次遭遇,可以说都真实折射出了当时社会的困顿与民众的疾苦,也是为什么说这个时期,先生的画作更像是一面“镜子”。
</GuideStepsItem>
</GuideSteps>
{/* 展品3:三毛的社会影响 */}
<GuideSteps
visible={guideVisible3}
onVisibleChange={(visible) => {
setGuideVisible3(visible);
// 对话结束后依次进入两道题目(仅在未完成时触发)
if (!visible) {
setQuiz3Step(1);
setQuiz3Visible(true);
}
}}
currentStep={guideStep3}
onStepChange={setGuideStep3}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
小沪,我一直知道张乐平先生被称为“三毛之父”,但是今天才发现张先生笔下成长时期的三毛看起来比早期的三毛多了一些磨难,张先生塑造的这样一个小流浪儿也在跟着时代一起成长吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
你的感受很准确。三毛初次亮相于1935年的上海,他的创作者张乐平先生也开创了中国无文字儿童连环漫画之先河。
</GuideStepsItem>
</GuideSteps>
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onClose={() => {
if (activeExhibitId !== null) {
// 展品2:关闭后先播放一段视频,视频结束再进入题目
if (activeExhibitId === 2) {
setActiveExhibitId(null);
setShowVideo2(true);
return;
}
// 展品3:需完成两道题目后才标记为已访问
if (activeExhibitId === 3) {
if (quiz3Finished) {
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);
}}
/>
{/* 展品2关闭后的讲解视频 */}
{showVideo2 && (
<Video
ref={video2Ref}
src={getPublicPath("video/sunzhongshan/sunzhongshan_03.mp4")}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo2(false);
setQuiz2Visible(true);
}}
onSkip={() => {
setShowVideo2(false);
setQuiz2Visible(true);
}}
/>
)}
{/* 展品2关闭后的视频 + 单选题(如图1) */}
<ScaledPortal
enabled={quiz2Visible || successAlert2Visible}
zIndex={10040}
>
<>
<SingleChoiceQuizPanel
visible={quiz2Visible}
zIndex={10000}
title="【单选】张乐平先生根据自己抗战前线的经历见闻创作的作品是:"
options={[
{ value: "A", label: "A.三毛流浪记" },
{ value: "B", label: "B.三毛日记" },
{ value: "C", label: "C.三毛从军记" },
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === "A") {
return "《三毛流浪记》以旧上海为背景,聚焦三毛在城市底层的流浪生活,主要反映旧社会儿童的苦难遭遇与人性冷暖。请你再想想吧!";
}
if (value === "B") {
return "《三毛日记》以日记体形式记录三毛的日常琐事与童真视角,内容偏向儿童生活的趣味表达。请你再想想吧!";
}
return "请你再想想吧!";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuiz2Visible(false);
setSuccessAlert2Content(
"《三毛从军记》是张乐平根据自己奔赴抗战前线的亲身经历与见闻创作的作品,通过三毛从军的荒诞经历,讽刺了当时的社会乱象,也传递出抗战背景下的民生百态。"
);
setSuccessAlert2Visible(true);
}}
/>
<SuccessAlert
visible={successAlert2Visible}
content={successAlert2Content}
onContinue={() => {
setSuccessAlert2Visible(false);
// 答题成功后标记展品2为已访问
setVisited((prev) => {
const next = new Set(prev);
next.add(2);
return next;
});
}}
/>
</>
</ScaledPortal>
{/* 展品3对话结束后的两道单选题 + 每题答对后的提示(使用 ScaledPortal) */}
<ScaledPortal
enabled={quiz3Visible || successAlert3Visible}
zIndex={10050}
>
<>
{/* 题目1:三毛形象首次亮相年份与地点 */}
<SingleChoiceQuizPanel
visible={quiz3Visible && quiz3Step === 1}
zIndex={10000}
title="【单选】1935年,张乐平先生创作的三毛形象初次亮相于:"
options={[
{ value: "A", label: "A.上海" },
{ value: "B", label: "B.延安" },
{ value: "C", label: "C.浙江" },
]}
correctAnswer="A"
onWrongSelect={(value) => {
if (value === "B") {
return "延安是当时的革命根据地,但张乐平先生的主要创作活动和延安没有关联。请你再想想吧!";
}
if (value === "C") {
return "浙江是张乐平先生的故乡,不是三毛初次亮相的地方。请你再想想吧!";
}
return "请再仔细阅读题干中的时间和地点信息~";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuiz3Visible(false);
setSuccessAlert3Content(
"张乐平先生创作的三毛初次亮相于1935年上海的《晨报》副刊。"
);
setSuccessAlert3Visible(true);
}}
/>
{/* 题目2:三毛与中国无文字儿童连环漫画 */}
<SingleChoiceQuizPanel
visible={quiz3Visible && quiz3Step === 2}
zIndex={10000}
title="【判断】张乐平先生创作的三毛开创了中国无文字儿童连环漫画之先河。"
options={[
{ value: "A", label: "A.正确" },
{ value: "B", label: "B.错误" },
]}
correctAnswer="A"
onWrongSelect={(value) => {
if (value === "B") {
return "再想一想三毛在中国儿童漫画史上的地位吧~";
}
return "请再仔细对照题干信息选择~";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuiz3Visible(false);
setSuccessAlert3Content("");
setSuccessAlert3Visible(true);
}}
/>
{/* 每题答对后的统一提示框 */}
<SuccessAlert
visible={successAlert3Visible}
content={successAlert3Content}
onContinue={() => {
setSuccessAlert3Visible(false);
// 进入下一题或标记完成
setQuiz3Step((prev) => {
const next = (prev + 1) as 1 | 2 | 3;
if (next === 2) {
setQuiz3Visible(true);
return 2;
}
// 两题全部完成
setQuiz3Finished(true);
// 标记展品3为已访问
setVisited((prevVisited) => {
const nextVisited = new Set(prevVisited);
nextVisited.add(3);
return nextVisited;
});
return prev;
});
}}
/>
</>
</ScaledPortal>
</div>
);
};
export default ZhanglepingPlace3;