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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+6
View File
@@ -35,11 +35,17 @@ export {
QuizPanel,
SingleChoiceQuizPanel,
MultipleChoiceQuizPanel,
DragQuizPanel,
MatchQuizPanel,
SelectFillQuizPanel,
} from "./layout/quiz";
export type {
QuizPanelProps,
SingleChoiceQuizPanelProps,
MultipleChoiceQuizPanelProps,
DragQuizPanelProps,
MatchQuizPanelProps,
SelectFillQuizPanelProps,
} from "./layout/quiz";
//Cursor
+83 -5
View File
@@ -6,8 +6,85 @@
- **SingleChoiceQuizPanel** - 单选题面板
- **MultipleChoiceQuizPanel** - 多选题面板
- **SelectFillQuizPanel** - 选择填空题面板
- **QuizPanel** - 旧版组件(已废弃,保留以保持向后兼容)
## SelectFillQuizPanel - 选择填空题面板
### 功能特性
- ✅ 支持多个空依次填写
- ✅ 下方提供一组可点击选项(如数字 0-9)
- ✅ 内部管理答题状态与当前激活空
- ✅ 支持正确答案校验与错误提示
- ✅ 关闭面板时自动重置内部状态
### 使用示例
```tsx
import { SelectFillQuizPanel } from "@/components";
function MyComponent() {
const [showQuiz, setShowQuiz] = useState(false);
return (
<SelectFillQuizPanel
visible={showQuiz}
titlePrefix="【填空】毛泽东主席认为,到了"
titleSuffix="年夏天,在理论上,而且在某种程度的行动上,他已成为一个马克思主义者。"
blanks={[
{ id: "year", prefix: "____" },
]}
options={[
{ value: "1919", label: "1919" },
{ value: "1920", label: "1920" },
{ value: "1921", label: "1921" },
]}
correctAnswers={{ year: "1920" }}
errorMessage="回想一下 1920 年 5 月到 7 月在上海居住的这段经历~"
onSubmit={(results, isCorrect) => {
console.log("选择结果:", results, "是否正确:", isCorrect);
if (isCorrect) {
setShowQuiz(false);
}
}}
onError={(error) => {
console.log("错误信息:", error);
}}
submitText="确定"
/>
);
}
```
### API
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| visible | boolean | - | 是否可见(必填) |
| zIndex | number | 10000 | z-index 层级 |
| titlePrefix | string | - | 整体题干前缀文本(可选) |
| titleSuffix | string | - | 整体题干后缀文本(可选) |
| blanks | SelectFillBlank[] | - | 每个填空位的配置(必填,包含 id / prefix / suffix) |
| options | { value: string; label: string }[] | - | 候选项列表(必填) |
| correctAnswers | Record<string, string> | - | 正确答案映射(可选,不传则不做校验,直接判定为正确) |
| errorMessage | string | `"还不太对,再仔细对照题干和提示想一想吧~"` | 自定义错误提示信息(答案错误时显示) |
| onSubmit | (results: SelectFillResult[], isCorrect: boolean) => void | - | 提交回调 |
| onError | (error: string) => void | - | 错误回调 |
| disabled | boolean | false | 是否禁用 |
| submitText | string | "提交" | 提交按钮文本 |
### 行为说明
1. **点击空格**:切换当前激活的填空位。
2. **点击下方选项**:将选项填入当前激活的空中,并自动跳转到下一个未填写的空。
3. **点击空中的 ×**:清除该空的内容,并把当前激活位置移动到该空。
4. **点击提交**:如果有空未填,给出“请先把所有空都填完再提交哦~”提示;如果提供了 `correctAnswers`,会逐空校验是否全部正确。
5. **答案正确**:调用 `onSubmit(results, true)`,同时清除错误提示。
6. **答案错误**:显示错误提示,仅调用 `onError(error)`,不调用 `onSubmit(false)`。
7. **未提供正确答案**:直接调用 `onSubmit(results, true)`,不进行校验。
8. **关闭面板**(`visible` 从 `true` 变为 `false`):内部会自动重置所有填空和错误提示。
## SingleChoiceQuizPanel - 单选题面板
### 功能特性
@@ -16,7 +93,7 @@
- ✅ 点击选项后自动提交
- ✅ 支持答案验证(可选)
- ✅ 自动显示错误提示
- ✅ 提交后禁用交互
- ✅ 关闭面板时自动重置内部状态
### 使用方法
@@ -72,9 +149,10 @@ function MyComponent() {
### 行为说明
1. **点击选项**:立即触发提交,如果提供了 `correctAnswer`,会自动验证
2. **答案正确**:调用 `onSubmit(value, true)`,面板进入已提交状态
3. **答案错误**:显示错误提示,调用 `onError(error)` 和 `onSubmit(value, false)`
2. **答案正确**:调用 `onSubmit(value, true)`,并清空内部选择与错误状态
3. **答案错误**:显示错误提示,仅调用 `onError(error)`,并清空内部选择状态
4. **未提供正确答案**:直接调用 `onSubmit(value, true)`,不进行验证
5. **关闭面板**(`visible` 从 `true` 变为 `false`):内部会自动重置已选择的值和错误提示
## MultipleChoiceQuizPanel - 多选题面板
@@ -205,7 +283,7 @@ function MyComponent() {
## 注意事项
1. 两个组件都使用内部状态管理,不需要外部传入 `value`
2. 提交后组件会进入禁用状态,防止重复提交
3. 如果提供了 `correctAnswer`,组件会自动验证并显示错误信息
2. 如果提供了 `correctAnswer`,组件会自动验证并显示错误信息
3. 当 `visible` 从 `true` 变为 `false` 时,单选题和多选题面板都会自动重置内部选择与错误状态,便于下次重新答题
4. 多选题的答案比较不考虑顺序,只要选择的选项和正确答案一致即可
5. 旧版 `QuizPanel` 仍然可用,但建议迁移到新组件
@@ -0,0 +1,239 @@
import { useEffect, useState } from "react";
import type {
SelectFillQuizPanelProps,
SelectFillResult,
} from "./types";
/**
* 选择填空题面板组件
*
* 示例场景:给出一句话中若干空格,在下方提供一排数字或短语,学生点击选中后依次填入空格,最后统一提交判断正误。
*/
const SelectFillQuizPanel = ({
visible,
zIndex = 10000,
titlePrefix,
titleSuffix,
blanks,
options,
correctAnswers,
errorMessage,
onSubmit,
onError,
disabled = false,
submitText = "提交",
}: SelectFillQuizPanelProps) => {
const [results, setResults] = useState<SelectFillResult[]>([]);
const [error, setError] = useState<string>("");
// 当前正在填的空的下标(从 0 开始),默认为第一个空
const [activeBlankIndex, setActiveBlankIndex] = useState(0);
// 关闭时重置内部状态
useEffect(() => {
if (!visible) {
setResults([]);
setError("");
setActiveBlankIndex(0);
}
}, [visible]);
if (!visible) return null;
const isAllFilled = blanks.every((blank) =>
results.some((r) => r.blankId === blank.id && r.value !== "")
);
// 点击下方选项:填入当前激活的空
const handleOptionClick = (value: string) => {
if (disabled) return;
const currentBlank = blanks[activeBlankIndex];
if (!currentBlank) return;
const newResults: SelectFillResult[] = (() => {
const others = results.filter((r) => r.blankId !== currentBlank.id);
return [...others, { blankId: currentBlank.id, value }];
})();
setResults(newResults);
setError("");
// 自动跳到下一个空
const nextIndex = (() => {
// 优先跳到还未填写的下一个空
for (let i = 0; i < blanks.length; i++) {
const blankItem = blanks[i]!;
const hasValue = newResults.some(
(r) => r.blankId === blankItem.id && r.value !== ""
);
if (!hasValue) return i;
}
return activeBlankIndex;
})();
setActiveBlankIndex(nextIndex);
};
// 清空某个空
const handleClearBlank = (blankId: string) => {
if (disabled) return;
setResults(results.filter((r) => r.blankId !== blankId));
setError("");
const index = blanks.findIndex((b) => b.id === blankId);
if (index >= 0) setActiveBlankIndex(index);
};
// 提交答案
const handleSubmit = () => {
if (disabled) return;
// 有空未填
if (!isAllFilled) {
const msg = "请先把所有空都填完再提交哦~";
setError(msg);
onError?.(msg);
return;
}
const filledResults = blanks.map((blank) => {
const r = results.find((res) => res.blankId === blank.id);
return { blankId: blank.id, value: r?.value ?? "" };
});
let isCorrect = true;
if (correctAnswers) {
for (const r of filledResults) {
const expected = correctAnswers[r.blankId];
if (expected !== undefined && expected !== r.value) {
isCorrect = false;
break;
}
}
}
if (!correctAnswers) {
onSubmit?.(filledResults, true);
return;
}
if (isCorrect) {
setError("");
onSubmit?.(filledResults, true);
} else {
const msg =
errorMessage || "还不太对,再仔细对照题干和提示想一想吧~";
setError(msg);
// 错误后清空已填写内容,重新作答
setResults([]);
setActiveBlankIndex(0);
onError?.(msg);
}
};
const getBlankValue = (blankId: string) =>
results.find((r) => r.blankId === blankId)?.value ?? "";
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/40"
style={{ zIndex }}
>
<div className="relative w-[1728px] h-[972px] flex flex-col items-center justify-center">
{/* 题干 + 空格区域 */}
<div className="w-[1700px] rounded-[32px] bg-black px-20 py-12 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]">
<div className="text-[32px] leading-relaxed text-white">
{titlePrefix && <span>{titlePrefix}</span>}
{blanks.map((blank, index) => {
const value = getBlankValue(blank.id);
const isActive = index === activeBlankIndex;
return (
<span key={blank.id}>
{blank.prefix && <span>{blank.prefix}</span>}
<button
type="button"
className={`inline-flex items-center justify-center min-w-[120px] h-[52px] px-4 mx-2 rounded-xl border-2 text-[28px] transition-all ${
disabled
? "opacity-60 cursor-not-allowed border-[#d4c5a9]/40"
: "cursor-pointer"
} ${
value
? "border-[#f0be75] bg-gradient-to-br from-[#3a3020] to-[#2a2520] text-[#f0be75]"
: "border-[#d4c5a9]/60 bg-white/10 text-white/40"
} ${
isActive && !disabled
? "ring-2 ring-[#f0be75] ring-offset-2 ring-offset-black"
: ""
}`}
onClick={() => {
if (disabled) return;
setActiveBlankIndex(index);
}}
>
{value || "____"}
{value && !disabled && (
<span
className="ml-3 text-[22px] text-[#f0be75]/80"
onClick={(e) => {
e.stopPropagation();
handleClearBlank(blank.id);
}}
>
×
</span>
)}
</button>
{blank.suffix && <span>{blank.suffix}</span>}
</span>
);
})}
{titleSuffix && <span>{titleSuffix}</span>}
</div>
{/* 答案反馈 */}
{error && (
<div className="mt-6 text-center text-[26px] text-[#fcbe53]">
{error}
</div>
)}
</div>
{/* 选项区域 */}
<div className="mt-16 w-[1500px] flex flex-col items-center">
<div className="flex flex-wrap justify-center gap-6">
{options.map((option) => (
<button
key={option.value}
type="button"
disabled={disabled}
className={`min-w-[90px] h-[72px] px-6 rounded-2xl text-[28px] font-medium border-2 transition-all ${
disabled
? "opacity-60 cursor-not-allowed bg-[#d7b888]/60 border-[#d4c5a9]/60 text-[#5c3e2e]/60"
: "cursor-pointer bg-[#d7b888] border-[#d4c5a9] text-[#5c3e2e] hover:bg-gradient-to-r hover:from-[#ddf3e1] hover:to-[#fbdd97] hover:border-[#f0be75]"
}`}
onClick={() => handleOptionClick(option.value)}
>
{option.label}
</button>
))}
</div>
{/* 提交按钮 */}
<div className="mt-10">
<button
type="button"
onClick={handleSubmit}
disabled={disabled || !isAllFilled}
className="px-24 py-4 text-[28px] font-medium text-white bg-[#c2995f] rounded-[40px] transition-all duration-200 hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
>
{submitText}
</button>
</div>
</div>
</div>
</div>
);
};
export default SelectFillQuizPanel;
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { SingleChoice } from "@/components";
import type { SingleChoiceQuizPanelProps } from "./types";
@@ -42,6 +42,14 @@ const SingleChoiceQuizPanel = ({
const [selectedValue, setSelectedValue] = useState<string>("");
const [error, setError] = useState<string>("");
// 当窗口关闭时,重置内部状态
useEffect(() => {
if (!visible) {
setSelectedValue("");
setError("");
}
}, [visible]);
if (!visible) return null;
// 处理选项点击
+4
View File
@@ -3,6 +3,7 @@ export { default as SingleChoiceQuizPanel } from "./SingleChoiceQuizPanel";
export { default as MultipleChoiceQuizPanel } from "./MultipleChoiceQuizPanel";
export { default as DragQuizPanel } from "./DragQuizPanel";
export { default as MatchQuizPanel } from "./MatchQuizPanel";
export { default as SelectFillQuizPanel } from "./SelectFillQuizPanel";
export type {
SingleChoiceQuizPanelProps,
MultipleChoiceQuizPanelProps,
@@ -10,6 +11,9 @@ export type {
MatchQuizPanelProps,
MatchQuizItem,
MatchQuizMatch,
SelectFillQuizPanelProps,
SelectFillBlank,
SelectFillResult,
} from "./types";
// 保留旧组件以保持向后兼容
+50
View File
@@ -125,6 +125,56 @@ export interface MatchQuizPanelProps {
disabled?: boolean;
}
/** 选择填空题中,每一个空的配置 */
export interface SelectFillBlank {
/** 唯一标识,用于和答案、结果关联 */
id: string;
/** 空前面的文本(可选) */
prefix?: string;
/** 空后面的文本(可选) */
suffix?: string;
}
/** 选择填空题的答题结果 */
export interface SelectFillResult {
blankId: string;
value: string;
}
/** 选择填空题面板配置 */
export interface SelectFillQuizPanelProps {
/** 是否可见 */
visible: boolean;
/** z-index 层级 */
zIndex?: number;
/** 题目前缀整体文本(可选) */
titlePrefix?: string;
/** 题目后缀整体文本(可选) */
titleSuffix?: string;
/** 填空位配置(控制有几个空、空前后的说明文本) */
blanks: SelectFillBlank[];
/** 可选项列表(如 0-9 的数字、若干短语等) */
options: { value: string; label: string }[];
/**
* 正确答案映射
* key 为 blankId,value 为该空对应的正确值
*/
correctAnswers?: Record<string, string>;
/** 自定义错误提示信息(答案错误时显示) */
errorMessage?: string;
/**
* 提交回调,返回用户选择结果与是否全部正确
* 注意:如果未提供 correctAnswers,则 isCorrect 恒为 true
*/
onSubmit?: (results: SelectFillResult[], isCorrect: boolean) => void;
/** 错误回调(可选) */
onError?: (error: string) => void;
/** 是否禁用 */
disabled?: boolean;
/** 提交按钮文本 */
submitText?: string;
}
/** @deprecated 使用 SingleChoiceQuizPanelProps 替代 */
export interface QuizPanelProps {
visible: boolean;
-2
View File
@@ -36,12 +36,10 @@ const Maozedong = () => {
<Image
button
src={getStaticImage("star/二-解锁")}
onClick={() => navigate(`/${moduleName}/${starName}/place2`)}
/>
<Image
button
src={getStaticImage("star/三-解锁")}
onClick={() => navigate(`/${moduleName}/${starName}/place3`)}
/>
</div>
</div>
+174 -15
View File
@@ -3,13 +3,18 @@ import {
GuideSteps,
GuideStepsItem,
Image,
StudyGuideModal,
Title,
SelectFillQuizPanel,
} from "@/components";
import { useNavigate, useParams } from "react-router-dom";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { 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";
import type { SelectFillResult } from "@/components/layout/quiz";
// starName 固定为 maozedong
const starName = "maozedong";
@@ -26,25 +31,73 @@ const MaozedongPlace1 = () => {
moduleName: string;
}>();
// 引导对话显示状态
const [visible, setVisible] = useState(false);
// 学习引导弹窗
const [showGuideModal, setShowGuideModal] = useState(true);
// 对话结束后的学习引导弹窗
const [showAfterDialogGuide, setShowAfterDialogGuide] = useState(false);
// 展品1的对话引导状态
const [guideVisible1, setGuideVisible1] = useState(false);
const [guideStep1, setGuideStep1] = useState(0);
// 展品2的对话引导状态
const [guideVisible2, setGuideVisible2] = useState(false);
const [guideStep2, setGuideStep2] = useState(0);
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
// 已访问的展品(本页内状态;如需持久化可接入 store)
const [visited, setVisited] = useState<Set<1 | 2>>(() => new Set());
// 展品2对话结束后的选择填空题
const [quizVisible, setQuizVisible] = useState(false);
const [quizPassed, setQuizPassed] = useState(false);
const [successAlertVisible, setSuccessAlertVisible] = useState(false);
// Cursor 顺序控制:1 -> 2
const { isEnabled } = useCursorController({
initialEnabled: [1],
initialEnabled: [2],
dependencies: {
2: [1],
1: [2],
},
visited,
});
const handleOpenExhibit = (id: 1 | 2) => {
setActiveExhibitId(id);
// 根据展品ID显示对应的对话
if (id === 1) {
setGuideVisible2(true);
setGuideStep2(0);
} else if (id === 2) {
setGuideVisible1(true);
setGuideStep1(0);
}
};
// 监听展品1对话引导步骤变化
const handleGuideStepChange1 = (step: number) => {
setGuideStep1(step);
};
// 监听展品1对话引导完成
const handleGuideVisibleChange1 = (visible: boolean) => {
setGuideVisible1(visible);
// 对话结束后,如果还未完成该题,则弹出选择填空题
if (!visible) {
setQuizVisible(true);
}
};
// 监听展品2对话引导步骤变化
const handleGuideStepChange2 = (step: number) => {
setGuideStep2(step);
};
// 监听展品2对话引导完成
const handleGuideVisibleChange2 = (visible: boolean) => {
setGuideVisible2(visible);
};
const progressCount = visited.size;
@@ -108,28 +161,54 @@ const MaozedongPlace1 = () => {
/>
)}
{/* 场景对话 */}
{/* 展品2的对话引导 */}
<GuideSteps
visible={visible}
onVisibleChange={(nextVisible) => {
setVisible(nextVisible);
}}
visible={guideVisible1}
onVisibleChange={handleGuideVisibleChange1}
currentStep={guideStep1}
onStepChange={handleGuideStepChange1}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
小沪,毛泽东是如何确立马克思主义信仰的?
小沪!我了解过,毛主席是以湖南代表身份参加中共一大的。但他之前不是在湖南一师读书,后来组织新民学会吗?怎么想到要建全国性的共产党呀?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1918年,毛泽东第一次来到北京,在北大图书馆担任助理员期间,他接触到了马克思主义。
同学问得好!毛主席在一师读书时,受杨昌济先生影响,读了很多进步书刊。后来和蔡和森、萧子升成立新民学会,最初只是想"改造中国与世界"——但到底怎么改?他们争论过很多次。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1919年五四运动后,毛泽东回到湖南,创办《湘江评论》,开始宣传新思想。
后来在陈独秀先生的影响下,让他坚定地认为如果要进行改造,就必须来场革命!如果革命成功,上策便是学习苏联,而马克思主义成为他后续抉择的指引。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1920年,毛泽东第二次来到北京,阅读了《共产党宣言》等马克思主义著作,思想上发生了根本转变。
“我第二次到北京期间,读了很多关于俄国情况的书。我热心地搜寻那时候能找到的为数不多的用中文写的共产主义书籍,有三本书特别深地铭刻在我的心中,建立起我对马克思主义的信仰。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1920年11月,毛泽东在长沙建立了共产主义小组,标志着他在思想上和组织上都确立了马克思主义信仰。
我一旦接受了马克思主义是对历史的正确解释以后,我对马克思主义的信仰就没有动摇过。这三本书是《共产党宣言》,陈望道译,这是用中文出版的第一本马克思主义的书;《阶级斗争》,考茨基著;《社会主义史》,柯卡普著。”
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1920年,毛主席从北京辗转抵达上海,1920年5月5日到7月初,在上海居住的2个月期间,毛主席的思想完成了向马克思主义者的转变。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
“到了一九二〇年夏天,在理论上,而且在某种程度的行动上,我已成为一个马克思主义者了。”毛泽东的这番表白,见载于埃德加·斯诺的《西行漫记》。
</GuideStepsItem>
</GuideSteps>
{/* 展品1的对话引导 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={handleGuideVisibleChange2}
currentStep={guideStep2}
onStepChange={handleGuideStepChange2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
那参加中共一大时,毛主席对未来有什么想法吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
当时全国只有50多个党员,毛主席所在的湖南也才几个。但他觉得,重要的是把马克思主义和中国的实际情况结合起来。就像种田,种子再好,也得看土壤——中国的"土壤"就是农民占大多数,这是和苏联不一样的地方。
</GuideStepsItem>
</GuideSteps>
@@ -148,9 +227,89 @@ const MaozedongPlace1 = () => {
setActiveExhibitId(null);
}}
/>
{/* 展品2对话结束后的选择填空题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
<SelectFillQuizPanel
visible={quizVisible}
titlePrefix="【填空】毛泽东主席认为,到了"
titleSuffix="年夏天,在理论上,而且在某种程度的行动上,他已成为一个马克思主义者了。"
blanks={[
{ id: "d1" },
{ id: "d2" },
{ id: "d3" },
{ id: "d4" },
]}
options={[
{ value: "0", label: "0" },
{ value: "1", label: "1" },
{ value: "2", label: "2" },
{ value: "3", label: "3" },
{ value: "4", label: "4" },
{ value: "5", label: "5" },
{ value: "6", label: "6" },
{ value: "7", label: "7" },
{ value: "8", label: "8" },
{ value: "9", label: "9" },
]}
correctAnswers={{
d1: "1",
d2: "9",
d3: "2",
d4: "0",
}}
errorMessage="1920年5月5日到7月初,在上海居住的2个月期间,毛主席的思想完成了向马克思主义者的转变。请你再想想吧!"
onSubmit={(_results: SelectFillResult[], isCorrect: boolean) => {
if (isCorrect) {
setQuizVisible(false);
setQuizPassed(true);
setSuccessAlertVisible(true);
}
}}
onError={(_error: string) => {
// 错误时仅提示,再让学生重做
}}
submitText="确定"
zIndex={10000}
/>
<SuccessAlert
visible={successAlertVisible}
onContinue={() => {
setSuccessAlertVisible(false);
}}
/>
</>
</ScaledPortal>
{/* 学习引导词 */}
<StudyGuideModal
visible={showGuideModal}
onContinue={() => {
setShowGuideModal(false);
setShowAfterDialogGuide(true)
}}
>
{`同学你好,欢迎来到“走进故居·对话”学习模块!
此刻你正处在毛泽东旧居陈列馆里,接下来请跟着时光的脚步,一同回望毛泽东主席人生中4段风云激荡的岁月:摸索方向 确立信仰、播撒红色火种、吹响合作号角、唤起工农群众。准备好推开这扇门,走进那段滚烫的峥嵘岁月了吗?`}
</StudyGuideModal>
{/* 对话结束后的学习引导 */}
<StudyGuideModal
visible={showAfterDialogGuide}
onContinue={() => setShowAfterDialogGuide(false)}
ailgn="left"
>
1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。
{"\n"}
2.在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点击查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习。
{"\n"}
3.所有动态光点的展品全部学习后才能进入下一个场景学习。
</StudyGuideModal>
</div>
);
};
export default MaozedongPlace1;
+140 -27
View File
@@ -4,12 +4,15 @@ import {
GuideStepsItem,
Image,
Title,
MultipleChoiceQuizPanel,
} from "@/components";
import { useNavigate, useParams } from "react-router-dom";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { 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";
@@ -26,14 +29,24 @@ const MaozedongPlace2 = () => {
moduleName: string;
}>();
// 引导对话显示状态
const [visible, setVisible] = useState(false);
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
// 已访问的展品(本页内状态;如需持久化可接入 store)
const [visited, setVisited] = useState<Set<1 | 2>>(() => new Set());
// 展品1的对话引导状态
const [guideVisible1, setGuideVisible1] = useState(false);
const [guideStep1, setGuideStep1] = useState(0);
// 展品2的对话引导状态
const [guideVisible2, setGuideVisible2] = useState(false);
const [guideStep2, setGuideStep2] = useState(0);
// 展品1对话结束后的多选题
const [quizVisible, setQuizVisible] = useState(false);
const [quizPassed, setQuizPassed] = useState(false);
const [successAlertVisible, setSuccessAlertVisible] = useState(false);
// Cursor 顺序控制:1 -> 2
const { isEnabled } = useCursorController({
initialEnabled: [1],
@@ -45,6 +58,38 @@ const MaozedongPlace2 = () => {
const handleOpenExhibit = (id: 1 | 2) => {
setActiveExhibitId(id);
if (id === 1) {
setGuideVisible1(true);
setGuideStep1(0);
// 重新进入时重置题目完成状态
setQuizPassed(false);
} else if (id === 2) {
setGuideVisible2(true);
setGuideStep2(0);
}
};
// 展品1对话步骤变化
const handleGuideStepChange1 = (step: number) => {
setGuideStep1(step);
};
// 展品1对话完成
const handleGuideVisibleChange1 = (visible: boolean) => {
setGuideVisible1(visible);
if (!visible && !quizPassed) {
setQuizVisible(true);
}
};
// 展品2对话步骤变化
const handleGuideStepChange2 = (step: number) => {
setGuideStep2(step);
};
// 展品2对话完成
const handleGuideVisibleChange2 = (visible: boolean) => {
setGuideVisible2(visible);
};
const progressCount = visited.size;
@@ -108,30 +153,6 @@ const MaozedongPlace2 = () => {
/>
)}
{/* 场景对话 */}
<GuideSteps
visible={visible}
onVisibleChange={(nextVisible) => {
setVisible(nextVisible);
}}
showMask={false}
>
<GuideStepsItem speaker="student">
小沪,毛泽东是如何传播马克思主义的?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1920年,毛泽东在长沙创办文化书社,销售进步书刊,传播新思想。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
他还在湖南第一师范学校任教,向学生宣传马克思主义理论。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1921年7月,毛泽东作为湖南代表参加了中国共产党第一次全国代表大会,成为中国共产党的创始人之一。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
回到湖南后,毛泽东建立了中共湖南支部,并担任书记,开始在湖南各地发展党员,建立党组织。
</GuideStepsItem>
</GuideSteps>
{/* 展品遮罩层 */}
<ExhibitOverlay
@@ -148,6 +169,98 @@ const MaozedongPlace2 = () => {
setActiveExhibitId(null);
}}
/>
{/* 展品1的对话引导(文字+语音播报占位) */}
<GuideSteps
visible={guideVisible1}
onVisibleChange={handleGuideVisibleChange1}
currentStep={guideStep1}
onStepChange={handleGuideStepChange1}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
中共一大定了“推翻资产阶级政权”的目标,但中共二大提
出了“反帝反封建的民主革命纲领”,为什么会有这样的转变呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
中共一大时我们中国共产党刚成立,像刚学走路的孩子,
目标很宏大——“建立无产阶级专政”“消灭阶级”。但毛主席回到湖南后,
做了实际调查发现必须先解决“眼前的敌人”。
</GuideStepsItem>
<GuideStepsItem speaker="student">
“眼前的敌人”?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
对!想想看1922年长沙的码头工人罢工,安源的路矿工人罢工,
他们的敌人是谁?是外国资本家,是军阀。中共二大时毛主席虽没有出席,
但完全赞同大会制定的“最低纲领”——打倒军阀,建立国内和平;
</GuideStepsItem>
<GuideStepsItem speaker="guide">
推翻国际帝国主义的压迫,达到中华民族的完全独立;
统一中国为真正的民主共和国。这就像盖房子,得先把地基打好;
等打倒了军阀和帝国主义,再谈社会主义也不迟。
</GuideStepsItem>
</GuideSteps>
{/* 展品2的对话引导 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={handleGuideVisibleChange2}
currentStep={guideStep2}
onStepChange={handleGuideStepChange2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="guide">
后来喊出的“打倒列强,除军阀”的口号,就是从这儿来的。
</GuideStepsItem>
</GuideSteps>
{/* 展品1对话结束后的多选题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quizVisible}
title="【多选】1922年7月16日至23日,中国共产党第二次全国代表大会在上海南成都路辅德里625号召开。大会制定了党的最低纲领和最高纲领,党的最低纲领即党在民主革命阶段的纲领是:"
options={[
{ value: "A", label: "A.打倒军阀,建立国内和平" },
{
value: "B",
label:
"B.推翻国际帝国主义的压迫,达到中华民族的完全独立",
},
{ value: "C", label: "C.统一中国为真正的民主共和国" },
]}
correctAnswer={["A", "B", "C"]}
minSelections={1}
maxSelections={3}
errorMessage={(selectedValues) => {
if (selectedValues.length < 3) {
return "你还没有全面了解中共二大提出的党的最低纲领哦。请你再想想吧!";
}
return "还不太对,再仔细对照对话内容想一想吧~";
}}
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuizVisible(false);
setQuizPassed(true);
setSuccessAlertVisible(true);
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlertVisible}
onContinue={() => {
setSuccessAlertVisible(false);
}}
/>
</>
</ScaledPortal>
</div>
);
};
+134 -25
View File
@@ -4,12 +4,17 @@ import {
GuideStepsItem,
Image,
Title,
Video,
type VideoRef,
MultipleChoiceQuizPanel,
} 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 { 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";
@@ -27,9 +32,6 @@ const MaozedongPlace3 = () => {
moduleName: string;
}>();
// 引导对话显示状态
const [visible, setVisible] = useState(false);
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<
1 | 2 | 3 | null
@@ -37,18 +39,32 @@ const MaozedongPlace3 = () => {
// 已访问的展品(本页内状态;如需持久化可接入 store)
const [visited, setVisited] = useState<Set<1 | 2 | 3>>(() => new Set());
// Cursor 顺序控制:1 -> 2 -> 3
// 光标3对应的对话状态
const [guide3Visible, setGuide3Visible] = useState(false);
const [guide3Step, setGuide3Step] = useState(0);
// 光标3的视频和题目状态
const [showVideo3, setShowVideo3] = useState(false);
const video3Ref = useRef<VideoRef>(null);
const [quiz3Visible, setQuiz3Visible] = useState(false);
const [successAlert3Visible, setSuccessAlert3Visible] = useState(false);
// Cursor
const { isEnabled } = useCursorController({
initialEnabled: [1],
initialEnabled: [3],
dependencies: {
2: [1],
3: [1, 2],
2: [3],
1: [3],
},
visited,
});
const handleOpenExhibit = (id: 1 | 2 | 3) => {
setActiveExhibitId(id);
if (id === 3) {
setGuide3Step(0);
setGuide3Visible(true);
}
};
const progressCount = visited.size;
@@ -119,28 +135,27 @@ const MaozedongPlace3 = () => {
/>
)}
{/* 场景对话 */}
{/* 光标3的对话(点击展品3时出现) */}
<GuideSteps
visible={visible}
visible={guide3Visible}
currentStep={guide3Step}
onStepChange={setGuide3Step}
onVisibleChange={(nextVisible) => {
setVisible(nextVisible);
setGuide3Visible(nextVisible);
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="student">
小沪,毛泽东是如何推动国共合作的?
中共三大决定和国民党合作,但党内有争论。毛主席当时是什
么态度呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1923年6月,中共三大在广州召开,毛泽东参加了会议,会议决定与国民党合作,建立革命统一战线。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1924年1月,国民党一大召开,标志着第一次国共合作正式形成。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
毛泽东在国民党内担任重要职务,积极推动两党合作,共同开展反帝反封建的革命斗争。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
在合作期间,毛泽东深入农村,开展农民运动,为革命积累了重要力量。
争论确实激烈!有人觉得和国民党合作是“降低身份”,但毛
主席认为这是必要的。当时孙中山先生领导的国民党有革命传
统,但缺真正的革命力量;我们有马克思主义,但缺群众基础。
国共合作就好比“星火借烈焰,共燃燎原势”。
</GuideStepsItem>
</GuideSteps>
@@ -149,16 +164,110 @@ const MaozedongPlace3 = () => {
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onClose={() => {
if (activeExhibitId !== null) {
const currentId = activeExhibitId;
if (currentId === 3) {
// 关闭展品3窗口:结束对话,播放视频
setGuide3Step(0);
setGuide3Visible(false);
setActiveExhibitId(null);
setShowVideo3(true);
return;
}
if (currentId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
next.add(currentId);
return next;
});
}
setActiveExhibitId(null);
}}
/>
{/* 光标3的视频:播放结束后出现多选题 */}
{showVideo3 && (
<Video
ref={video3Ref}
src={getPublicPath("video/sunzhongshan/sunzhongshan_03.mp4")}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo3(false);
setQuiz3Visible(true);
}}
/>
)}
{/* 光标3的视频后的多选题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quiz3Visible || successAlert3Visible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quiz3Visible}
zIndex={10000}
title="【多选】1923年6月12日至20日,中国共产党第三次全国代表大会在广州召开。以下符合中共三大核心内容及历史意义的有:"
options={[
{
value: "A",
label:
"A.大会中心议题是讨论全体共产党员以个人身份加入国民党,实现国共“党内合作”",
},
{
value: "B",
label:
"B.大会强调共产党党员加入国民党后,需保持政治、思想和组织上的独立性",
},
{
value: "C",
label:
"C.大会标志着中国共产党以国共合作为基础的统一战线政策正式形成",
},
{
value: "D",
label:
"D.大会提出了“打倒军阀、推翻帝国主义”的革命目标",
},
]}
correctAnswer={["A", "B", "C"]}
minSelections={1}
maxSelections={4}
errorMessage={(selectedValues) => {
if (
selectedValues.includes("D")
) {
return "“打倒军阀、推翻帝国主义”的革命目标是中共二大提出的,中共三大是肯定该目标的正确性,并明确通过国共合作推进这一目标的实现。请你再想想吧!";
}
if (selectedValues.length < 3) {
return "恭喜你,答案已经十分接近!请再结合上下文仔细完善吧。";
}
return "请再仔细分析一下题目,做出更全面的选择吧!";
}}
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuiz3Visible(false);
setSuccessAlert3Visible(true);
// 答题成功后标记展品3已访问
setVisited((prev) => {
const next = new Set(prev);
next.add(3);
return next;
});
}
}}
/>
<SuccessAlert
content="你理解了中共三大和国民党合作的核心内容及历史意义"
visible={successAlert3Visible}
onContinue={() => {
setSuccessAlert3Visible(false);
}}
/>
</>
</ScaledPortal>
</div>
);
};
+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>
);
};