feat: 更新图像资源和引导对话,提升学习体验

- 替换多个场景中的图像资源,确保视觉内容的准确性和一致性。
- 更新信件组件中的输入校验功能,提升用户填写体验。
- 调整引导对话文本,增强互动性和信息传达的清晰度。
- 引入结束窗口,鼓励用户深入了解鲁迅和孙中山的历史背景。
This commit is contained in:
zhongyidian committed 2026-01-18 01:41:53 +08:00
1 parent 5bdfdadede
commit ac7bb420fd
69 files changed
+968 -244

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 KiB

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

After

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

After

Width:  |  Height:  |  Size: 706 KiB

+265 -14
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image } from "@/components";
import { Image, Select } from "@/components";
export interface LetterToLuxunProps {
/** 是否可见 */
@@ -23,15 +23,88 @@ const LetterToLuxun = ({
}: LetterToLuxunProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([]);
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
// 填空类型定义:0=填空1(限填),1=填空2(限填),其他=任填
const BLANK_1_ANSWER = "首在立人";
const BLANK_2_ANSWER = "批判求真";
// 填空1的选项
const BLANK_1_OPTIONS = [
"首在立人",
// 可以添加其他选项
];
// 填空2的选项
const BLANK_2_OPTIONS = [
"批判求真",
// 可以添加其他选项
];
// 校验输入:确保所有填空符合标准
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
const nextErrors: boolean[] = Array(placeholderCount).fill(false);
const nextValues: string[] = [...blankValues];
let hasError = false;
for (let i = 0; i < placeholderCount; i++) {
const raw = nextValues[i] || "";
const v = raw.trim();
if (i === 0) {
// 填空1:限填,必须选择"首在立人"
if (!v || v !== BLANK_1_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
} else if (i === 1) {
// 填空2:限填,必须选择"批判求真"
if (!v || v !== BLANK_2_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
} else {
// 其他填空:任填,不能为空,不超过 10 个字
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
}
}
setBlankValues(nextValues);
setBlankErrors(nextErrors);
return !hasError;
};
// 弹窗关闭后重置内容,方便下次重新填写
useEffect(() => {
if (!visible) {
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
}
}, [visible]);
// 信件内容模板(根据设计稿)
const letterContent = `敬爱的鲁迅先生:
您好!
今天,我怀着崇敬的心情走进大陆新村9号,看着您亲手撰写的《呐喊》《且介亭杂文》《故事新编》等作品,您编辑的《萌芽》《前哨》等杂志,您翻译的《艺术论》《死魂灵》等译文,仿佛能看到您"舌将上下而求索"的身影。在这场跨越时空的对话中,我终于明白您坚守的"____",不是抽象的理念,而是我们安身立命的根本;您秉持的"____",不是无谓的锋芒,而是我们直面现实的勇气。
今天,我怀着崇敬的心情走进大陆新村9号,看着您亲手撰写的《呐喊》《且介亭杂文》《故事新编》等作品,您编辑的《萌芽》《前哨》等杂志,您翻译的《艺术论》《死魂灵》等译文,仿佛能看到您"吾将上下而求索"的身影。在这场跨越时空的对话中,我终于明白您坚守的"____",不是抽象的理念,而是我们安身立命的根本;您秉持的"____",不是无谓的锋芒,而是我们直面现实的勇气。
我是____专业的学生,今后,我会以您"____干夫指"的骨气,在是非面前坚守立场,不随波逐流;我会以您"俯首甘为____"的热忱,参与____活动,用微光温暖他人;我更会记住您"____,走的人多了,便成了路"的箴言,在专业领域勇于探索,努力用____为民族复兴、文化繁荣添砖加瓦。
我是____专业的学生,今后,我会以您"____千夫指"的骨气,在是非面前坚守立场,不随波逐流;我会以您"俯首甘为____"的热忱,参与____活动,用微光温暖他人;我更会记住您"____,走的人多了,便成了路"的箴言,在专业领域勇于探索,努力用____为民族复兴、文化繁荣添砖加瓦。
请您放心,您播下的"立人"火种,我们会让它在新时代燎原;您毕生追求的"人国"愿景,我们会用青春和奋斗去守护!`;
@@ -65,8 +138,26 @@ const LetterToLuxun = ({
<div
className="relative w-[1200px] h-full pointer-events-auto"
onClick={(e) => e.stopPropagation()}
style={{ perspective: "1200px" }}
>
{/* 信纸背景 */}
{/* 编辑态 / 只读态:Y 轴翻转切换 */}
<motion.div
className="relative w-full h-full"
initial={false}
animate={{ rotateY: submitted ? 180 : 0 }}
transition={{ duration: 0.7, ease: "easeInOut" }}
style={{ transformStyle: "preserve-3d" }}
>
{/* 前面:编辑态 */}
<div
className="absolute inset-0"
style={{
backfaceVisibility: "hidden",
// 编辑态展示时才响应事件,避免与背面点击冲突
pointerEvents: submitted ? "none" : "auto",
}}
>
{/* 编辑态:信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
alt="信纸"
@@ -86,13 +177,15 @@ const LetterToLuxun = ({
</div>
</div>
{/* 信件内容区域 */}
{/* 信件内容区域(可编辑) */}
<div className="absolute w-[700px] left-[320px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent.split("\n").map((line, lineIndex) => {
return letterContent
.split("\n")
.map((line, lineIndex) => {
// 检测是否已经找到"您好"行
if (line.includes("您好")) {
foundNiHao = true;
@@ -112,28 +205,85 @@ const LetterToLuxun = ({
style={needsIndent ? { textIndent: "2em" } : {}}
>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1;
const isLast =
partIndex === segments.length - 1;
if (isLast) {
return (
<span key={`part-${lineIndex}-${partIndex}`}>
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
</span>
);
}
const currentIndex = placeholderIndex++;
const isLimitedFill =
currentIndex === 0 || currentIndex === 1; // 填空1和填空2为限填
const options =
currentIndex === 0
? BLANK_1_OPTIONS
: BLANK_2_OPTIONS;
return (
<span key={`part-${lineIndex}-${partIndex}`}>
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{isLimitedFill ? (
// 限填:下拉选择
<Select
className="inline"
options={options.map((opt) => ({
value: opt,
label: opt,
}))}
value={
blankValues[currentIndex] ?? ""
}
onChange={(selectedValue) => {
const next = [...blankValues];
next[currentIndex] = selectedValue;
setBlankValues(next);
// 用户再次选择时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
placeholder="请选择"
error={blankErrors[currentIndex]}
zIndex={10000}
fitContent={true}
/>
) : (
// 任填:文本输入
<input
className="inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed border-[#4e402e] text-[24px] text-center outline-none"
value={blankValues[currentIndex] ?? ""}
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? "border-red-500"
: "border-[#4e402e]"
}`}
value={
blankValues[currentIndex] ?? ""
}
onChange={(e) => {
const next = [...blankValues];
next[currentIndex] = e.target.value;
setBlankValues(next);
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
/>
)}
</span>
);
})}
@@ -153,12 +303,15 @@ const LetterToLuxun = ({
/>
</div>
{/* 提交按钮 */}
{/* 提交按钮:切换到只读信件态 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
alt="可提交状态"
onClick={() => onSubmit?.(blankValues)}
onClick={() => {
if (!validateBlanks()) return;
setSubmitted(true);
}}
button
/>
</div>
@@ -172,6 +325,104 @@ const LetterToLuxun = ({
/>
</div>
</div>
{/* 背面:只读态 */}
<div
className="absolute inset-0"
style={{
transform: "rotateY(180deg)",
backfaceVisibility: "hidden",
// 只读态展示时才响应事件
pointerEvents: submitted ? "auto" : "none",
}}
>
{/* 只读态:新背景 */}
<img
src={getStaticImage("components/letter/信")}
alt="信件"
className="absolute inset-0 w-full h-full object-contain"
/>
<Image
src={getStaticImage("components/close")}
alt="信纸"
className="absolute right-38 top-0"
button
onClick={() => {
setSubmitted(false);
}}
/>
{/* 信件内容区域(只读) */}
<div className="absolute w-[700px] left-[260px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent
.split("\n")
.map((line, lineIndex) => {
if (line.includes("您好")) {
foundNiHao = true;
}
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
style={needsIndent ? { textIndent: "2em" } : {}}
>
{segments.map((part, partIndex) => {
const isLast =
partIndex === segments.length - 1;
if (isLast) {
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
</span>
);
}
const currentIndex = placeholderIndex++;
const value = blankValues[currentIndex] ?? "";
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{value || "  "}
</span>
);
})}
</div>
);
});
})()}
</div>
</div>
{/* 寄出按钮:关闭窗口 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/寄出")}
alt="寄出"
onClick={() => {
onSubmit?.(blankValues);
}}
button
/>
</div>
</div>
</motion.div>
</div>
</motion.div>
</>
)}
+171 -6
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image } from "@/components";
@@ -23,6 +23,47 @@ const LetterToSunzhongshan = ({
}: LetterToSunzhongshanProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([]);
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
// 校验输入:不能为空、每个不超过 10 个字符
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
const nextErrors: boolean[] = Array(placeholderCount).fill(false);
const nextValues: string[] = [...blankValues];
let hasError = false;
for (let i = 0; i < placeholderCount; i++) {
const raw = blankValues[i] || "";
const v = raw.trim();
// 为空 或 超过 10 个字,都视为不符合要求:清空并标红
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
}
setBlankValues(nextValues);
setBlankErrors(nextErrors);
return !hasError;
};
// 弹窗关闭后重置内容,方便下次重新填写
useEffect(() => {
if (!visible) {
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
}
}, [visible]);
// 信件内容模板(根据设计稿)
const letterContent = `敬爱的孙中山先生:
@@ -65,8 +106,26 @@ const LetterToSunzhongshan = ({
<div
className="relative w-[1200px] h-full pointer-events-auto"
onClick={(e) => e.stopPropagation()}
style={{ perspective: "1200px" }}
>
{/* 信纸背景 */}
{/* 编辑态 / 只读态:Y 轴翻转切换 */}
<motion.div
className="relative w-full h-full"
initial={false}
animate={{ rotateY: submitted ? 180 : 0 }}
transition={{ duration: 0.7, ease: "easeInOut" }}
style={{ transformStyle: "preserve-3d" }}
>
{/* 前面:编辑态 */}
<div
className="absolute inset-0"
style={{
backfaceVisibility: "hidden",
// 编辑态展示时才响应事件,避免与背面点击冲突
pointerEvents: submitted ? "none" : "auto",
}}
>
{/* 编辑态:信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
alt="信纸"
@@ -86,7 +145,7 @@ const LetterToSunzhongshan = ({
</div>
</div>
{/* 信件内容区域 */}
{/* 信件内容区域(可编辑) */}
<div className="absolute w-[700px] left-[320px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
@@ -126,12 +185,24 @@ const LetterToSunzhongshan = ({
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
<input
className="inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed border-[#4e402e] text-[24px] text-center outline-none"
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? "border-red-500"
: "border-[#4e402e]"
}`}
value={blankValues[currentIndex] ?? ""}
onChange={(e) => {
const next = [...blankValues];
next[currentIndex] = e.target.value;
setBlankValues(next);
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
/>
</span>
@@ -153,12 +224,15 @@ const LetterToSunzhongshan = ({
/>
</div>
{/* 提交按钮 */}
{/* 提交按钮:切换到只读信件态 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
alt="可提交状态"
onClick={() => onSubmit?.(blankValues)}
onClick={() => {
if (!validateBlanks()) return;
setSubmitted(true);
}}
button
/>
</div>
@@ -172,6 +246,97 @@ const LetterToSunzhongshan = ({
/>
</div>
</div>
{/* 背面:只读态 */}
<div
className="absolute inset-0"
style={{
transform: "rotateY(180deg)",
backfaceVisibility: "hidden",
// 只读态展示时才响应事件
pointerEvents: submitted ? "auto" : "none",
}}
>
{/* 只读态:新背景 */}
<img
src={getStaticImage("components/letter/信")}
alt="信件"
className="absolute inset-0 w-full h-full object-contain"
/>
<Image
src={getStaticImage("components/close")}
alt="信纸"
className="absolute right-38 top-0"
button
onClick={() => {
setSubmitted(false);
}}
/>
{/* 信件内容区域(只读) */}
<div className="absolute w-[700px] left-[260px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent.split("\n").map((line, lineIndex) => {
if (line.includes("您好")) {
foundNiHao = true;
}
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
style={needsIndent ? { textIndent: "2em" } : {}}
>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1;
if (isLast) {
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
</span>
);
}
const currentIndex = placeholderIndex++;
const value = blankValues[currentIndex] ?? "";
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
{value || "  "}
</span>
);
})}
</div>
);
});
})()}
</div>
</div>
{/* 寄出按钮:关闭窗口 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/寄出")}
alt="寄出"
onClick={() => {
onSubmit?.(blankValues);
}}
button
/>
</div>
</div>
</motion.div>
</div>
</motion.div>
</>
)}
+65 -25
View File
@@ -7,6 +7,7 @@ import {
type VideoRef,
LetterToLuxun,
StudyGuideModal,
Narration,
} from "@/components";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { SuccessAlert } from "@/components/layout";
@@ -40,8 +41,8 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
{
id: "quiz-1",
time: 155, // 2:35
title: "题目1",
prompt: "以下属于鲁迅先生作品的是( )。",
title: "",
prompt: "【多选】以下属于鲁迅先生作品的是( )。",
type: "multiple",
options: [
{ value: "A", label: "孔乙己", isCorrect: true },
@@ -53,43 +54,49 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
{
id: "quiz-2",
time: 355, // 5:55
title: "题目2",
prompt: "对于鼓励和培养青年一代,鲁迅先生的做法是( )。",
title: "",
prompt: "【多选】对于鼓励和培养青年一代,鲁迅先生的做法是( )。",
type: "multiple",
options: [
{ value: "A", label: "组织文学社团", isCorrect: true },
{ value: "B", label: "木刻运动", isCorrect: true },
{ value: "C", label: "写纪念烈士文章", isCorrect: true },
{
value: "D",
label: "让青年都去参军",
},
// {
// value: "D",
// label: "让青年都去参军",
// },
],
explanation:
"虽然鲁迅热爱革命,但他主要通过文学社团、木刻艺术和纪念文章来教育和启发青年,而不是通过参军来培养青年。",
// explanation:
// "虽然鲁迅热爱革命,但他主要通过文学社团、木刻艺术和纪念文章来教育和启发青年,而不是通过参军来培养青年。",
explanation: "请你再想想吧!",
},
{
id: "quiz-3",
time: 420, // 7:00
title: "题目3",
prompt: "以下那句诗出自鲁迅的作品( )。",
title: "",
prompt: "【单选】以下那句诗出自鲁迅的作品( )。",
type: "single",
options: [
{ value: "A", label: "寄意寒星荃不察,我以我血荐轩辕", isCorrect: true },
{
value: "A",
label: "寄意寒星荃不察,我以我血荐轩辕",
isCorrect: true,
explanation: "本句出自鲁迅的《自题小像》",
},
{
value: "B",
label: "我自横刀向天笑,去留肝胆两昆仑",
explanation: "出自谭嗣同《狱中题壁》",
explanation: "本句出自谭嗣同的《狱中题壁》,请再想想吧!",
},
{
value: "C",
label: "此去泉台招旧部,旌旗十万斩阎罗",
explanation: "出自陈毅《梅岭三章》",
explanation: "本句出自陈毅的《梅岭三章》,请再想想吧!",
},
{
value: "D",
label: "雄关漫道真如铁,而今迈步从头越",
explanation: "出自毛泽东《忆秦娥·娄山关》",
explanation: "本句出自毛泽东的《忆秦娥・娄山关》,请再想想吧!",
},
],
},
@@ -103,6 +110,7 @@ const LuxunInternalization = () => {
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(
null
);
const [showNarration, setShowNarration] = useState(true); // 是否显示旁白
const [visible, setVisible] = useState(false);
const [showVideo, setShowVideo] = useState(false);
const [postGuideVisible, setPostGuideVisible] = useState(false);
@@ -111,9 +119,12 @@ const LuxunInternalization = () => {
const videoRef = useRef<VideoRef>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
// 旁白关闭后显示对话引导
useEffect(() => {
if (!showNarration) {
setVisible(true);
}, []);
}
}, [showNarration]);
// 组件卸载时清理背景音乐
useEffect(() => {
@@ -152,17 +163,40 @@ const LuxunInternalization = () => {
<SingleChoiceQuizPanel
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}`,
}))}
correctAnswer={correctAnswers[0]}
errorMessage="再思考一下,选择正确答案。"
onWrongSelect={(value: string) => {
// 为题目3提供详细的选项解释
if (item.id === "quiz-3") {
const selectedOption = item.options.find(
(opt) => opt.value === value
);
return (
selectedOption?.explanation ||
"再思考一下,选择正确答案。"
);
}
return undefined;
}}
onSubmit={(value: string, isCorrect: boolean) => {
if (isCorrect) {
setActiveSuccess(item.id);
// 为题目3提供正确答案的详细解释
if (item.id === "quiz-3") {
const correctOption = item.options.find(
(opt) => opt.isCorrect
);
setSuccessContent(
correctOption?.explanation || "回答正确,请继续探索。"
);
} else {
setSuccessContent("回答正确,请继续探索。");
}
setResumeCallback(() => resume);
}
}}
@@ -196,7 +230,7 @@ const LuxunInternalization = () => {
<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}`,
@@ -279,9 +313,7 @@ const LuxunInternalization = () => {
setPostGuideVisible(true);
if (audioRef.current) {
audioRef.current.currentTime = 0;
audioRef.current
.play()
.catch((error) => {
audioRef.current.play().catch((error) => {
console.warn("背景音乐播放失败:", error);
});
}
@@ -289,7 +321,16 @@ const LuxunInternalization = () => {
/>
)}
{/* 引导 */}
{/* 旁白 */}
<Narration
visible={showNarration}
text={`走进上海鲁迅故居的红砖小楼,解锁了先生从绍兴到上海的文学足迹,我们读懂了他以笔为刃的战斗姿态,也见证了他与进步人士的革命情谊。那些针砭时弊的杂文、借古讽今的篇章,还有藏在6平方米客房里的红色坚守,都在诉说着先生的热忱。1907年,鲁迅先生在《文化偏至论》里写到:"是故将生存两间,角逐列国是务,其首在立人,人立而后凡事举;若其道术,乃必尊个性而张精神。"先生认为,中华民族要屹立于世界民族之林,"首在立人","立人"思想从此贯穿在鲁迅先生的人生和作品中。`}
onClose={() => {
setShowNarration(false);
}}
/>
{/* 对话引导 */}
<GuideSteps
visible={visible}
onVisibleChange={(visible) => {
@@ -359,8 +400,7 @@ const LuxunInternalization = () => {
navigate(`/${moduleName}/${starName}/home`);
}}
>
恭喜你已经顺利完成了鲁迅故居的学习,至此,你已经完成了本课程所有故居的学习,
希望你能有所收获,学习八位名人的优秀品质和精神,在新时代为中华民族伟大复兴发光发热!
恭喜你已经顺利完成了鲁迅故居的学习,请继续前往下一个故居吧!
</StudyGuideModal>
</div>
);
+27 -17
View File
@@ -33,6 +33,8 @@ const LuxunPlace1 = () => {
const [afterDialogGuideShown, setAfterDialogGuideShown] = useState(false);
// 引导对话显示状态
const [visible, setVisible] = useState(false);
const [visibleIndex, setVisibleIndex] = useState(0);
// 浏览完展品后的对话显示状态
const [showAfterExhibitDialog, setShowAfterExhibitDialog] = useState(false);
const [afterExhibitDialogShown, setAfterExhibitDialogShown] = useState(false);
@@ -44,6 +46,10 @@ const LuxunPlace1 = () => {
const [visited, setVisited] = useState<Set<number>>(new Set());
const handleOpenExhibit = (index: 1 | 2) => {
if (index === 1) {
setVisibleIndex(0);
setVisible(true);
}
setActiveExhibitId(index);
};
@@ -120,11 +126,11 @@ const LuxunPlace1 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
description={
activeExhibitId == 1
? `1892-1898年,少年时期在绍兴三味书屋求学积淀;\n1919-1923年,于北京八道湾参加新文化运动、酝酿《阿Q正传》;\n1926年9-12月,赴厦门大学任教期间聚焦底层创作杂文;\n1927年3-9月,广州白云楼整理旧作定稿《朝花夕拾》部分篇章。`
: undefined
}
// description={
// activeExhibitId == 1
// ? `1892-1898年,少年时期在绍兴三味书屋求学积淀;\n1919-1923年,于北京八道湾参加新文化运动、酝酿《阿Q正传》;\n1926年9-12月,赴厦门大学任教期间聚焦底层创作杂文;\n1927年3-9月,广州白云楼整理旧作定稿《朝花夕拾》部分篇章。`
// : undefined
// }
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
@@ -142,31 +148,35 @@ const LuxunPlace1 = () => {
visible={showGuideModal}
onContinue={() => {
setShowGuideModal(false);
setVisible(true);
if (!visible && !afterDialogGuideShown) {
setAfterDialogGuideShown(true);
setShowAfterDialogGuide(true);
}
}}
>
{`同学你好,欢迎来到走进故居-对话学习模块!
进入鲁迅故居后,我们一起来了解鲁迅先生经历的4个阶段:"风雨前夕的思想淬炼"->"杂文集里的匕首与投枪"->"历史新编中的现实呐喊&为亡友编纂的精神丰碑"->"6平方米的红色庇护所";`}
{`同学你好,欢迎来到“走进故居·对话”学习模块!
此刻你正立在鲁迅故居的一楼客厅里,接下来请随旧时光的纹路,一同翻开先生思想长卷里的5段深刻轨迹:风雨前夕的思想淬炼、杂文集里的匕首与投枪、历史新编中的现实呐喊、为亡友编纂的精神丰碑、6平方米的红色庇护所。准备好共赴这场笔尖与灵魂的滚烫相遇了吗?`}
</StudyGuideModal>
{/* 引导 */}
<GuideSteps
visible={visible}
currentStep={visibleIndex}
onStepChange={setVisibleIndex}
onVisibleChange={(nextVisible) => {
setVisible(nextVisible);
if (!nextVisible && !afterDialogGuideShown) {
setAfterDialogGuideShown(true);
setShowAfterDialogGuide(true);
}
}}
showMask={true}
highlightSelectors={[]}
showMask={false}
zIndex={10000}
>
<GuideStepsItem speaker="student">
小沪!我知道鲁迅是1933年搬到大陆新村9号的,那在这之前他为什么会选择来到上海,又有哪些思想转变呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
你问得很关键!而1927年是鲁迅思想的重要转折点,这一年他在广州亲历了"四一二"反革命政变,亲眼看见进步青年和革命志士倒在血泊中,曾经对"温和改革"的幻想彻底破灭。
你问得很关键!从1892-1898年少年时期在绍兴三味书屋求学积淀,到1919-1923年于北京八道湾参加新文化运动、酝酿《阿Q正传》,再到1926年9-12月赴厦门大学任教期间聚焦底层创作杂文。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1927年3-9月广州白云楼整理旧作定稿《朝花夕拾》部分篇章,鲁迅的创作足迹跨越南北,而1927年是鲁迅思想的重要转折点,这一年他在广州亲历了"四一二"反革命政变,亲眼看见进步青年和革命志士倒在血泊中,曾经对"温和改革"的幻想彻底破灭。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
此前他写下《狂人日记》《阿Q正传》批判封建礼教,参加语丝社针砭时弊,但始终在"彷徨"中摸索救国之路。广州的血雨腥风,让他认清了反动派的真面目。
@@ -199,10 +209,10 @@ const LuxunPlace1 = () => {
highlightSelectors={[]}
>
<GuideStepsItem speaker="student">
刚到上海的鲁迅住在哪里,又在做什么?
这张照片也是鲁迅在上海期间的住所吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
1927年10月,鲁迅带着许广平来到上海,先住进了虹口区横浜路景云里。在景云里的两年半,他结识了冯雪峰等共产党员,参与发起中国左翼作家联盟,从"独战的英雄"转变为革命文艺战线的旗手。
不错!1927年10月,鲁迅带着许广平来到上海,先住进了虹口区横浜路景云里。在景云里的两年半,他结识了冯雪峰等共产党员,参与发起中国左翼作家联盟,从"独战的英雄"转变为革命文艺战线的旗手。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
这段时间他写下《三闲集》《二心集》,表达他在当时历史条件下针对社会现象、文化问题和论战的看法与社会批判,此间还翻译了大量马列文艺理论著作,为此后的战斗打下了坚实思想基础。
+37 -12
View File
@@ -39,6 +39,10 @@ const LuxunPlace2 = () => {
const [guide2Visible, setGuide2Visible] = useState(false);
const [guide2Step, setGuide2Step] = useState(0);
// 光标3的引导对话状态
const [guide3Visible, setGuide3Visible] = useState(false);
const [guide3Step, setGuide3Step] = useState(0);
const handleOpenExhibit = (index: 1 | 2 | 3) => {
if (index === 1) {
// 光标1:点击时同时打开展品和引导对话
@@ -50,6 +54,10 @@ const LuxunPlace2 = () => {
setActiveExhibitId(2);
setGuide2Visible(true);
setGuide2Step(0);
} else if (index === 3) {
setActiveExhibitId(3);
setGuide3Visible(true);
setGuide3Step(0);
} else {
// 光标3:直接打开
setActiveExhibitId(index);
@@ -99,7 +107,6 @@ const LuxunPlace2 = () => {
subtitleClassName={subtitleClassName}
className="absolute top-10 left-1/2 -translate-x-1/2"
/>
<Image
className="absolute right-5 top-5"
src={getStaticImage("components/离开")}
@@ -110,7 +117,6 @@ const LuxunPlace2 = () => {
}
}}
/>
{/* 光标1 */}
<Cursor
className="absolute right-218 bottom-104"
@@ -132,7 +138,6 @@ const LuxunPlace2 = () => {
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
/>
{completed && (
<Image
className="absolute bottom-10 left-42"
@@ -143,15 +148,14 @@ const LuxunPlace2 = () => {
}}
/>
)}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
description={
activeExhibitId === 3
? '这篇《中国人失掉自信力了吗》收录于《且介亭杂文》中,它将"匕首与投枪"的特质发挥到极致。当时国民党当局接连丧权辱国,一些文人却散布"中国人失掉自信力"的悲观论调,迎合反动派的卖国行径。鲁迅直接驳斥这种谬论,一针见血指出:"我们从古以来,就有埋头苦干的人,有拼命硬干的人,有为民请命的人,有舍身求法的人……这就是中国的脊梁。"文字如利刃般戳破虚假论调,揭露了反动派掩盖自身无能、麻痹民众的阴谋,唤醒国人的民族自信,尽显战斗文字的锋芒与力量。'
: undefined
}
// description={
// activeExhibitId === 3
// ? '这篇《中国人失掉自信力了吗》收录于《且介亭杂文》中,它将"匕首与投枪"的特质发挥到极致。当时国民党当局接连丧权辱国,一些文人却散布"中国人失掉自信力"的悲观论调,迎合反动派的卖国行径。鲁迅直接驳斥这种谬论,一针见血指出:"我们从古以来,就有埋头苦干的人,有拼命硬干的人,有为民请命的人,有舍身求法的人……这就是中国的脊梁。"文字如利刃般戳破虚假论调,揭露了反动派掩盖自身无能、麻痹民众的阴谋,唤醒国人的民族自信,尽显战斗文字的锋芒与力量。'
// : undefined
// }
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
@@ -163,7 +167,6 @@ const LuxunPlace2 = () => {
setActiveExhibitId(null);
}}
/>
{/* 光标1的引导对话 */}
<GuideSteps
visible={guide1Visible}
@@ -178,13 +181,12 @@ const LuxunPlace2 = () => {
小沪,这里的《且介亭杂文》就是鲁迅先生在大陆新村9号创作的吧,有人将先生的杂文评价为"匕首与投枪",《且介亭杂文》中有具体体现吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
当然有!20世纪30年代,国民党"文化围剿"愈烈,进步思想遭严控,鲁迅在大陆新村9号创作出《且介亭杂文》等集子。这些杂文聚焦文化乱象与社会沉疴,篇幅精炼却直击要害,像匕首般刺穿虚伪表象,似投枪般直戳问题核心...
当然有!20世纪30年代,国民党"文化围剿"愈烈,进步思想遭严控,鲁迅在大陆新村9号创作出《且介亭杂文》等集子。这些杂文聚焦文化乱象与社会沉疴,篇幅精炼却直击要害,像匕首般刺穿虚伪表象,似投枪般直戳问题核心。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
用锋利文字与黑暗势力对峙,成为那个时代最具战斗力的思想武器,"匕首与投枪"的赞誉恰是其战斗性的生动写照。
</GuideStepsItem>
</GuideSteps>
{/* 光标2的引导对话 */}
<GuideSteps
visible={guide2Visible}
@@ -202,6 +204,29 @@ const LuxunPlace2 = () => {
这是鲁迅的自嘲,他曾说"我不会说绵软的苏白,不会打响亮的京腔,不入调,不入流",所以戏称自己的文章是"南腔北调"。这本书里的《〈守常全集》题记》,赞美了李大钊同志,说"他的遗文却将永驻,因为这是先驱者的遗产,革命史上的丰碑。"
</GuideStepsItem>
</GuideSteps>
\{/* 光标3的引导对话 */}
<GuideSteps
visible={guide3Visible}
onVisibleChange={setGuide3Visible}
currentStep={guide3Step}
onStepChange={setGuide3Step}
showMask={false}
highlightSelectors={[]}
zIndex={10000}
>
<GuideStepsItem speaker="student">
我们初中学的《中国人失掉自信力了吗》就是收录于《且介亭杂文》中的!
</GuideStepsItem>
<GuideStepsItem speaker="guide">
那你应该更有体会!这篇《中国人失掉自信力了吗》是将“匕首与投枪”的特质发挥到极致。当时国民党当局接连丧权辱国,一些文人却散布“中国人失掉自信力”的悲观论调,迎合反动派的卖国行径。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
鲁迅直接驳斥这种谬论,一针见血指出:“我们从古以来,就有埋头苦干的人,有拼命硬干的人,有为民请命的人,有舍身求法的人……这就是中国的脊梁。”
</GuideStepsItem>
<GuideStepsItem speaker="guide">
文字如利刃般戳破虚假论调,揭露了反动派掩盖自身无能、麻痹民众的阴谋,唤醒国人的民族自信,尽显战斗文字的锋芒与力量。
</GuideStepsItem>
</GuideSteps>
</div>
);
};
+45 -18
View File
@@ -56,6 +56,10 @@ const LuxunPlace3 = () => {
const [guide1Visible, setGuide1Visible] = useState(false);
const [guide1Step, setGuide1Step] = useState(0);
// 光标2的引导对话状态
const [guide2Visible, setGuide2Visible] = useState(false);
const [guide2Step, setGuide2Step] = useState(0);
// 光标2的题目状态
const [quiz2Visible, setQuiz2Visible] = useState(false);
const [successAlert2Visible, setSuccessAlert2Visible] = useState(false);
@@ -85,6 +89,8 @@ const LuxunPlace3 = () => {
} else if (index === 2) {
// 光标2:只打开展品(带描述)
setActiveExhibitId(2);
setGuide2Visible(true);
setGuide2Step(0);
} else if (index === 3) {
// 光标3:点击时同时打开展品和引导对话
setActiveExhibitId(3);
@@ -119,7 +125,7 @@ const LuxunPlace3 = () => {
}}
>
<Title
title="历史新编中的现实呐喊&为亡友编纂的精神丰碑"
title={`历史新编中的现实呐喊\n为亡友编纂的精神丰碑`}
subtitle={subtitle}
subtitleClassName={subtitleClassName}
className="absolute top-10 left-1/2 -translate-x-1/2"
@@ -186,13 +192,13 @@ const LuxunPlace3 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
description={
activeExhibitId === 2
? '本书里面还收录了《理水》,文章写的是大禹治水"三过家门而不入"的上古传说,实则藏着对现实的尖锐针砭。当时反动派统治腐朽,面对内忧外患只会粉饰太平,还有一群空谈理论、脱离实际的"学者"混淆视听。大禹带领民众跋山涉水、实地勘察,摒弃"堵"的旧法改用"疏"的良方,历经千辛万苦终平水患。这哪里是写古代治水?分明是在赞颂脚踏实地、敢破敢立的实干精神,讽刺那些庸碌无为的当权者与空谈家,用远古的正义之光映照现实的黑暗。'
: activeExhibitId === 5
? "为了妥善出版亡友遗著,鲁迅亲自选稿、校对、设计封面。为了躲避反动派的审查,他还特意用“诸夏怀霜社”的名义出版,“怀霜”就是为了纪念瞿秋白。1936年《海上述林》上卷出版时,鲁迅的身体已经极度虚弱,但他仍一遍遍翻看样书,说“这是我对秋白最好的纪念”。"
: undefined
}
// description={
// activeExhibitId === 2
// ? '本书里面还收录了《理水》,文章写的是大禹治水"三过家门而不入"的上古传说,实则藏着对现实的尖锐针砭。当时反动派统治腐朽,面对内忧外患只会粉饰太平,还有一群空谈理论、脱离实际的"学者"混淆视听。大禹带领民众跋山涉水、实地勘察,摒弃"堵"的旧法改用"疏"的良方,历经千辛万苦终平水患。这哪里是写古代治水?分明是在赞颂脚踏实地、敢破敢立的实干精神,讽刺那些庸碌无为的当权者与空谈家,用远古的正义之光映照现实的黑暗。'
// : activeExhibitId === 5
// ? "为了妥善出版亡友遗著,鲁迅亲自选稿、校对、设计封面。为了躲避反动派的审查,他还特意用“诸夏怀霜社”的名义出版,“怀霜”就是为了纪念瞿秋白。1936年《海上述林》上卷出版时,鲁迅的身体已经极度虚弱,但他仍一遍遍翻看样书,说“这是我对秋白最好的纪念”。"
// : undefined
// }
onClose={() => {
if (activeExhibitId === 2) {
// 光标2:关闭展品框后显示题目
@@ -250,26 +256,47 @@ const LuxunPlace3 = () => {
</GuideStepsItem>
</GuideSteps>
{/* 光标2的引导对话 */}
<GuideSteps
visible={guide2Visible}
onVisibleChange={setGuide2Visible}
currentStep={guide2Step}
onStepChange={setGuide2Step}
showMask={false}
highlightSelectors={[]}
zIndex={10000}
>
<GuideStepsItem speaker="student">
这本书里面还收录了《理水》,是大禹治水的故事吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
不错,文章写的是大禹治水“三过家门而不入”的上古传说,实则藏着对现实的尖锐针砭。当时反动派统治腐朽,面对内忧外患只会粉饰太平,还有一群空谈理论、脱离实际的“学者”混淆视听。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
大禹带领民众跋山涉水、实地勘察,摒弃“堵”的旧法改用“疏”的良方,历经千辛万苦终平水患。这哪里是写古代治水?分明是在赞颂脚踏实地、敢破敢立的实干精神,讽刺那些庸碌无为的当权者与空谈家,用远古的正义之光映照现实的黑暗。
</GuideStepsItem>
</GuideSteps>
{/* 光标2的题目 */}
<SingleChoiceQuizPanel
visible={quiz2Visible}
title="鲁迅在上海大陆新村9号居住期间,翻译外国作品的核心目的不包括()"
title="【单选】鲁迅在上海大陆新村9号居住期间,翻译外国作品的核心目的不包括:"
options={[
{
value: "A",
label: "为中国文艺引进新思想、新血液",
label: "A.为中国文艺引进新思想、新血液",
},
{
value: "B",
label: "让中国青年接触更先进的文化理念",
label: "B.让中国青年接触更先进的文化理念",
},
{
value: "C",
label: "直接参与武装革命的战略部署",
label: "C.直接参与武装革命的战略部署",
},
{
value: "D",
label: "以翻译为武器助力思想启蒙",
label: "D.以翻译为武器助力思想启蒙",
},
]}
correctAnswer="C"
@@ -369,12 +396,12 @@ const LuxunPlace3 = () => {
{/* 光标5的题目 */}
<SingleChoiceQuizPanel
visible={quiz5Visible}
title="鲁迅在上海大陆新村9号居住期间,为纪念瞿秋白编校的译文集是()"
title="【单选】鲁迅在上海大陆新村9号居住期间,为纪念瞿秋白编校的译文集是:"
options={[
{ value: "A", label: "《仿自由书》" },
{ value: "B", label: "《南腔北调集》" },
{ value: "C", label: "《海上述林》" },
{ value: "D", label: "《故事新编》" },
{ value: "A", label: "A.《仿自由书》" },
{ value: "B", label: "B.《南腔北调集》" },
{ value: "C", label: "C.《海上述林》" },
{ value: "D", label: "D.《故事新编》" },
]}
correctAnswer="C"
errorMessage={quiz5Error}
+3 -3
View File
@@ -137,9 +137,9 @@ const LuxunPlace5 = () => {
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
description={
activeExhibitId === 1
? '"鲁迅是莱谟斯,是野兽的奶汁所喂养大的,是封建宗法社会的逆子,是绅士阶级的贰臣,而同时也是一些浪漫谛克的革命家的诤友!"'
? '瞿秋白同志曾说,"鲁迅是莱谟斯,是野兽的奶汁所喂养大的,是封建宗法社会的逆子,是绅士阶级的贰臣,而同时也是一些浪漫谛克的革命家的诤友!"'
: activeExhibitId === 2
? '"人生得一知己足矣,斯世当以同怀视之"'
? '鲁迅先生曾经抄录清朝诗人何瓦琴的对联赠给瞿秋白,"人生得一知己足矣,斯世当以同怀视之"'
: undefined
}
onClose={() => {
@@ -188,7 +188,7 @@ const LuxunPlace5 = () => {
{/* 光标2的题目 */}
<SingleChoiceQuizPanel
visible={quizVisible}
title="上海鲁迅故居三楼客房被称为“红色庇护所”,以下哪位共产党人曾在此避难并与鲁迅探讨革命议题"
title="【单选】上海鲁迅故居三楼客房被称为“红色庇护所”,以下哪位共产党人曾在此避难并与鲁迅探讨革命议题?"
options={[
{ value: "A", label: "陈独秀" },
{ value: "B", label: "瞿秋白" },
+57 -15
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { GuideSteps, GuideStepsItem, Image } from "@/components";
import { Image, StudyGuideModal } from "@/components";
import SuccessAlert from "@/components/layout/alert/success-alert";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
@@ -73,12 +73,16 @@ const LuxunReflectionScene1 = () => {
// 已点击的城市ID集合
const [clickedCities, setClickedCities] = useState<Set<number>>(new Set());
// 是否显示成功提示
// 是否显示成功提示(全部完成时)
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
// 当前显示的城市提示ID(单个地点点击后)
const [currentCityAlertId, setCurrentCityAlertId] = useState<number | null>(
null
);
// 是否全部完成
const [allCompleted, setAllCompleted] = useState(false);
// 引导对话是否显示
const [guideVisible, setGuideVisible] = useState(true);
// 学习引导对话是否显示
const [showGuideModal, setShowGuideModal] = useState(true);
const progress = clickedCities.size;
// 当前应当点亮的城市索引(按照 cityConfig 顺序)
@@ -90,7 +94,6 @@ const LuxunReflectionScene1 = () => {
useEffect(() => {
if (progress === totalTasks && !allCompleted) {
setAllCompleted(true);
setShowSuccessAlert(true);
}
}, [progress, totalTasks, allCompleted]);
@@ -104,12 +107,31 @@ const LuxunReflectionScene1 = () => {
next.add(cityId);
return next;
});
// 显示当前城市的成功提示
setCurrentCityAlertId(cityId);
};
const handleSuccessAlertClose = () => {
setShowSuccessAlert(false);
};
const handleCityAlertClose = () => {
const cityId = currentCityAlertId;
setCurrentCityAlertId(null);
// 如果关闭的是最后一个城市(上海鲁迅故居,id=5),则显示全部完成的提示
const lastCity = cityConfig[cityConfig.length - 1];
if (lastCity && cityId === lastCity.id) {
setShowSuccessAlert(true);
}
};
// 获取当前显示的城市信息
const currentCity = currentCityAlertId
? cityConfig.find((city) => city.id === currentCityAlertId)
: null;
return (
<div
className="flex justify-between w-full h-full bg-cover bg-center bg-no-repeat gap-8 relative"
@@ -245,14 +267,14 @@ const LuxunReflectionScene1 = () => {
// 全部完成后显示详细信息
<div className="space-y-10 h-full overflow-y-auto custom-scrollbar">
{cityConfig.map((city) => (
<div key={city.id} className="space-y-4">
<div className="text-red-600 text-5xl flex items-end gap-6">
<div key={city.id} className="space-y-2">
<div className="text-red-600 text-3xl flex items-end gap-6">
<div>{city.location}</div>
<div className="text-gray-600 text-4xl font-normal">
<div className="text-gray-600 text-2xl font-normal">
{city.time}
</div>
</div>
<div className="text-black text-4xl font-normal leading-relaxed">
<div className="text-black text-2xl font-normal leading-relaxed">
{city.description}
</div>
</div>
@@ -267,9 +289,7 @@ const LuxunReflectionScene1 = () => {
return (
<div
key={city.id}
className={
isCompleted ? "text-green-600 line-through" : ""
}
className={isCompleted ? "text-green-600" : ""}
>
{index + 1}.点击图标{city.location}
</div>
@@ -281,7 +301,19 @@ const LuxunReflectionScene1 = () => {
</div>
</div>
{/* 成功提示弹窗 */}
{/* 单个地点点击成功提示弹窗 */}
{currentCity && (
<SuccessAlert
visible={true}
title=""
// title={`你找到了${currentCity.location}!`}
content={`${currentCity.time}\n${currentCity.description}`}
onClose={handleCityAlertClose}
onContinue={handleCityAlertClose}
/>
)}
{/* 全部完成成功提示弹窗 */}
<SuccessAlert
visible={showSuccessAlert}
title="你找到了鲁迅创作的足迹,接下来,填充文字里的故事吧!"
@@ -289,7 +321,7 @@ const LuxunReflectionScene1 = () => {
onContinue={handleSuccessAlertClose}
/>
{/* 学习引导对话 */}
<GuideSteps
{/* <GuideSteps
visible={guideVisible}
onVisibleChange={setGuideVisible}
showMask={false}
@@ -302,7 +334,17 @@ const LuxunReflectionScene1 = () => {
<GuideStepsItem speaker="guide">
请根据年代由远及近的顺序,依“手指提示”点亮先生的创作足迹,解锁更多的故事背景!
</GuideStepsItem>
</GuideSteps>
</GuideSteps> */}
<StudyGuideModal
visible={showGuideModal}
onContinue={() => {
setShowGuideModal(false);
}}
>
{`同学你好,欢迎来到“跨越时空·体悟”学习模块!
请登陆“时空回溯・亲历时代”体验仓,下一站:1892年,绍兴三味书屋。
请按照年代由远及近的顺序,依“手指提示”,点亮先生的创作足迹,解锁更多故事背景!时空门已开,请做好准备吧!`}
</StudyGuideModal>
</div>
);
};
+68 -15
View File
@@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { Image, DragFill, GuideSteps, GuideStepsItem } from "@/components";
import type { DragFillResult } from "@/components";
import ErrorAlert from "@/components/layout/alert/error-alert";
import SuccessAlert from "@/components/layout/alert/success-alert";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
const starName = "luxun";
@@ -14,56 +15,64 @@ const dragItems = [
name: "闰土",
imagePath: "luxun/reflection/scene2/拖拽-润土",
targetBlankId: "city-1-label-0", // 绍兴-人物
errorHint: "闰土的故事和鲁迅的少年绍兴时光有关哦",
errorHint: "❌闰土的故事和鲁迅的少年绍兴时光有关哦",
successMessage: "✅答对啦! 闰土是鲁迅绍兴时期笔下的经典人物",
},
{
id: "baicaoyuan",
name: "《从百草园到三味书屋》",
imagePath: "luxun/reflection/scene2/拖拽-百草园到三味书屋",
targetBlankId: "city-1-label-1", // 绍兴-作品
errorHint: "这个作品写的是鲁迅在绍兴的故事哦",
errorHint: "❌这个作品写的是鲁迅在绍兴的故事哦",
successMessage: "✅真棒! 这篇散文记录了鲁迅在绍兴的求学时光",
},
{
id: "aqzhengzhuan",
name: "《阿Q正传》",
imagePath: "luxun/reflection/scene2/拖拽-阿Q正传",
targetBlankId: "city-2-label-0", // 北京-作品
errorHint: "这部经典小说和鲁迅在北京的创作有关哦",
errorHint: "❌这部经典小说和鲁迅在北京的创作有关哦",
successMessage: "✅答对啦! 《阿Q正传》是鲁迅小说的代表作, 诞生于北京",
},
{
id: "jingshenshengli",
name: '"精神胜利法"',
imagePath: "luxun/reflection/scene2/拖拽-精神胜利法",
targetBlankId: "city-2-label-1", // 北京-核心主题
errorHint: "这个主题是《阿Q正传》的核心,和北京创作",
errorHint: "❌这个主题是《阿Q正传》的核心, 和北京创作背景相关哦",
successMessage: "✅没错! 《阿Q正传》画出了国人的灵魂, 以便疗救。",
},
{
id: "guanzhudiceng",
name: '"关注底层"',
imagePath: "luxun/reflection/scene2/关注底层",
targetBlankId: "city-3-label-0", // 厦门-创作方向
errorHint: "这个创作方向是鲁迅在厦门的核心追求哦",
errorHint: "❌这个创作方向是鲁迅在厦门的核心追求哦",
successMessage: "✅答对啦!鲁迅在厦门任教时,依旧关注底层民众的命运",
},
{
id: "zhaohuaxishi",
name: "《朝花夕拾》",
imagePath: "luxun/reflection/scene2/拖拽-朝花夕拾",
targetBlankId: "city-4-label-0", // 广州-作品集
errorHint: "这个作品集和鲁迅在广州的旅居时光有关哦",
errorHint: "❌这个作品集和鲁迅在广州的旅居时光有关哦",
successMessage: "✅真棒!《朝花夕拾》部分篇章在广州白云楼定稿",
},
{
id: "gushixinbian",
name: "《故事新编》",
imagePath: "luxun/reflection/scene2/拖拽-故事新编",
targetBlankId: "city-5-label-0", // 上海-作品
errorHint: "再试试~这个作品和上海的创作故事更相关哦",
errorHint: "❌再试试~这个作品和上海的创作故事更相关哦",
successMessage: "✅答对啦!《故事新编》是鲁迅在上海故居完成的 经典作品集",
},
{
id: "biweidaoqiang",
name: '"笔为刀枪"',
imagePath: "luxun/reflection/scene2/笔为刀枪",
targetBlankId: "city-5-label-1", // 上海-创作中心
errorHint: "这个初心是鲁迅在上海创作的核心哦",
errorHint: "❌这个初心是鲁迅在上海创作的核心哦",
successMessage: "✅没错!鲁迅在上海以笔为刃,批判黑暗的社会现实",
},
];
@@ -153,10 +162,15 @@ const LuxunReflectionScene2 = () => {
// 错误提示相关状态
const [showErrorAlert, setShowErrorAlert] = useState(false);
const [errorHint, setErrorHint] = useState("");
// 成功提示相关状态
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [successMessage, setSuccessMessage] = useState("");
// 引导对话是否显示
const [guideVisible, setGuideVisible] = useState(true);
// 通关提示对话是否显示
const [completionGuideVisible, setCompletionGuideVisible] = useState(false);
// 通关提示是否已经显示过(防止重复显示)
const [completionGuideShown, setCompletionGuideShown] = useState(false);
// 转换为 DragFill 的 options
const options = useMemo(
@@ -200,6 +214,7 @@ const LuxunReflectionScene2 = () => {
// 检查改变的结果是否有错误
let hasError = false;
let errorMessage = "";
let successItem = null;
for (const result of changedResults) {
const itemId = result.optionValue;
@@ -211,6 +226,11 @@ const LuxunReflectionScene2 = () => {
hasError = true;
errorMessage = item.errorHint || "放置位置不正确,请再试试~";
break; // 只显示第一个错误的提示
} else if (item && item.targetBlankId === blankId) {
// 如果放置正确,记录成功项(只记录第一个正确的)
if (!successItem) {
successItem = item;
}
}
}
@@ -232,6 +252,12 @@ const LuxunReflectionScene2 = () => {
return item && item.targetBlankId === blankId;
});
// 如果拖拽正确,显示成功提示
if (successItem) {
setSuccessMessage(successItem.successMessage);
setShowSuccessAlert(true);
}
setDragFillValue(filteredValue);
},
[dragFillValue]
@@ -244,8 +270,7 @@ const LuxunReflectionScene2 = () => {
useEffect(() => {
if (progress === totalTasks && !allCompleted) {
setAllCompleted(true);
// 任务完成后自动显示通关提示对话
setCompletionGuideVisible(true);
// 不在这里显示通关提示,等待所有成功提示关闭后再显示
}
}, [progress, totalTasks, allCompleted]);
@@ -254,6 +279,19 @@ const LuxunReflectionScene2 = () => {
setErrorHint("");
};
const handleSuccessAlertClose = () => {
setShowSuccessAlert(false);
setSuccessMessage("");
};
// 当所有任务完成且成功提示关闭后,显示通关提示(只显示一次)
useEffect(() => {
if (allCompleted && !showSuccessAlert && !completionGuideShown) {
setCompletionGuideVisible(true);
setCompletionGuideShown(true);
}
}, [allCompleted, showSuccessAlert, completionGuideShown]);
// 获取每个标签区域已放置的元素
const getLabelPlacedItem = (cityId: number, labelIndex: number) => {
const blankId = `city-${cityId}-label-${labelIndex}`;
@@ -394,7 +432,8 @@ const LuxunReflectionScene2 = () => {
{/* 鲁迅画廊 */}
<div className="absolute top-[calc(2.4rem+204px)] left-0 w-[995px] h-[467px] pt-16 pl-70 pr-10">
{/* 提示图标 */}
<div className="absolute left-14 top-35 flex items-center z-10 pointer-events-none">
<div className="absolute left-14 top-35 space-y-2 z-10 pointer-events-none">
<div className="flex items-center">
<img
src={getStaticImage(
`${starName}/reflection/scene2/鲁迅体悟拖一拖`
@@ -408,6 +447,10 @@ const LuxunReflectionScene2 = () => {
alt="拖一拖图标"
/>
</div>
<div className="w-45 leading-relaxed tracking-wider text-black text-3xl font-bold">
请将右侧图标拖到左侧对应位置。
</div>
</div>
{/* 使用 DragFill 组件 */}
<div className="mt-10">
@@ -490,10 +533,12 @@ const LuxunReflectionScene2 = () => {
) : (
// 未完成时显示提示内容
<div className="space-y-8 text-center">
<div className="text-black text-5xl font-bold">任务展示窗口</div>
<div className="text-red-600 text-4xl font-bold">
请将以下内容拖拽至正确位置
<div className="text-black text-5xl font-bold mb-10">
任务展示窗口
</div>
{/* <div className="text-red-600 text-4xl font-bold">
请将以下内容拖拽至正确位置
</div> */}
<div className="grid grid-cols-4 gap-4 text-3xl font-bold">
{dragItems.map((item) => {
const isPlaced = dragFillValue.some(
@@ -503,7 +548,7 @@ const LuxunReflectionScene2 = () => {
<div
key={item.id}
className={`${
isPlaced ? "text-green-600 line-through" : "text-black"
isPlaced ? "text-green-600" : "text-black"
}`}
>
{item.name}
@@ -524,6 +569,14 @@ const LuxunReflectionScene2 = () => {
onMaskClick={handleErrorAlertClose}
/>
{/* 成功提示弹窗 */}
<SuccessAlert
visible={showSuccessAlert}
title={successMessage}
onClose={handleSuccessAlertClose}
onContinue={handleSuccessAlertClose}
/>
{/* 学习引导对话 */}
<GuideSteps
visible={guideVisible}
+61 -18
View File
@@ -1,8 +1,9 @@
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Image, GuideSteps, GuideStepsItem } from "@/components";
import { Image, GuideSteps, GuideStepsItem, EndingOverlay } from "@/components";
import { SingleChoiceQuizPanel } from "@/components/layout/quiz";
import SuccessAlert from "@/components/layout/alert/success-alert";
const starName = "luxun";
@@ -22,7 +23,15 @@ const LuxunReflectionScene3 = () => {
const [completedQuiz2, setCompletedQuiz2] = useState(false); // 题目2是否完成
const [showSummary, setShowSummary] = useState(false); // 是否显示总结
const [completionGuideVisible, setCompletionGuideVisible] = useState(false); // 通关提示对话是否显示
const [completionGuideFinished, setCompletionGuideFinished] = useState(false); // 通关提示对话是否已完成
const [_completionGuideFinished, setCompletionGuideFinished] =
useState(false); // 通关提示对话是否已完成
// 成功提示相关状态
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [successMessage, setSuccessMessage] = useState("");
// 结束语覆盖层是否显示
const [showEndingOverlay, setShowEndingOverlay] = useState(false);
// 结束语覆盖层是否已关闭
const [endingOverlayFinished, setEndingOverlayFinished] = useState(false);
const allCompleted = completedQuiz1 && completedQuiz2;
const progress = (completedQuiz1 ? 1 : 0) + (completedQuiz2 ? 1 : 0);
@@ -62,6 +71,9 @@ const LuxunReflectionScene3 = () => {
setCompletedQuiz1(true);
setShowQuiz1(false);
setHintPosition("piping"); // 提示图标移动到"批判现实"旁边
// 显示成功提示
setSuccessMessage("鲁迅一生辗转,只为唤醒国人,守护家国。");
setShowSuccessAlert(true);
}
};
@@ -71,6 +83,9 @@ const LuxunReflectionScene3 = () => {
setCompletedQuiz2(true);
setShowQuiz2(false);
setHintPosition("luxun2"); // 提示图标移动到"鲁迅2"旁边
// 显示成功提示
setSuccessMessage("鲁迅以文字批判旧时代的黑暗,唤醒国人的觉醒。");
setShowSuccessAlert(true);
}
};
@@ -245,30 +260,30 @@ const LuxunReflectionScene3 = () => {
<div className="h-full px-8 overflow-y-auto custom-scrollbar">
<div className="space-y-0">
<div className="text-red-600 text-4xl font-bold leading-relaxed">
1、批判现实 (他以犀利的文笔批判封建传统);
1.关注底层:他笔下的主角多是社会底层
</div>
<div className="text-red-600 text-4xl font-bold leading-relaxed">
2、关注底层 (他笔下的主角多是社会底层);
2.批判现实:他以犀利的文笔批判封建传统
</div>
<div className="text-red-600 text-4xl font-bold leading-relaxed">
3、家国情怀 (他投身新文化与左翼运动);
3.家国情怀:他致力于唤醒国人、守护家国
</div>
</div>
</div>
) : (
// 未完成时显示提示内容
<div className="text-center text-black text-4xl font-bold">
请跟随小手指点一点,完成操作和题目以查看总结
请跟随小手指点一点,拼接鲁迅的精神脉络!
</div>
)}
</div>
</div>
{/* 继续前进按钮 - 只在通关提示对话结束后显示 */}
{completionGuideFinished && (
{/* 继续前进按钮 - 只在结束语覆盖层关闭后显示 */}
{endingOverlayFinished && (
<Image
className="absolute bottom-45 right-0"
src={getStaticImage("components/place/继续前进")}
src={getStaticImage("components/place/离开场景")}
button
onClick={() => {
navigate(`/${moduleName}/${starName}/home`);
@@ -279,29 +294,44 @@ const LuxunReflectionScene3 = () => {
{/* 题目1:家国情怀 */}
<SingleChoiceQuizPanel
visible={showQuiz1}
title="【单选题】先生的作品《狂人日记》、《阿Q正传》都在揭露的是()"
title={"【单选】先生辗转多地创作,核心追求是(\xa0\xa0\xa0\xa0)"}
options={[
{ value: "A", label: "A. 旧时代的黑暗与愚昧" },
{ value: "B", label: "B. 生活中的小美好" },
{ value: "A", label: "A. 唤醒国人,改变社会" },
{ value: "B", label: "B. 只为个人成名" },
]}
correctAnswer="A"
errorMessage='不是哦,先生的小说核心是批判而非歌颂哦!"请结合阿Q的碎片回忆一下吧!"'
errorMessage='不是哦,先生的创作从不是为了自己哦!请回忆先生"弃医从文"的初心。'
onSubmit={handleQuiz1Submit}
/>
{/* 题目2:批判现实 */}
<SingleChoiceQuizPanel
visible={showQuiz2}
title="【单选题】先生辗转多地创作,核心追求是()"
title="【单选】先生的作品《狂人日记》《阿Q正传》都在揭露的是:"
options={[
{ value: "A", label: "A. 唤醒国人,改变社会" },
{ value: "B", label: "B. 只为个人成名" },
{ value: "A", label: "A. 旧时代的黑暗与愚昧" },
{ value: "B", label: "B. 生活中的小美好" },
]}
correctAnswer="A"
errorMessage='不是哦,先生的创作从不是为了自己哦!"请回忆先生"弃医从文"的初心。"'
errorMessage="不是哦,先生的小说核心是批判而非歌颂哦!请结合阿Q的碎片回忆一下吧!"
onSubmit={handleQuiz2Submit}
/>
{/* 成功提示弹窗 */}
<SuccessAlert
visible={showSuccessAlert}
title="恭喜你,答对啦!"
content={successMessage}
onClose={() => {
setShowSuccessAlert(false);
setSuccessMessage("");
}}
onContinue={() => {
setShowSuccessAlert(false);
setSuccessMessage("");
}}
/>
{/* 通关提示对话 */}
<GuideSteps
visible={completionGuideVisible}
@@ -309,6 +339,8 @@ const LuxunReflectionScene3 = () => {
if (!visible) {
setCompletionGuideVisible(false);
setCompletionGuideFinished(true); // 对话关闭后标记为已完成
// 显示结束语覆盖层
setShowEndingOverlay(true);
}
}}
showMask={false}
@@ -316,9 +348,20 @@ const LuxunReflectionScene3 = () => {
zIndex={10000}
>
<GuideStepsItem speaker="guide">
恭喜你读懂了鲁迅的文字与初心,愿这份精神能伴你前行
恭喜你读懂了鲁迅的文字与初心,愿这份精神能伴你前行!
</GuideStepsItem>
</GuideSteps>
{/* 结束语覆盖层 */}
<EndingOverlay
visible={showEndingOverlay}
text="愿你走过我的文学足迹,读懂文字的力量,心怀热望,直面生活!"
animationSpeed={200}
onContinue={() => {
setShowEndingOverlay(false);
setEndingOverlayFinished(true);
}}
/>
</div>
);
};
+27 -25
View File
@@ -8,6 +8,8 @@ import {
type DragFillResult,
LetterToSunzhongshan,
StudyGuideModal,
Narration,
EndingOverlay,
} from "@/components";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { SuccessAlert, DragQuizPanel } from "@/components/layout";
@@ -74,25 +76,20 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
id: "quiz-2",
time: 42,
title: "题目2",
prompt: "我们新时代青年从孙中山先生身上能学到哪些?",
prompt: "【多选】新时代的青年要从孙中山先生身上学习哪些精神?",
type: "multiple",
options: [
{
value: "A",
label: '"心系民众,天下为公"的家国情怀',
label: "A.“振兴中华”的初心",
isCorrect: true,
},
{
value: "B",
label: "与时俱进,主动学习新知识,适应新形势",
isCorrect: true,
},
{ value: "C", label: "为中华民族伟大复兴而读书", isCorrect: true },
{
value: "D",
label: "将振兴中国之责任,置之于自身之肩上",
label: "B.“天下为公”的情怀",
isCorrect: true,
},
{ value: "C", label: "C.“与时俱进”的智慧", isCorrect: true },
],
},
];
@@ -112,6 +109,7 @@ const ChenyunInternalization = () => {
const [dragQuizErrors, setDragQuizErrors] = useState<Record<string, string>>(
{}
);
const [showNarration, setShowNarration] = useState(true); // 是否显示旁白
const [visible, setVisible] = useState(false);
const [showVideo, setShowVideo] = useState(false);
const [postGuideVisible, setPostGuideVisible] = useState(false);
@@ -120,9 +118,12 @@ const ChenyunInternalization = () => {
const videoRef = useRef<VideoRef>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
// 旁白关闭后显示对话引导
useEffect(() => {
if (!showNarration) {
setVisible(true);
}, []);
}
}, [showNarration]);
// 组件卸载时清理背景音乐
useEffect(() => {
@@ -330,9 +331,7 @@ const ChenyunInternalization = () => {
setPostGuideVisible(true);
if (audioRef.current) {
audioRef.current.currentTime = 0;
audioRef.current
.play()
.catch((error) => {
audioRef.current.play().catch((error) => {
console.warn("背景音乐播放失败:", error);
});
}
@@ -340,7 +339,16 @@ const ChenyunInternalization = () => {
/>
)}
{/* 引导 */}
{/* 旁白 */}
<Narration
visible={showNarration}
text="从翠亨村的农家院落到影响中国的革命征程,孙中山先生以'亟拯斯民于水火,切扶大厦之将倾'的担当,在近代中国的黑暗中点燃了民主共和的火种。他留下的不仅是推翻帝制、建立共和的历史功绩,更有'振兴中华'的初心、'天下为公'的情怀、'与时俱进'的智慧。今天,我们站在故居的青砖黛瓦间,不仅要回望他为民族独立、人民幸福奋斗的一生,更要让这份精神穿越时空,成为新时代青年砥砺前行的力量。"
onClose={() => {
setShowNarration(false);
}}
/>
{/* 对话引导 */}
<GuideSteps
visible={visible}
onVisibleChange={(visible) => {
@@ -353,7 +361,7 @@ const ChenyunInternalization = () => {
showMask={false}
>
<GuideStepsItem speaker="student">
小沪,参观完故居,我对孙中山先生从“医人”到“医国”的转变、《实业计划》里的宏大构想有了很多感触,但总觉得这些历史和精神离我们的生活还有些远,不知道该怎么真正理解和传承?
小沪,参观完故居,我对孙中山先生从"医人"到"医国"的转变、《实业计划》里的宏大构想有了很多感触,但总觉得这些历史和精神离我们的生活还有些远,不知道该怎么真正理解和传承?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
你的感受特别真实!孙中山先生的精神不是停留在书本里的文字,而是能指导我们当下行动的思想财富。今天我们特别邀请到了上海孙中山故居纪念馆研究部主任孙旻老师,让她带我们一起解读先生精神的时代意义,把这份传承落到实处。
@@ -389,8 +397,10 @@ const ChenyunInternalization = () => {
}}
/>
<StudyGuideModal
<EndingOverlay
visible={showStudyGuide}
text="恭喜你已经顺利完成了黄炎培故居的学习,请继续前往下一个故居吧!"
animationSpeed={200}
onContinue={() => {
if (audioRef.current) {
audioRef.current.pause();
@@ -399,15 +409,7 @@ const ChenyunInternalization = () => {
setShowStudyGuide(false);
navigate(`/${moduleName}/${starName}/home`);
}}
>
恭喜你已经顺利完成了孙中山故居纪念馆的学习,至此,你已经完成了本课程所有故居的学习,
希望你能有所收获,学习八位名人的优秀品质和精神,在新时代为中华民族伟大复兴发光发热!
{/* 同学你好,欢迎来到走进故居-对话学习模块!{"\n"}
进入陈云故居后,我们一起来了解陈云同志经历的7个阶段{"\n"}
“水乡少年 艰辛成长”、“商务启航 投身革命”、“苏区淬炼 崭露头角”、“延安担当
经济破局”、{"\n"}
“恢复时期 经济奠基”、“建设时期 务实兴邦”、“改革浪潮 誉画实业”; */}
</StudyGuideModal>
/>
</div>
);
};
+75 -12
View File
@@ -14,6 +14,48 @@ const SunzhongshanReflection = () => {
const [selectedScene, setSelectedScene] = useState<number | null>(null);
const [showGuide, setShowGuide] = useState(false);
const [sceneCompleted, setSceneCompleted] = useState<{
scene1: boolean;
scene2: boolean;
scene3: boolean;
}>({
scene1: false,
scene2: false,
scene3: false,
});
// 检查场景完成状态 - 每次组件挂载或从场景页面返回时重新检查
const checkSceneCompleted = () => {
const scene1Completed =
localStorage.getItem(`${starName}-reflection-scene1-completed`) ===
"true";
const scene2Completed =
localStorage.getItem(`${starName}-reflection-scene2-completed`) ===
"true";
const scene3Completed =
localStorage.getItem(`${starName}-reflection-scene3-completed`) ===
"true";
setSceneCompleted({
scene1: scene1Completed,
scene2: scene2Completed,
scene3: scene3Completed,
});
};
useEffect(() => {
checkSceneCompleted();
// 监听页面可见性变化,当页面重新可见时重新检查场景完成状态
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
checkSceneCompleted();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
// 检查是否是第一次进入,显示学习引导
useEffect(() => {
@@ -23,8 +65,19 @@ const SunzhongshanReflection = () => {
}
}, []);
// 检查场景是否可解锁
const isSceneUnlocked = (sceneNumber: number): boolean => {
if (sceneNumber === 1) return true; // 场景1始终可解锁
if (sceneNumber === 2) return sceneCompleted.scene1; // 场景2需要场景1完成
if (sceneNumber === 3) return sceneCompleted.scene2; // 场景3需要场景2完成
return false;
};
const handleSceneClick = (sceneNumber: number) => {
// 只有解锁的场景才能被选择
if (isSceneUnlocked(sceneNumber)) {
setSelectedScene(sceneNumber);
}
};
const handleGuideClose = () => {
@@ -61,9 +114,11 @@ const SunzhongshanReflection = () => {
<div className="absolute top-[46%] left-1/2 -translate-x-1/2 -translate-y-1/2 w-full flex items-center justify-center px-26">
{/* 场景一 */}
<div
className={`relative cursor-pointer transition-all duration-300 ${
selectedScene === 1 ? "z-30" : "z-10"
}`}
className={`relative transition-all duration-300 ${
isSceneUnlocked(1)
? "cursor-pointer"
: "cursor-not-allowed opacity-50"
} ${selectedScene === 1 ? "z-30" : "z-10"}`}
style={{
transform: `rotate(0deg) ${
selectedScene === 1 ? "scale(1.15)" : "scale(1)"
@@ -78,9 +133,11 @@ const SunzhongshanReflection = () => {
{/* 场景二 */}
<div
className={`relative cursor-pointer transition-all duration-300 ${
selectedScene === 2 ? "z-30" : "z-20"
}`}
className={`relative transition-all duration-300 ${
isSceneUnlocked(2)
? "cursor-pointer"
: "cursor-not-allowed opacity-50"
} ${selectedScene === 2 ? "z-30" : "z-20"}`}
style={{
transform: `rotate(0deg) ${
selectedScene === 2 ? "scale(1.15)" : "scale(1)"
@@ -95,9 +152,11 @@ const SunzhongshanReflection = () => {
{/* 场景三 */}
<div
className={`relative cursor-pointer transition-all duration-300 ${
selectedScene === 3 ? "z-30" : "z-10"
}`}
className={`relative transition-all duration-300 ${
isSceneUnlocked(3)
? "cursor-pointer"
: "cursor-not-allowed opacity-50"
} ${selectedScene === 3 ? "z-30" : "z-10"}`}
style={{
transform: `rotate(0deg) ${
selectedScene === 3 ? "scale(1.15)" : "scale(1)"
@@ -115,15 +174,19 @@ const SunzhongshanReflection = () => {
<div className="absolute bottom-30 left-1/2 -translate-x-1/2 z-10">
<div
className={`transition-all duration-300 ${
selectedScene
selectedScene && isSceneUnlocked(selectedScene)
? "opacity-100 cursor-pointer hover:scale-105"
: "opacity-50 cursor-not-allowed"
}`}
onClick={selectedScene ? handleStartExplore : undefined}
onClick={
selectedScene && isSceneUnlocked(selectedScene)
? handleStartExplore
: undefined
}
>
<Image
src={getStaticImage(`${starName}/reflection/home/按钮`)}
button={!!selectedScene}
button={!!(selectedScene && isSceneUnlocked(selectedScene))}
/>
</div>
</div>
+15 -14
View File
@@ -7,6 +7,7 @@ import {
flyToAnimation,
Video,
StudyGuideModal,
EndingOverlay,
} from "@/components";
import { SuccessAlert } from "@/components/layout";
import { SingleChoiceQuizPanel } from "@/components/layout/quiz";
@@ -234,6 +235,8 @@ const SunzhongshanReflectionScene1 = () => {
// 处理完成提示关闭
const handleCompleteGuideClose = () => {
setShowCompleteGuide(false);
// 保存场景1完成状态
localStorage.setItem(`${starName}-reflection-scene1-completed`, "true");
};
// 获取线索图片
@@ -433,9 +436,10 @@ const SunzhongshanReflectionScene1 = () => {
onContinue={handleGuideClose}
zIndex={10000}
>
同学你好,欢迎来到二十世纪初的上海。{"\n"}
同学你好,欢迎来到二十世纪初的上海。
<br />
辛亥革命之后,民主共和的观念深入人心。面对中外反动势力,社会各界更广泛地行动起来,以各自的方式争取着民主自由和国家尊严。
{"\n"}
<br />
你需要在本场景中找出4条线索,并正确回答所有问题后,才能拨开时局迷雾,进入下一个场景。
</StudyGuideModal>
@@ -444,8 +448,8 @@ const SunzhongshanReflectionScene1 = () => {
visible={showQuiz1}
title="今天的日期是?"
options={[
{ value: "A", label: "A: 1919年5月5日" },
{ value: "B", label: "B: 1921年5月5日" },
{ value: "A", label: "A.1919年5月5日" },
{ value: "B", label: "B.1921年5月5日" },
]}
correctAnswer="B"
errorMessage="回答错误,请你再仔细思考一下吧。"
@@ -461,12 +465,12 @@ const SunzhongshanReflectionScene1 = () => {
{
value: "A",
label:
"今天路过莫利爱路29号,旁观了中国国民党党部举行的庆祝活动。得知孙中山先生重整旗鼓,预备再度联合西南军阀进行二次护法。思及1918年第一次护法运动的失败,隐隐为孙先生此番努力感到忧虑。",
"A.今天路过莫利爱路29号,旁观了中国国民党党部举行的庆祝活动。得知孙中山先生重整旗鼓,预备再度联合西南军阀进行二次护法。思及1918年第一次护法运动的失败,隐隐为孙先生此番努力感到忧虑。",
},
{
value: "B",
label:
"参加了中华职业学校同学为庆祝广州军政府成立、孙中山在广州就任非常大总统而组织的活动,深感革命成功有望。",
"B.参加了中华职业学校同学为庆祝广州军政府成立、孙中山在广州就任非常大总统而组织的活动,深感革命成功有望。",
},
]}
correctAnswer="A"
@@ -482,11 +486,11 @@ const SunzhongshanReflectionScene1 = () => {
options={[
{
value: "A",
label: "A: 团结强大的军阀,共举民主共和旗帜",
label: "A.团结强大的军阀,共举民主共和旗帜",
},
{
value: "B",
label: "B: 团结各族各界群众,建立广泛的革命统一战线",
label: "B.团结各族各界群众,建立广泛的革命统一战线",
},
]}
correctAnswer="B"
@@ -496,14 +500,11 @@ const SunzhongshanReflectionScene1 = () => {
/>
{/* 完成提示弹窗 */}
<StudyGuideModal
<EndingOverlay
visible={showCompleteGuide}
text={`"恭喜你完成场景一的探索!{\n}粤军总司令陈炯明与孙中山政见无法统一。1922年6月16日,陈炯明部将叶举率4000余人包围总统府,炮击孙中山住所粤秀楼。这一叛变行动使得北伐事业再次陷于停顿,第二次护法运动失败……{\n}请你走进场景二,继续探索吧!"`}
onContinue={handleCompleteGuideClose}
zIndex={10002}
verticalAlign="center"
>
完成本游戏探索!快去体验其他故事吧!
</StudyGuideModal>
/>
</div>
);
};
+11 -9
View File
@@ -7,6 +7,7 @@ import {
flyToAnimation,
Video,
StudyGuideModal,
EndingOverlay,
} from "@/components";
import { SuccessAlert } from "@/components/layout";
import { SingleChoiceQuizPanel } from "@/components/layout/quiz";
@@ -244,6 +245,8 @@ const SunzhongshanReflectionScene2 = () => {
// 处理完成提示关闭
const handleCompleteGuideClose = () => {
setShowCompleteGuide(false);
// 保存场景2完成状态
localStorage.setItem(`${starName}-reflection-scene2-completed`, "true");
};
// 获取线索图片
@@ -448,8 +451,10 @@ const SunzhongshanReflectionScene2 = () => {
zIndex={10000}
>
1921年7月,中国共产党第一次全国代表大会在上海召开,中国革命的面貌自此焕然一新。
{"\n"}
1921-1923的两年间,你顺利完成了一、二年级的在校学习任务,并在三年级进入工厂实习。身为学子,你专注于业务精进,同时亦非常关注时局的变化。一些同学和工友加入了党组织,你也对此心生向往。本游戏需要在场景中找出5条线索,并回答正确所有问题后才能完成本游戏探索。
<br />
1921-1923的两年间,你顺利完成了一、二年级的在校学习任务,并在三年级进入工厂实习。身为学子,你专注于业务精进,同时亦非常关注时局的变化。一些同学和工友加入了党组织,你也对此心生向往……
<br />
你需要在本场景中找出5条线索,并正确回答所有问题后,才能拨开时局迷雾,进入下一个场景。
</StudyGuideModal>
{/* 题目1:当下无产阶级工人运动面对的敌人主要有哪些? */}
@@ -479,7 +484,7 @@ const SunzhongshanReflectionScene2 = () => {
{
value: "B",
label:
"B.回想一年多来,全国已经组织起了大小罢工上百次,多有捷报。此时虽面对一些挫折,但只要工人们始终坚定地团结在一起,敢于牺牲,就一定能战胜内外敌人!",
"B.回想一年多来,全国已经组织起了大小罢工上百次,多有捷报。仅依靠工人团结一定能够取得胜利!",
},
]}
correctAnswer="A"
@@ -509,14 +514,11 @@ const SunzhongshanReflectionScene2 = () => {
/>
{/* 完成提示弹窗 */}
<StudyGuideModal
<EndingOverlay
visible={showCompleteGuide}
text={`"恭喜你完成场景二的探索!{\n}1922年夏到1924年初,李大钊来到上海,在宣传革命思想的同时与孙中山进行了数次会面,为改组国民党和促成第一次国共合作作出了巨大贡献。{\n}1924年1月,中国国民党第一次全国代表大会在广州召开,孙中山重新解释了三民主义,事实上确立了联俄、联共、扶助农工三大政策。李大钊、毛泽东等10名共产党员当选为中国国民党第一届中央执行委员会委员或候补委员。国民党一大的召开标志着第一次国共合作的正式形成。{\n}请你走进场景三,继续探索吧!"`}
onContinue={handleCompleteGuideClose}
zIndex={10002}
>
1922年夏到1924年初,李大钊来到上海,在宣传革命思想的同时与孙中山进行了数次会面,为改组国民党和促成第一次国共合作作出了巨大贡献。
1924年1月,中国国民党第一次全国代表大会在广州召开,孙中山重新解释了三民主义,事实上确立了联俄、联共、扶助农工三大政策。李大钊、毛泽东等10名共产党员当选为中国国民党第一届中央执行委员会委员或候补委员。国民党一大的召开标志着第一次国共合作的正式形成。
</StudyGuideModal>
/>
</div>
);
};
+41 -41
View File
@@ -8,6 +8,7 @@ import {
Video,
StudyGuideModal,
Cursor,
EndingOverlay,
} from "@/components";
import { SuccessAlert, ErrorAlert } from "@/components/layout";
import { SingleChoiceQuizPanel } from "@/components/layout/quiz";
@@ -229,29 +230,31 @@ const SunzhongshanReflectionScene3 = () => {
}
};
// 处理题目2提交 - 两个选项都正确
const handleQuiz2Submit = (_value: string, _isCorrect: boolean) => {
// 题目2的两个选项都正确,所以总是成功
// 处理题目2提交
const handleQuiz2Submit = (_value: string, isCorrect: boolean) => {
if (isCorrect) {
setShowQuiz2(false);
setSuccessAlertContent("回答正确!");
setShowSuccessAlert(true);
setPendingQuiz("quiz3");
}
};
// 处理题目3提交
const handleQuiz3Submit = (_value: string, isCorrect: boolean) => {
if (isCorrect) {
// 处理题目3提交 - 两个选项都正确
const handleQuiz3Submit = (_value: string, _isCorrect: boolean) => {
// 题目3的两个选项都正确,所以总是成功
setShowQuiz3(false);
setSuccessAlertContent("回答正确!");
setShowSuccessAlert(true);
// 设置待显示的完成提示
setPendingQuiz("complete");
}
};
// 处理完成提示关闭
const handleCompleteGuideClose = () => {
setShowCompleteGuide(false);
// 保存场景3完成状态
localStorage.setItem(`${starName}-reflection-scene3-completed`, "true");
};
// 处理 Cursor 点击 - 切换到主场景
@@ -501,7 +504,7 @@ const SunzhongshanReflectionScene3 = () => {
后来,学校建立了中国社会主义青年团团支部,你积极靠拢,成为一名光荣的团员。
国共合作大背景下,国民革命正在如火如荼地开展,北伐也在积极酝酿之中。你在自己的岗位上认真工作,也密切关注着时局和两党动态。
{"\n"}
本游戏需要在场景中找出4条线索,并回答正确所有问题后才能完成本游戏探索。
你需要在本场景中找出4条线索,并正确回答所有问题后,才能拨开时局迷雾,完成终极任务。
</StudyGuideModal>
{/* 题目1:冯玉祥发动"北京政变"后邀请孙中山北上共商国是,各界对此事的看法如何? */}
@@ -509,8 +512,8 @@ const SunzhongshanReflectionScene3 = () => {
visible={showQuiz1}
title='冯玉祥发动"北京政变"后邀请孙中山北上共商国是,各界对此事的看法如何?'
options={[
{ value: "A", label: "A: 意见高度一致,认为和平统一已经十拿九稳" },
{ value: "B", label: "B: 期待、信任为主,亦有担忧和谨慎的探讨" },
{ value: "A", label: "A.意见高度一致,认为和平统一已经十拿九稳" },
{ value: "B", label: "B.期待、信任为主,亦有担忧和谨慎的探讨" },
]}
correctAnswer="B"
errorMessage="回答错误,请你再仔细思考一下吧。"
@@ -518,58 +521,55 @@ const SunzhongshanReflectionScene3 = () => {
zIndex={10001}
/>
{/* 题目2:今天的日记写什么呢? */}
{/* 题目2:毅然北上赴会的孙中山先生,为争取国家和平统一作出了哪些努力? */}
<SingleChoiceQuizPanel
visible={showQuiz2}
title="今天的日记写什么呢?"
options={[
{
value: "A",
label:
"A: 当前情形,反对军阀势力进而反对列强压迫已成为国共两党乃至各界共识。只是这一目标应当如何达成呢?今年上半年就听说黄埔军校在全国招生的消息,大批青年踊跃报考。可见组建军队,武力北伐是一个公认的选项。然下半年直系军阀倒台,冯玉祥电邀孙先生北上主政,便又打开了和平统一的一线契机,如能实现,亦是应了国人的期盼。今日孙先生行程抵沪,马不停蹄接见各界人士,宣讲北上主张,我辈见之无不钦佩其鞠躬尽瘁之心。前路未明,探索不止,希望国民共同努力,早日迎来和平民主的新天地。",
},
{
value: "B",
label:
'B: 孙中山先生抵沪,来自百年后的我站在欢迎的人群中,终于亲眼见到了这位伟大的革命先行者。1924年的秋天,冯玉祥北京政变为国家的和平统一带来一线契机。回望历史,我们知道,这在中国近代的民族民主革命路途上,不过是一个插曲。但对于孙中山先生而言,抱病北上,不遗余力地宣传反帝反封建主张,几乎成为他为国家前途命运而作的最后争取。他在迷途中坚守,在变局中担当,穷极一生从未停下脚步。伟大复兴的新征程上,我们早已摆脱了屈辱与苦难,向着国家富强的光明前景、人民幸福的美好未来进发。先生"振兴中华"的呐喊犹在耳畔,"天下为公"的初心始终滚烫。今天我们所能做的,便是铭记这份坚守与担当,做跨越百年的追梦路上最坚定的继承者与前行者。',
},
]}
correctAnswer="A"
errorMessage=""
onSubmit={handleQuiz2Submit}
zIndex={10001}
/>
{/* 题目3:毅然北上赴会的孙中山先生,为争取国家和平统一作出了哪些努力? */}
<SingleChoiceQuizPanel
visible={showQuiz3}
title="毅然北上赴会的孙中山先生,为争取国家和平统一作出了哪些努力?"
options={[
{
value: "A",
label:
"A: 重视舆论导向,动身前发表《北上宣言》明确态度,沿途不辞劳苦与各界会晤交流,反复重申主张。",
"A.重视舆论导向,动身前发表《北上宣言》明确态度,沿途不辞劳苦与各界会晤交流,反复重申主张。",
},
{
value: "B",
label: "B: 珍惜契机,沿途不做停顿,以最快的速度赶往北京。",
label: "B.珍惜契机,沿途不做停顿,以最快的速度赶往北京。",
},
]}
correctAnswer="A"
errorMessage="回答错误,请你再仔细思考一下吧。"
onSubmit={handleQuiz2Submit}
zIndex={10001}
/>
{/* 题目3:今天的日记写什么呢? */}
<SingleChoiceQuizPanel
visible={showQuiz3}
title="今天的日记写什么呢?"
options={[
{
value: "A",
label:
"A.当前情形,反对军阀势力进而反对列强压迫已成为国共两党乃至各界共识。只是这一目标应当如何达成呢?今年上半年就听说黄埔军校在全国招生的消息,大批青年踊跃报考。可见组建军队,武力北伐是一个公认的选项。然下半年直系军阀倒台,冯玉祥电邀孙先生北上主政,便又打开了和平统一的一线契机,如能实现,亦是应了国人的期盼。今日孙先生行程抵沪,马不停蹄接见各界人士,宣讲北上主张,我辈见之无不钦佩其鞠躬尽瘁之心。前路未明,探索不止,希望国民共同努力,早日迎来和平民主的新天地。",
},
{
value: "B",
label:
'B.孙中山先生抵沪,来自百年后的我站在欢迎的人群中,终于亲眼见到了这位伟大的革命先行者。1924年的秋天,冯玉祥北京政变为国家的和平统一带来一线契机。回望历史,我们知道,这在中国近代的民族民主革命路途上,不过是一个插曲。但对于孙中山先生而言,抱病北上,不遗余力地宣传反帝反封建主张,几乎成为他为国家前途命运而作的最后争取。他在迷途中坚守,在变局中担当,穷极一生从未停下脚步。伟大复兴的新征程上,我们早已摆脱了屈辱与苦难,向着国家富强的光明前景、人民幸福的美好未来进发。先生"振兴中华"的呐喊犹在耳畔,"天下为公"的初心始终滚烫。今天我们所能做的,便是铭记这份坚守与担当,做跨越百年的追梦路上最坚定的继承者与前行者。',
},
]}
correctAnswer="A"
errorMessage=""
onSubmit={handleQuiz3Submit}
zIndex={10001}
/>
{/* 完成提示弹窗 */}
<StudyGuideModal
<EndingOverlay
visible={showCompleteGuide}
text={`"辛亥革命拉开了中国完全意义上的近代民族民主革命的序幕。北洋军阀首领袁世凯窃取革命成果后,各地军阀势力对内割据混战,对外出卖国家利益,在政治上谋求专制和独裁,俨然成为民族独立、民主政治、民生幸福之路上的头号绊脚石。{\n}为了改变这一时期的困局,孙中山尝试过高举护法旗帜,两度领导护法运动;与新生的中国共产党展开合作,与无产阶级工农群众共建起统一战线;为了团结一切可以团结的力量,调动一切可以调动的因素,在生命最后的日子里抱病北上,一路不遗余力地向社会各界宣传反帝反封建主张。他经历了太多挫折,却始终以'行'求'知'。"`}
onContinue={handleCompleteGuideClose}
zIndex={10002}
>
辛亥革命拉开了中国完全意义上的近代民族民主革命的序幕。北洋军阀首领袁世凯窃取革命成果后,各地军阀势力对内割据混战,对外出卖国家利益,在政治上谋求专制和独裁,俨然成为民族独立、民主政治、民生幸福之路上的头号绊脚石。
为了改变这一时期的困局,孙中山尝试过高举护法旗帜,两度领导护法运动;与新生的中国共产党展开合作,与无产阶级工农群众共建起统一战线;为了团结一切可以团结的力量,调动一切可以调动的因素,在生命最后的日子里抱病北上,一路不遗余力地向社会各界宣传反帝反封建主张。他经历了太多挫折,却始终以“行”求“知”。
</StudyGuideModal>
/>
</div>
);
};