395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
import { useEffect, useMemo, useState, useRef } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import {
|
|
Image,
|
|
Video,
|
|
type VideoAnchor,
|
|
type VideoRef,
|
|
type DragFillResult,
|
|
LetterToChenyun,
|
|
StudyGuideModal,
|
|
} from "@/components";
|
|
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
|
|
import { SuccessAlert, DragQuizPanel } from "@/components/layout";
|
|
import { MultipleChoiceQuizPanel } 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" | "drag";
|
|
options: QuizOption[];
|
|
// 拖拽题专用
|
|
blanks?: { id: string; label: string }[];
|
|
};
|
|
|
|
// 星名固定为 chenyun
|
|
const starName = "chenyun";
|
|
|
|
// 锚点数据:可根据实际视频时长调整 time
|
|
const QUIZ_ANCHORS: QuizAnchor[] = [
|
|
{
|
|
id: "quiz-1",
|
|
time: Math.ceil(5.52 * 60), //5.52分钟
|
|
title: "题目1",
|
|
prompt: "陈云经济思想的核心内容指的是( )。",
|
|
type: "multiple",
|
|
options: [
|
|
{ value: "A", label: "综合平衡", isCorrect: true },
|
|
{
|
|
value: "B",
|
|
label: "鸟笼比喻",
|
|
explanation: "鸟笼比喻是正确的处理好计划与市场的关系的一个发展阶段",
|
|
},
|
|
{ value: "C", label: "正确的处理好计划与市场的关系", isCorrect: true },
|
|
{ value: "D", label: "经济建设的目的论", isCorrect: true },
|
|
],
|
|
},
|
|
{
|
|
id: "quiz-2",
|
|
time: Math.ceil(10.01 * 60),
|
|
title: "题目2",
|
|
prompt: "陈云说过以下哪些话能着重体现出他的经济思想。",
|
|
type: "multiple",
|
|
options: [
|
|
{
|
|
value: "A",
|
|
label: "不唯上、不唯书、只唯实,交换、比较、反复",
|
|
isCorrect: true,
|
|
},
|
|
{ value: "B", label: "无农不稳,无粮则乱", isCorrect: true },
|
|
{ value: "C", label: "个人名利淡如水,党的事业重如山。", isCorrect: true },
|
|
{
|
|
value: "D",
|
|
label: "领导机关制定政策,要用百分之九十以上的时间作调查研究工作。",
|
|
isCorrect: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "quiz-3",
|
|
time: Math.ceil(16.04 * 60),
|
|
title: "题目3",
|
|
prompt: "我们新时代的青年可以从陈云同志身上学习到哪些精神",
|
|
type: "drag",
|
|
options: [
|
|
{ value: "积极的人生态度", label: "积极的人生态度", isCorrect: true },
|
|
{ value: "刻苦的学习精神", label: "刻苦的学习精神", isCorrect: true },
|
|
{ value: "朴素的节俭精神", label: "朴素的节俭精神", isCorrect: true },
|
|
{ value: "实事求是的精神", label: "实事求是的精神", isCorrect: true },
|
|
],
|
|
blanks: [
|
|
{ id: "blank-1", label: "____" },
|
|
{ id: "blank-2", label: "____" },
|
|
{ id: "blank-3", label: "____" },
|
|
{ id: "blank-4", label: "____" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const ChenyunInternalization = () => {
|
|
const navigate = useNavigate();
|
|
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 [dragQuizValues, setDragQuizValues] = useState<
|
|
Record<string, DragFillResult[]>
|
|
>({});
|
|
const [dragQuizErrors, setDragQuizErrors] = useState<Record<string, string>>(
|
|
{}
|
|
);
|
|
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 === "drag" && item.blanks) {
|
|
return {
|
|
id: item.id,
|
|
time: item.time,
|
|
once: true,
|
|
afterResume: "remove",
|
|
render: ({ resume }) => {
|
|
const handleDragChange = (val: DragFillResult[]) => {
|
|
setDragQuizValues((prev) => ({ ...prev, [item.id]: val }));
|
|
setDragQuizErrors((prev) => ({ ...prev, [item.id]: "" }));
|
|
|
|
if (val.length < correctAnswers.length) return;
|
|
|
|
// 检查是否包含了所有正确答案(不要求顺序)
|
|
const filledAnswers = val.map((v) => v.optionValue);
|
|
const answerSet = new Set(correctAnswers);
|
|
const filledSet = new Set(filledAnswers);
|
|
const isCorrect =
|
|
answerSet.size === filledSet.size &&
|
|
[...answerSet].every((answer) => filledSet.has(answer));
|
|
|
|
if (isCorrect) {
|
|
setActiveSuccess(item.id);
|
|
setSuccessContent("回答正确,请继续探索。");
|
|
setResumeCallback(() => resume);
|
|
} else {
|
|
setDragQuizErrors((prev) => ({
|
|
...prev,
|
|
[item.id]: "再想一想,确保选择了所有正确答案~",
|
|
}));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<DragQuizPanel
|
|
visible={activeSuccess !== item.id}
|
|
zIndex={10000}
|
|
title={`${item.title}(拖拽)`}
|
|
questionPrefix={`${item.prompt}(`}
|
|
questionSuffix=")?"
|
|
options={item.options.map((opt) => ({
|
|
value: opt.value,
|
|
label: opt.label,
|
|
}))}
|
|
blanks={item.blanks || []}
|
|
value={dragQuizValues[item.id] || []}
|
|
error={dragQuizErrors[item.id]}
|
|
onChange={handleDragChange}
|
|
/>
|
|
<SuccessAlert
|
|
visible={activeSuccess === item.id}
|
|
title="恭喜你, 选择正确!"
|
|
content={successContent}
|
|
onContinue={() => {
|
|
setActiveSuccess(null);
|
|
setSuccessContent("");
|
|
setDragQuizValues((prev) => {
|
|
const next = { ...prev };
|
|
delete next[item.id];
|
|
return next;
|
|
});
|
|
setDragQuizErrors((prev) => {
|
|
const next = { ...prev };
|
|
delete next[item.id];
|
|
return next;
|
|
});
|
|
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,
|
|
dragQuizValues,
|
|
dragQuizErrors,
|
|
]
|
|
);
|
|
|
|
if (!moduleName) return null;
|
|
|
|
return (
|
|
<div
|
|
className="relative h-full w-full bg-black"
|
|
style={{
|
|
backgroundImage: getBackgroundImage("components/internalization/bg"),
|
|
}}
|
|
>
|
|
{/* 离开按钮 */}
|
|
<Image
|
|
className="absolute right-8 top-8 z-10"
|
|
src={getStaticImage("components/离开")}
|
|
button
|
|
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
|
|
/>
|
|
|
|
{showVideo && (
|
|
<Video
|
|
ref={videoRef}
|
|
src={getPublicPath("video/chenyun/chenyun_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="student">
|
|
小沪,跟着你参观陈云纪念馆真是收获满满!看到馆里展示的“米棉之战”史料、全国财经统筹的实物展品,我真切感受到陈云爷爷在建国初期平抑物价、稳定经济的魄力。
|
|
</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="student">
|
|
听完房主任的讲解,从陈云爷爷童年成长的水乡故居,到他主持"银元之战""米棉之战"的史料,再到那台称量粮食的天平、握不住的铅笔头,我真切感受到他"不唯上、不唯书、只唯实"的精神,也懂了他"为老百姓做事"的初心...
|
|
</GuideStepsItem>
|
|
<GuideStepsItem speaker="student">
|
|
这些都让我特别受触动!
|
|
</GuideStepsItem>
|
|
<GuideStepsItem speaker="guide">
|
|
你的感悟很深刻!陈云同志用一生诠释了"务实"和"为人民服务"——从破解沿海地区经济困境,到掌管国家工业化蓝图,再到改革开放时力挺轻纺、钢铁建设支援,每一步都扎根实际、心系百姓。
|
|
</GuideStepsItem>
|
|
<GuideStepsItem speaker="guide">
|
|
你不妨把这份触动凝练成几句心里话,既总结参观的收获,也想想未来该如何践行这份精神,用自己的行动传承这份初心吧!
|
|
</GuideStepsItem>
|
|
</GuideSteps>
|
|
|
|
{/* 信件组件:视频引导结束后弹出,带飞入和放大动画 */}
|
|
<LetterToChenyun
|
|
visible={showLetter}
|
|
onSubmit={(values) => {
|
|
// 提交信件的逻辑(values 为多个填空的内容)
|
|
console.log("提交信件", values);
|
|
setShowLetter(false);
|
|
setShowStudyGuide(true);
|
|
}}
|
|
/>
|
|
|
|
<StudyGuideModal
|
|
visible={showStudyGuide}
|
|
onContinue={() => {
|
|
setShowStudyGuide(false);
|
|
navigate(`/${moduleName}/${starName}/home`);
|
|
}}
|
|
>
|
|
恭喜你已经顺利完成了陈云纪念馆的学习,至此,你已经完成了本课程所有故居的学习,
|
|
希望你能有所收获,学习八位名人的优秀品质和精神,在新时代为中华民族伟大复兴发光发热!
|
|
{/* 同学你好,欢迎来到走进故居-对话学习模块!{"\n"}
|
|
进入陈云故居后,我们一起来了解陈云同志经历的7个阶段{"\n"}
|
|
“水乡少年 艰辛成长”、“商务启航 投身革命”、“苏区淬炼 崭露头角”、“延安担当
|
|
经济破局”、{"\n"}
|
|
“恢复时期 经济奠基”、“建设时期 务实兴邦”、“改革浪潮 誉画实业”; */}
|
|
</StudyGuideModal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChenyunInternalization;
|