feat: 更新拖拽题功能和图像资源,提升学习体验

- 在 DragQuizPanel 组件中添加空白分隔符功能,增强用户交互性。
- 更新多个场景中的图像资源,确保视觉内容的准确性和一致性。
- 调整引导对话文本,提升信息传达的清晰度。
- 引入新的拖拽题逻辑,优化用户填写体验。
This commit is contained in:
zhongyidian committed 2026-01-18 16:45:06 +08:00
1 parent 9184c41cc7
commit cf64e7ec50
25 files changed
+726 -241

No files matched your search

+72 -46
View File
@@ -7,7 +7,6 @@ import {
type VideoRef,
type DragFillResult,
LetterToSunzhongshan,
StudyGuideModal,
Narration,
EndingOverlay,
} from "@/components";
@@ -28,10 +27,16 @@ type QuizAnchor = {
time: number;
title: string;
prompt: string;
// 拖拽题专用:题干后缀(空白区域之后的文本)
promptSuffix?: string;
type: "single" | "multiple" | "drag";
options: QuizOption[];
// 拖拽题专用
blanks?: { id: string; label: string }[];
// 拖拽题专用:空白之间的分隔符数组
blankSeparators?: string[];
// 拖拽题正确答案映射:blankId -> optionValue
correctAnswersMap?: Record<string, string>;
explanation?: string;
};
@@ -42,54 +47,52 @@ const starName = "sunzhongshan";
const QUIZ_ANCHORS: QuizAnchor[] = [
{
id: "quiz-1",
time: 11, //5.52分钟
time: 60 + 13, //5.52分钟
title: "题目1",
prompt: '"振兴中华"对我们新时代青年还有意义吗?',
type: "multiple",
prompt:
'【拖拽】习近平总书记在纪念孙中山先生诞辰150周年大会上指出: "中国共产党人是孙中山先生革命事业最坚定的',
promptSuffix: '、最忠实的继承者。"',
type: "drag",
blanks: [
{ id: "blank-1", label: "最坚定的" },
{ id: "blank-2", label: "最忠诚的" },
],
options: [
{
value: "A",
label: "在民族复兴的征程上,解决发展中的新问题、实现更高水平的发展",
value: "支持者",
label: "支持者",
isCorrect: true,
},
{
value: "B",
label:
"当时的中国正遭受帝国主义列强的野蛮侵略和封建专制制度的腐朽统治,对于新时代的意义不大",
},
{
value: "C",
label:
"我们要专注学习,努力提升自身技能和素养,用精益求精的精神服务于中国智造",
isCorrect: true,
},
{
value: "D",
label: "中华民族的伟大复兴离不开我们每一位新时代青年的奋斗",
value: "合作者",
label: "合作者",
isCorrect: true,
},
],
explanation:
"“振兴中华”需要每一代人的努力,先生当年在上海撰写的《实业计划》里如今已经实现的那些宏伟计划就是我们国家代代相传的结果。答案B的观点错误",
blankSeparators: ["、最忠诚的"],
correctAnswersMap: {
"blank-1": "支持者",
"blank-2": "合作者",
},
},
{
id: "quiz-2",
time: 42,
time: 60 * 5 + 40,
title: "题目2",
prompt: "【多选】新时代的青年要从孙中山先生身上学习哪些精神?",
type: "multiple",
options: [
{
value: "A",
label: "A.“振兴中华”的初心",
label: "“振兴中华”的初心",
isCorrect: true,
},
{
value: "B",
label: "B.“天下为公”的情怀",
label: "“天下为公”的情怀",
isCorrect: true,
},
{ value: "C", label: "C.“与时俱进”的智慧", isCorrect: true },
{ value: "C", label: "“与时俱进”的智慧", isCorrect: true },
],
},
];
@@ -162,25 +165,47 @@ const ChenyunInternalization = () => {
setDragQuizValues((prev) => ({ ...prev, [item.id]: val }));
setDragQuizErrors((prev) => ({ ...prev, [item.id]: "" }));
if (val.length < correctAnswers.length) return;
if (!item.blanks || val.length < item.blanks.length) return;
// 检查是否包含了所有正确答案(不要求顺序)
const filledAnswers = val.map((v) => v.optionValue);
const answerSet = new Set(correctAnswers);
const filledSet = new Set(filledAnswers);
const isCorrect =
answerSet.size === filledSet.size &&
[...answerSet].every((answer) => filledSet.has(answer));
// 如果有位置特定的答案映射,使用它进行验证
if (item.correctAnswersMap) {
const isCorrect = item.blanks.every((blank) => {
const filledResult = val.find(
(v) => v.blankId === blank.id
);
const correctAnswer = item.correctAnswersMap![blank.id];
return filledResult?.optionValue === correctAnswer;
});
if (isCorrect) {
setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
setResumeCallback(() => resume);
if (isCorrect) {
setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
setResumeCallback(() => resume);
} else {
setDragQuizErrors((prev) => ({
...prev,
[item.id]: "再想一想,选择正确答案~",
}));
}
} else {
setDragQuizErrors((prev) => ({
...prev,
[item.id]: "再想一想,确保选择了所有正确答案~",
}));
// 如果没有位置特定的映射,使用原来的逻辑(检查是否包含了所有正确答案)
const filledAnswers = val.map((v) => v.optionValue);
const answerSet = new Set(correctAnswers);
const filledSet = new Set(filledAnswers);
const isCorrect =
answerSet.size === filledSet.size &&
[...answerSet].every((answer) => filledSet.has(answer));
if (isCorrect) {
setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
setResumeCallback(() => resume);
} else {
setDragQuizErrors((prev) => ({
...prev,
[item.id]: "再想一想,选择正确答案~",
}));
}
}
};
@@ -189,14 +214,15 @@ const ChenyunInternalization = () => {
<DragQuizPanel
visible={activeSuccess !== item.id}
zIndex={10000}
title={`${item.title}(拖拽)`}
questionPrefix={`${item.prompt}(`}
questionSuffix=")?"
title={``}
questionPrefix={item.prompt}
questionSuffix={item.promptSuffix || ""}
options={item.options.map((opt) => ({
value: opt.value,
label: opt.label,
}))}
blanks={item.blanks || []}
blankSeparators={item.blankSeparators}
value={dragQuizValues[item.id] || []}
error={dragQuizErrors[item.id]}
onChange={handleDragChange}
@@ -241,7 +267,7 @@ const ChenyunInternalization = () => {
<MultipleChoiceQuizPanel
visible={activeSuccess !== item.id}
zIndex={10000}
title={`${item.title}(多选题):${item.prompt}`}
title={`${item.prompt}`}
options={item.options.map((opt) => ({
value: opt.value,
label: `${opt.value}. ${opt.label}`,
@@ -361,7 +387,7 @@ const ChenyunInternalization = () => {
showMask={false}
>
<GuideStepsItem speaker="student">
小沪,参观完故居,我对孙中山先生从"医人"到"医国"的转变、《实业计划》里的宏大构想有了很多感触,但总觉得这些历史和精神离我们的生活还有些远,不知道该怎么真正理解和传承?
小沪,参观完故居,我对孙中山先生从"医人"到"医国"的转变、《实业计划》里的宏大构想有了很多感触,但总觉得这些历史和精神离我们的生活还有些远,不知道该怎么真正理解和传承。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
你的感受特别真实!孙中山先生的精神不是停留在书本里的文字,而是能指导我们当下行动的思想财富。今天我们特别邀请到了上海孙中山故居纪念馆研究部主任孙旻老师,让她带我们一起解读先生精神的时代意义,把这份传承落到实处。
+8 -2
View File
@@ -405,8 +405,8 @@ const SunzhongshanPlace1 = () => {
<DragQuizPanel
visible={dragQuiz4Visible}
zIndex={10000}
title="【拖拽】"
questionPrefix="三民主义是孙中山所倡导的民主革命纲领,是其民主思想的精髓和高度概括,具体包括"
title=""
questionPrefix="【拖拽】三民主义是孙中山所倡导的民主革命纲领,是其民主思想的精髓和高度概括,具体包括"
questionSuffix="。"
options={[
{ value: "民族", label: "民族" },
@@ -522,6 +522,12 @@ const SunzhongshanPlace1 = () => {
<GuideStepsItem speaker="guide">
可以说,这是当时客观条件限制下的一种无奈妥协。因为革命党人的首要目标是推翻帝制,在武装力量不足以对抗北洋军阀的情况下,只能暂时信任袁世凯,希望他能维护共和。你看旁边的"南北议和"纪念章和纪念墨盒,就是当时这段历史的见证。
</GuideStepsItem>
<GuideStepsItem speaker="student">
可后来袁世凯不是复辟当皇帝了吗?那孙中山先生是不是特别后悔呀?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
的确,所以辛亥革命未能改变旧中国半殖民地半封建的社会性质和中国人民的悲惨境遇,也未能完成实现民族独立、人民解放的历史任务。孙中山先生十分痛心,但也更坚定了捍卫共和的决心。这也为他后来发起二次革命、护法运动埋下了伏笔。
</GuideStepsItem>
</GuideSteps>
{/* 光标1的多选题 */}
+16 -16
View File
@@ -37,9 +37,9 @@ const SunzhongshanPlace2 = () => {
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: {
2: [1],
3: [2],
4: [3],
2: [3],
3: [4],
4: [1],
},
visited: visited,
});
@@ -55,9 +55,9 @@ const SunzhongshanPlace2 = () => {
const [guide4Visible, setGuide4Visible] = useState(false);
const [guide4Step, setGuide4Step] = useState(0);
// 光标3的视频状态
const [showVideo3, setShowVideo3] = useState(false);
const video3Ref = useRef<VideoRef>(null);
// 光标2的视频状态
const [showVideo2, setShowVideo2] = useState(false);
const video2Ref = useRef<VideoRef>(null);
const handleOpenExhibit = (index: 1 | 2 | 3 | 4) => {
setActiveExhibitId(index);
@@ -162,13 +162,13 @@ const SunzhongshanPlace2 = () => {
setActiveExhibitId(null);
setShowVideo1(true);
return;
} else if (currentId === 2) {
// 光标2:关闭窗口后播放视频
setActiveExhibitId(null);
setShowVideo2(true);
return;
} else if (currentId === 4) {
// 光标4:正常关闭,更新visited状态
} else if (currentId === 3) {
// 光标3:关闭窗口后播放视频
setActiveExhibitId(null);
setShowVideo3(true);
return;
}
if (currentId !== null) {
setVisited((prev) => {
@@ -248,21 +248,21 @@ const SunzhongshanPlace2 = () => {
</GuideStepsItem>
</GuideSteps>
{/* 光标3的视频 */}
{showVideo3 && (
{/* 光标2的视频 */}
{showVideo2 && (
<Video
ref={video3Ref}
ref={video2Ref}
src={getPublicPath("video/sunzhongshan/sunzhongshan_06.mp4")}
className="fixed inset-0 z-[20000]"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo3(false);
setShowVideo2(false);
// 修改cursor状态
setVisited((prev) => {
const next = new Set(prev);
next.add(3);
next.add(2);
return next;
});
}}
+5 -5
View File
@@ -37,11 +37,11 @@ const SunzhongshanPlace3 = () => {
// Cursor 顺序控制:1 -> 2 -> 3 -> 4
const { isEnabled } = useCursorController({
initialEnabled: [1],
initialEnabled: [4],
dependencies: {
1: [3],
2: [1],
3: [2],
4: [3],
3: [4],
},
visited: visited,
});
@@ -262,8 +262,8 @@ const SunzhongshanPlace3 = () => {
<DragQuizPanel
visible={dragQuiz2Visible}
zIndex={10000}
title="【拖拽】"
questionPrefix="3. 中国国民党第一次全国代表大会事实上确立了"
title=""
questionPrefix="【拖拽】中国国民党第一次全国代表大会事实上确立了:"
questionSuffix="三大政策。"
options={[
{ value: "联俄", label: "联俄" },