feat:黄炎培对话对接中
This commit is contained in:
1 parent
3611a0857a
commit
6ab184b328
125 files changed
+6617
-2391
No files matched your search
@@ -1,38 +1,382 @@
|
||||
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,
|
||||
type DragFillResult,
|
||||
LetterToSunzhongshan,
|
||||
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 }[];
|
||||
};
|
||||
|
||||
// 星名固定为 sunzhongshan
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
const SunzhongshanInternalization = () => {
|
||||
// 锚点数据:可根据实际视频时长调整 time
|
||||
const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
{
|
||||
id: "quiz-1",
|
||||
time: 11, //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: 42,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ChenyunInternalization = () => {
|
||||
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 [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="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/sunzhongshan/sunzhongshan_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="guide">
|
||||
你的感受特别真实!孙中山先生的精神不是停留在书本里的文字,而是能指导我们当下行动的思想财富。今天我们特别邀请到了上海孙中山故居纪念馆研究部主任孙旻老师,让她带我们一起解读先生精神的时代意义,把这份传承落到实处。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 视频结束后的引导 */}
|
||||
<GuideSteps
|
||||
visible={postGuideVisible}
|
||||
onVisibleChange={(visible) => {
|
||||
setPostGuideVisible(visible);
|
||||
// 引导结束后显示信件
|
||||
if (!visible) {
|
||||
setShowLetter(true);
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
zIndex={60}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
再看香山路7号这座小楼,相信你有很多心里话想对孙中山先生说。现在,就请你把这份感悟浓缩成几句话,既呼应先生的精神,也给自己未来许下一个承诺吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 信件组件:视频引导结束后弹出,带飞入和放大动画 */}
|
||||
<LetterToSunzhongshan
|
||||
visible={showLetter}
|
||||
onSubmit={(values: string[]) => {
|
||||
// 提交信件的逻辑(values 为多个填空的内容)
|
||||
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`);
|
||||
}}
|
||||
>
|
||||
恭喜你已经顺利完成了孙中山故居纪念馆的学习,至此,你已经完成了本课程所有故居的学习,
|
||||
希望你能有所收获,学习八位名人的优秀品质和精神,在新时代为中华民族伟大复兴发光发热!
|
||||
{/* 同学你好,欢迎来到走进故居-对话学习模块!{"\n"}
|
||||
进入陈云故居后,我们一起来了解陈云同志经历的7个阶段{"\n"}
|
||||
“水乡少年 艰辛成长”、“商务启航 投身革命”、“苏区淬炼 崭露头角”、“延安担当
|
||||
经济破局”、{"\n"}
|
||||
“恢复时期 经济奠基”、“建设时期 务实兴邦”、“改革浪潮 誉画实业”; */}
|
||||
</StudyGuideModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanInternalization;
|
||||
|
||||
export default ChenyunInternalization;
|
||||
@@ -53,8 +53,8 @@ const SunzhongshanPlace1 = () => {
|
||||
const total = 5;
|
||||
const completed = progressCount >= total;
|
||||
const subtitle = completed
|
||||
? "探索孙中山故居老宅堂屋(已完成)"
|
||||
: `探索孙中山故居老宅堂屋(${progressCount}/${total})`;
|
||||
? "探索孙中山故居纪念馆老宅堂屋(已完成)"
|
||||
: `探索孙中山故居纪念馆老宅堂屋(${progressCount}/${total})`;
|
||||
const subtitleClassName = completed
|
||||
? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]"
|
||||
: undefined;
|
||||
|
||||
@@ -50,8 +50,8 @@ const SunzhongshanPlace2 = () => {
|
||||
const total = 4;
|
||||
const completed = progressCount >= total;
|
||||
const subtitle = completed
|
||||
? "探索孙中山故居老宅堂屋(已完成)"
|
||||
: `探索孙中山故居老宅堂屋(${progressCount}/${total})`;
|
||||
? "探索孙中山故居纪念馆老宅堂屋(已完成)"
|
||||
: `探索孙中山故居纪念馆老宅堂屋(${progressCount}/${total})`;
|
||||
const subtitleClassName = completed
|
||||
? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]"
|
||||
: undefined;
|
||||
|
||||
@@ -31,8 +31,8 @@ const SunzhongshanPlace3 = () => {
|
||||
const total = 4;
|
||||
const completed = progressCount >= total;
|
||||
const subtitle = completed
|
||||
? "探索孙中山故居老宅堂屋(已完成)"
|
||||
: `探索孙中山故居老宅堂屋(${progressCount}/${total})`;
|
||||
? "探索孙中山故居纪念馆老宅堂屋(已完成)"
|
||||
: `探索孙中山故居纪念馆老宅堂屋(${progressCount}/${total})`;
|
||||
const subtitleClassName = completed
|
||||
? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]"
|
||||
: undefined;
|
||||
|
||||
@@ -27,8 +27,8 @@ const SunzhongshanPlace4 = () => {
|
||||
const total = 2;
|
||||
const completed = progressCount >= total;
|
||||
const subtitle = completed
|
||||
? "探索孙中山故居老宅堂屋(已完成)"
|
||||
: `探索孙中山故居老宅堂屋(${progressCount}/${total})`;
|
||||
? "探索孙中山故居纪念馆老宅堂屋(已完成)"
|
||||
: `探索孙中山故居纪念馆老宅堂屋(${progressCount}/${total})`;
|
||||
const subtitleClassName = completed
|
||||
? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]"
|
||||
: undefined;
|
||||
|
||||
Reference in new issue
Block a user