feat: 添加选择填空题和多选题组件,增强学习互动性

- 在 quiz 组件中引入 SelectFillQuizPanel 和 MultipleChoiceQuizPanel,丰富用户的学习体验。
- 更新 Maozedong 相关页面,增加新的选择填空题和多选题,提升互动性。
- 优化状态管理,确保题目完成后的反馈逻辑流畅。
- 更新 README 文档,详细说明新组件的功能和使用示例。
This commit is contained in:
zhongyidian committed 2026-02-27 17:50:47 +08:00
1 parent 2c8bc73e57
commit b91f2430c6
13 files changed
+1386 -238

No files matched your search

+409 -31
View File
@@ -2,26 +2,31 @@ import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
EndingOverlay,
Image,
Title,
Video,
type VideoRef,
SingleChoiceQuizPanel,
} from "@/components";
import { useNavigate, useParams } from "react-router-dom";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useState } from "react";
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useEffect, 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 固定为 maozedong
const starName = "maozedong";
// 展品 ID 与资源路径映射,ID 用来做唯一标识
const exhibitSrcMap: Record<1 | 2 | 3 | 4 | 5 | 6, string> = {
const exhibitSrcMap: Record<1 | 2 | 3 | 4 | 5 , string> = {
1: "maozedong/place4/场景4展品1",
2: "maozedong/place4/场景4展品2",
3: "maozedong/place4/场景4展品3",
4: "maozedong/place4/场景4展品4",
5: "maozedong/place4/场景4展品5",
6: "maozedong/place4/场景4展品6",
2: "maozedong/place4/场景4展品2_",
3: "maozedong/place4/场景4展品4",
4: "maozedong/place4/场景4展品5",
5: "maozedong/place4/场景4展品6",
};
const MaozedongPlace4 = () => {
@@ -30,40 +35,89 @@ const MaozedongPlace4 = () => {
moduleName: string;
}>();
// 引导对话显示状态
// 场景级引导对话显示状态(暂未使用,可根据需要开启)
const [visible, setVisible] = useState(false);
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<
1 | 2 | 3 | 4 | 5 | 6 | null
1 | 2 | 3 | 4 | 5 | null
>(null);
// 已访问的展品(本页内状态;如需持久化可接入 store)
const [visited, setVisited] = useState<Set<1 | 2 | 3 | 4 | 5 | 6>>(
() => new Set()
);
// 光标1的对话状态
const [guide1Visible, setGuide1Visible] = useState(false);
const [guide1Step, setGuide1Step] = useState(0);
// 光标3+光标5均查看后的总结对话
const [guide35Visible, setGuide35Visible] = useState(false);
const [guide35Step, setGuide35Step] = useState(0);
const [guide35Shown, setGuide35Shown] = useState(false);
// 光标4(展品4)对话
const [guide4Visible, setGuide4Visible] = useState(false);
const [guide4Step, setGuide4Step] = useState(0);
// 关闭展品4后的结束语
const [endingVisible, setEndingVisible] = useState(false);
// 光标2的对话状态
const [guide2Visible, setGuide2Visible] = useState(false);
const [guide2Step, setGuide2Step] = useState(0);
// 光标2的视频与题目状态
const [showVideo2, setShowVideo2] = useState(false);
const video2Ref = useRef<VideoRef>(null);
const [quizStep, setQuizStep] = useState(0); // 0 表示未开始,1-4 为四道题
const [quizVisible, setQuizVisible] = useState(false);
const [successAlertVisible, setSuccessAlertVisible] = useState(false);
const [successAlertContent, setSuccessAlertContent] = useState<string>("");
// Cursor 顺序控制:1 -> 2 -> 3 -> 4 -> 5 -> 6
const { isEnabled } = useCursorController({
initialEnabled: [1],
initialEnabled: [2],
dependencies: {
2: [1],
3: [1, 2],
4: [1, 2, 3],
5: [1, 2, 3, 4],
6: [1, 2, 3, 4, 5],
1:[2],
3:[1],
4:[3,5],
5:[1],
},
visited,
});
const handleOpenExhibit = (id: 1 | 2 | 3 | 4 | 5 | 6) => {
const handleOpenExhibit = (id: 1 | 2 | 3 | 4 | 5 ) => {
setActiveExhibitId(id);
if (id === 1) {
setGuide1Step(0);
setGuide1Visible(true);
}
if (id === 2) {
setGuide2Step(0);
setGuide2Visible(true);
setQuizStep(0);
}
if (id === 4) {
setGuide4Step(0);
setGuide4Visible(true);
}
};
// 光标3和光标5都已查看后,弹出一次总结对话(图1)
useEffect(() => {
if (guide35Shown) return;
if (!visited.has(3) || !visited.has(5)) return;
setGuide35Step(0);
setGuide35Visible(true);
setGuide35Shown(true);
}, [visited, guide35Shown]);
if (!moduleName) return null;
const progressCount = visited.size;
const total = 6;
const total = 5;
const completed = progressCount >= total;
const subtitle = completed
? "关于中共四大(已完成)"
@@ -103,39 +157,39 @@ const MaozedongPlace4 = () => {
/>
{/* 光标2 */}
<Cursor
className="absolute left-179 bottom-163"
className="absolute left-199 bottom-162"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
<Cursor
{/* <Cursor
className="absolute left-215 bottom-163"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/> */}
{/* 光标3 */}
<Cursor
className="absolute left-212 bottom-123"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{/* 光标4 */}
<Cursor
className="absolute left-212 bottom-123"
className="absolute right-186 bottom-162"
onClick={() => handleOpenExhibit(4)}
read={visited.has(4)}
disabled={!isEnabled(4)}
/>
{/* 光标5 */}
<Cursor
className="absolute right-186 bottom-162"
className="absolute right-153 bottom-123"
onClick={() => handleOpenExhibit(5)}
read={visited.has(5)}
disabled={!isEnabled(5)}
/>
{/* 光标6 */}
<Cursor
className="absolute right-153 bottom-123"
onClick={() => handleOpenExhibit(6)}
read={visited.has(6)}
disabled={!isEnabled(6)}
/>
{/* 场景对话 */}
<GuideSteps
@@ -165,21 +219,345 @@ const MaozedongPlace4 = () => {
</GuideStepsItem>
</GuideSteps>
{/* 光标1的对话:点击展品1时出现 */}
<GuideSteps
visible={guide1Visible}
currentStep={guide1Step}
onStepChange={setGuide1Step}
onVisibleChange={(nextVisible) => {
setGuide1Visible(nextVisible);
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
看来中共四大明确提出了“无产阶级领导权”和“工农联盟”,这和毛主席的实践有关吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
太有关系了!中共三大后毛主席到上海工作了一段时间,看到工人运动轰轰烈烈,
但农民还是“沉默的大多数”。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
后来毛主席回湖南,专门跑了湘潭、湘乡、衡山、
醴陵、长沙五县,发现农民不仅能革命,而且革命性极强——他们被剥削得最狠,
反抗起来也最坚决!
</GuideStepsItem>
</GuideSteps>
{/* 光标2的对话:点击展品2时出现 */}
<GuideSteps
visible={guide2Visible}
currentStep={guide2Step}
onStepChange={setGuide2Step}
onVisibleChange={(nextVisible) => {
setGuide2Visible(nextVisible);
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
那中共四大为什么召开呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
因为此前中共三大虽然确定了国共合作的方针,但未明确解决
“谁领导革命”的问题。我们必须要明确“谁领导革命”“依靠谁革命”
这两个基本问题。
</GuideStepsItem>
</GuideSteps>
{/* 光标3 + 光标5 都已查看后的对话(图1) */}
<GuideSteps
visible={guide35Visible}
currentStep={guide35Step}
onStepChange={setGuide35Step}
onVisibleChange={(nextVisible) => {
setGuide35Visible(nextVisible);
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
所以毛主席觉得,光靠工人不够,得把农民拉进革命队伍?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
对!无产阶级要领导革命,必须联合农民——他们是工人阶级的“天然盟友”。
四大虽然还没有明确提出“农村包围城市”,但已经意识到农民问题的重要性。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
后来毛主席常说:“国民革命的中心问题,就是农民问题,一切都要靠农民问题的解决。”
这句话的种子,就是在四大前后埋下的。
</GuideStepsItem>
</GuideSteps>
{/* 光标4打开展品4时出现的对话(图2、图3) */}
<GuideSteps
visible={guide4Visible}
currentStep={guide4Step}
onStepChange={setGuide4Step}
onVisibleChange={(nextVisible) => {
setGuide4Visible(nextVisible);
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
这张照片看起来就是在四大前后拍摄的,那为什么全家福中缺少了男主人呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
这是1924年杨开慧与毛岸英、毛岸青的合影,是这个家庭留下的唯一一张“全家福”,
被誉为“不完美的完美全家福”。说它“不完美”,是因为当时党的纪律要求重要人物不得拍照,“全家福”里缺少了男主人。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
为何又要说它“完美”呢?是因为它完美地诠释了共产党人严格遵守党的规章制度,
舍小家为大家的家国情怀。六年后,照片中的杨开慧在湖南牺牲。二十六年后,毛岸英在朝鲜战场牺牲。
为了中国的革命事业,毛主席一家先后牺牲了六位亲人……
</GuideStepsItem>
<GuideStepsItem speaker="student">
为有牺牲多壮志,敢教日月换新天。小沪,通过您的讲解,
我明白了中共一大到四大是在艰难探索中不断前进的——从建党的理想,
到制定民主革命纲领;从国共合作,到发现农民的力量。这些探索对我们今天有什么启示呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
同学问到了根子上!革命就像爬山,一开始可能看不清山顶的样子,
但只要沿着正确的方向,一步一步扎扎实实地走,总会离目标越来越近。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
你们现在学历史,不能只记年代和事件,更要理解“为什么”——
为什么马克思主义要和中国实际相结合?为什么农民是革命的重要力量?
把这些道理想通了,将来不管做什么事,都能脚踏实地。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
“实事求是”这四个字,
是我们党一路走来的法宝,希望你也能记住。
</GuideStepsItem>
</GuideSteps>
{/* 展品遮罩层 */}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onClose={() => {
if (activeExhibitId !== null) {
const currentId = activeExhibitId;
// 展品2:关闭后播放视频和答题,全部正确后再标记已访问
if (currentId === 2) {
setGuide2Step(0);
setGuide2Visible(false);
setActiveExhibitId(null);
setShowVideo2(true);
return;
}
// 展品4:关闭后出现结束语(EndingOverlay)
if (currentId === 4) {
setGuide4Step(0);
setGuide4Visible(false);
setActiveExhibitId(null);
setEndingVisible(true);
return;
}
if (currentId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
next.add(currentId);
return next;
});
}
setActiveExhibitId(null);
}}
/>
{/* 结束语(图4):关闭展品4后出现 */}
<EndingOverlay
visible={endingVisible}
text="我们通过毛主席在1921~1924年参加党的“一大、二大、三大、四大”等重大历史事件中所扮演的重要角色及他个人思想育理论成果的积累,深刻认识到早期共产党人一步一脚印探索中国革命道路的曲折历程。"
onContinue={() => {
setEndingVisible(false);
// 结束语关闭后,标记展品4为已查看
setVisited((prev) => {
const next = new Set(prev);
next.add(4);
return next;
});
}}
animationSpeed={200}
/>
{/* 光标2:展品关闭后播放的视频 */}
{showVideo2 && (
<Video
ref={video2Ref}
src={getPublicPath("video/sunzhongshan/sunzhongshan_03.mp4")}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo2(false);
setQuizStep(1);
setQuizVisible(true);
}}
/>
)}
{/* 光标2:四道单选题 + 每题答对后的提示 */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
{/* 题目1:中共四大召开时间 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 1}
zIndex={10000}
title="【单选】中共四大召开的时间是( )"
options={[
{ value: "A", label: "A.1923年6月" },
{ value: "B", label: "B.1924年1月" },
{ value: "C", label: "C.1925年1月" },
{ value: "D", label: "D.1926年7月" },
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === "A") {
return "1923年6月,中共三大召开,确定了国共合作方针。请你再想想吧!";
}
if (value === "B") {
return "1924年1月,国民党一大召开,国共合作正式形成。请你再想想吧!";
}
if (value === "D") {
return "1926年7月,北伐战争开始。请你再想想吧!";
}
return "请结合时间线再仔细思考一下~";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuizVisible(false);
setSuccessAlertContent(
"中共四大召开于1925年1月11日至22日,在上海。"
);
setSuccessAlertVisible(true);
}}
/>
{/* 题目2:召开地点 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 2}
zIndex={10000}
title="【单选】中共四大召开的地点是:"
options={[
{ value: "A", label: "A.北京" },
{ value: "B", label: "B.上海" },
{ value: "C", label: "C.广州" },
{ value: "D", label: "D.武汉" },
]}
correctAnswer="B"
onWrongSelect={(value) => {
if (value === "A") {
return "现在中国共产党全国代表大会多在北京召开,但是中共四大不是哦!请你再想想吧!";
}
if (value === "C") {
return "中共三大在广州召开,请你再想想吧!";
}
if (value === "D") {
return "中共五大在武汉召开,请你再想想吧!";
}
return "请再仔细对照题干信息选择~";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuizVisible(false);
setSuccessAlertContent("");
setSuccessAlertVisible(true);
}}
/>
{/* 题目3:首次明确提出的重要问题 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 3}
zIndex={10000}
title="【单选】中共四大第一次明确提出的重要问题是:"
options={[
{ value: "A", label: "A.党的建设问题" },
{ value: "B", label: "B.武装斗争问题" },
{ value: "C", label: "C.无产阶级领导权和工农联盟问题" },
{ value: "D", label: "D.土地革命问题" },
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === "A") {
return "古田会议更系统阐述党的建设问题哦!请你再想想吧!";
}
if (value === "B") {
return "八七会议更系统阐述武装斗争问题哦!请你再想想吧!";
}
if (value === "D") {
return "八七会议后提出土地革命问题哦!请你再想想吧!";
}
return "请再仔细比较不同会议的主要议题~";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuizVisible(false);
setSuccessAlertContent("");
setSuccessAlertVisible(true);
}}
/>
{/* 题目4:党的基本组织形式 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 4}
zIndex={10000}
title="【单选】中共四大确立的党的基本组织形式是:"
options={[
{ value: "A", label: "A.组" },
{ value: "B", label: "B.支部" },
// { value: "C", label: "C.区委" },
// { value: "D", label: "D.中央局" },
]}
correctAnswer="B"
onWrongSelect={(value) => {
if (value === "A") {
return "中共四大以前党的基本组织形式是组或者小组,请你再想想吧!";
}
return "请注意中共四大之后对基层组织形式的规定~";
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setQuizVisible(false);
setSuccessAlertContent(
"中共四大明确“支部是党的基本组织”,规定“有三名党员即可成立一个支部”。"
);
setSuccessAlertVisible(true);
// 全部答对后标记展品2为已访问
setVisited((prev) => {
const next = new Set(prev);
next.add(2);
return next;
});
}}
/>
{/* 每题答对后的统一提示框 */}
<SuccessAlert
visible={successAlertVisible}
content={successAlertContent}
onContinue={() => {
setSuccessAlertVisible(false);
// 进入下一题
setQuizStep((prev) => {
const next = prev + 1;
if (next >= 1 && next <= 4) {
setQuizVisible(true);
}
return next;
});
}}
/>
</>
</ScaledPortal>
</div>
);
};