feat:黄炎培对话对接中
This commit is contained in:
1 parent
3611a0857a
commit
6ab184b328
125 files changed
+6617
-2391
No files matched your search
@@ -1,38 +1,335 @@
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import {
|
||||
Image,
|
||||
Video,
|
||||
type VideoAnchor,
|
||||
type VideoRef,
|
||||
LetterToLuxun,
|
||||
StudyGuideModal,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
|
||||
import { SuccessAlert } from "@/components/layout";
|
||||
import {
|
||||
MultipleChoiceQuizPanel,
|
||||
SingleChoiceQuizPanel,
|
||||
} from "@/components/layout/quiz";
|
||||
import { GuideSteps, GuideStepsItem } from "@/components";
|
||||
|
||||
type QuizOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
isCorrect?: boolean;
|
||||
explanation?: string;
|
||||
};
|
||||
|
||||
type QuizAnchor = {
|
||||
id: string;
|
||||
time: number;
|
||||
title: string;
|
||||
prompt: string;
|
||||
type: "single" | "multiple";
|
||||
options: QuizOption[];
|
||||
};
|
||||
|
||||
const starName = "luxun";
|
||||
|
||||
// 锚点数据:根据视频时长调整 time
|
||||
const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
{
|
||||
id: "quiz-1",
|
||||
time: 155, // 2:35
|
||||
title: "题目1",
|
||||
prompt: "以下属于鲁迅先生作品的是( )。",
|
||||
type: "multiple",
|
||||
options: [
|
||||
{ value: "A", label: "孔乙己", isCorrect: true },
|
||||
{ value: "B", label: "药", isCorrect: true },
|
||||
{ value: "C", label: "狂人日记", isCorrect: true },
|
||||
{ value: "D", label: "阿Q正传", isCorrect: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quiz-2",
|
||||
time: 355, // 5:55
|
||||
title: "题目2",
|
||||
prompt: "对于鼓励和培养青年一代,鲁迅先生的做法是( )。",
|
||||
type: "multiple",
|
||||
options: [
|
||||
{ value: "A", label: "组织文学社团", isCorrect: true },
|
||||
{ value: "B", label: "木刻运动", isCorrect: true },
|
||||
{ value: "C", label: "写纪念烈士文章", isCorrect: true },
|
||||
{
|
||||
value: "D",
|
||||
label: "让青年都去参军",
|
||||
explanation: "鲁迅先生还是在文学和思想领域培养青年一代,传递思想的火苗",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quiz-3",
|
||||
time: 420, // 7:00
|
||||
title: "题目3",
|
||||
prompt: "以下那句诗出自鲁迅的作品( )。",
|
||||
type: "single",
|
||||
options: [
|
||||
{ value: "A", label: "寄意寒星荃不察,我以我血荐轩辕", isCorrect: true },
|
||||
{
|
||||
value: "B",
|
||||
label: "我自横刀向天笑,去留肝胆两昆仑",
|
||||
explanation: "出自谭嗣同《狱中题壁》",
|
||||
},
|
||||
{
|
||||
value: "C",
|
||||
label: "此去泉台招旧部,旌旗十万斩阎罗",
|
||||
explanation: "出自陈毅《梅岭三章》",
|
||||
},
|
||||
{
|
||||
value: "D",
|
||||
label: "雄关漫道真如铁,而今迈步从头越",
|
||||
explanation: "出自毛泽东《忆秦娥·娄山关》",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const LuxunInternalization = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
const { moduleName } = useParams<{ moduleName: string }>();
|
||||
const [activeSuccess, setActiveSuccess] = useState<string | null>(null);
|
||||
const [successContent, setSuccessContent] = useState<string>("");
|
||||
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(
|
||||
null
|
||||
);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [showVideo, setShowVideo] = useState(false);
|
||||
const [postGuideVisible, setPostGuideVisible] = useState(false);
|
||||
const [showLetter, setShowLetter] = useState(false);
|
||||
const [showStudyGuide, setShowStudyGuide] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleVideoCanPlay = () => {
|
||||
if (showVideo && videoRef.current) {
|
||||
videoRef.current.play().catch((error) => {
|
||||
console.warn("视频播放失败:", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const anchors: VideoAnchor[] = useMemo(
|
||||
() =>
|
||||
QUIZ_ANCHORS.map((item) => {
|
||||
const correctAnswers = item.options
|
||||
.filter((opt) => opt.isCorrect)
|
||||
.map((opt) => opt.value);
|
||||
|
||||
// 单选题处理
|
||||
if (item.type === "single") {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: "remove",
|
||||
render: ({ resume }) => (
|
||||
<>
|
||||
<SingleChoiceQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title={`${item.title}(单选题):${item.prompt}`}
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: `${opt.value}. ${opt.label}`,
|
||||
}))}
|
||||
correctAnswer={correctAnswers[0]}
|
||||
errorMessage="再思考一下,选择正确答案。"
|
||||
onSubmit={(value: string, isCorrect: boolean) => {
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id);
|
||||
setSuccessContent("回答正确,请继续探索。");
|
||||
setResumeCallback(() => resume);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null);
|
||||
setSuccessContent("");
|
||||
const cb = resumeCallback;
|
||||
setResumeCallback(null);
|
||||
cb?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 多选题处理
|
||||
if (item.type === "multiple") {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: "remove",
|
||||
render: ({ resume }) => (
|
||||
<>
|
||||
<MultipleChoiceQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title={`${item.title}(多选题):${item.prompt}`}
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: `${opt.value}. ${opt.label}`,
|
||||
}))}
|
||||
correctAnswer={correctAnswers}
|
||||
errorMessage="再思考一下,注意选择所有正确答案。"
|
||||
onSubmit={(values: string[], isCorrect: boolean) => {
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id);
|
||||
setSuccessContent("回答正确,请继续探索。");
|
||||
setResumeCallback(() => resume);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null);
|
||||
setSuccessContent("");
|
||||
const cb = resumeCallback;
|
||||
setResumeCallback(null);
|
||||
cb?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: "remove",
|
||||
render: () => null,
|
||||
};
|
||||
}),
|
||||
[activeSuccess, resumeCallback, successContent]
|
||||
);
|
||||
|
||||
if (!moduleName) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
className="relative h-full w-full bg-black"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/internalization/底图`),
|
||||
backgroundImage: getBackgroundImage("components/internalization/bg"),
|
||||
}}
|
||||
>
|
||||
{/* 离开按钮 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
|
||||
/>
|
||||
|
||||
{showVideo && (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={getPublicPath("video/luxun/luxun_neihua.mp4")}
|
||||
className="h-full w-full"
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false);
|
||||
setPostGuideVisible(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible);
|
||||
if (!visible) {
|
||||
setShowVideo(true);
|
||||
handleVideoCanPlay();
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
小沪,关于"首在立人"走进上海鲁迅故居的红砖小楼,解锁了先生从绍兴到上海的文学足迹,我们读懂了他以笔为刃的战斗姿态,也见证了他与进步人士的革命情谊。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
那些针砭时弊的杂文、借古讽今的篇章,还有藏在6平方米客房里的红色坚守,都在诉说着先生的热忱。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
1907年,鲁迅先生在《文化偏至论》里写到:“是故将生存两间,角逐列国是务,其首在立人,人立而后凡事举;若其道术,乃必尊个性而张精神。”
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
先生认为,中华民族要屹立于世界民族之林,“首在立人”,“立人”思想从此贯穿在鲁迅先生的人生和作品中。”先生认为,中华民族要屹立于世界民族之林,“首在立人”,“立人”思想从此贯穿在鲁迅先生的人生和作品中。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="student">
|
||||
小沪,关于"首在立人"这四个字我还是有些不理解,立人为什么这么重要?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
"首在立人"这四个字,可以说是鲁迅先生一生的价值观。今天我们有幸邀请到了上海鲁迅纪念馆研究馆员施晓燕。关于你的问题,我想施老师会给你指点迷津!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 视频结束后的引导 */}
|
||||
<GuideSteps
|
||||
visible={postGuideVisible}
|
||||
onVisibleChange={(visible) => {
|
||||
setPostGuideVisible(visible);
|
||||
if (!visible) {
|
||||
setShowLetter(true);
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
zIndex={60}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
听完施老师的讲解,相信你对鲁迅先生的"立人"精神有了更深的理解。现在,就请你把这份感悟写下来,既总结参观的收获,也给自己未来许下一个承诺吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 信件组件 */}
|
||||
<LetterToLuxun
|
||||
visible={showLetter}
|
||||
onSubmit={(values: string[]) => {
|
||||
console.log("提交信件", values);
|
||||
setShowLetter(false);
|
||||
setShowStudyGuide(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">鲁迅故居-内化模块(待完善)</div>
|
||||
</div>
|
||||
|
||||
<StudyGuideModal
|
||||
visible={showStudyGuide}
|
||||
onContinue={() => {
|
||||
setShowStudyGuide(false);
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
>
|
||||
恭喜你已经顺利完成了鲁迅故居的学习,至此,你已经完成了本课程所有故居的学习,
|
||||
希望你能有所收获,学习八位名人的优秀品质和精神,在新时代为中华民族伟大复兴发光发热!
|
||||
</StudyGuideModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunInternalization;
|
||||
|
||||
Reference in new issue
Block a user