feat:黄炎培对话对接中

This commit is contained in:
zhongyidian committed 2026-01-07 21:08:38 +08:00
1 parent 3611a0857a
commit 6ab184b328
125 files changed
+6617 -2391

No files matched your search

+406 -15
View File
@@ -1,38 +1,429 @@
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,
LetterToHuangyanpei,
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 }[];
};
const starName = "huangyanpei";
// 锚点数据:根据视频时长调整 time
const QUIZ_ANCHORS: QuizAnchor[] = [
{
id: "quiz-1",
time: 96, // 1:36
title: "题目1",
prompt:
"当年黄炎培先生创办中华职业学校,开设的专业都是当时中国最需要的实业领域,分别是( )。",
type: "multiple",
options: [
{ value: "A", label: "铁工", isCorrect: true },
{
value: "B",
label: "自动控制",
explanation:
"这一工程理论框架成形于20世纪中叶,70年代实现集中控制,新中国成立前,没有相关学科基础、设备与技术条件",
},
{ value: "C", label: "木工", isCorrect: true },
{ value: "D", label: "纽扣科", isCorrect: true },
],
},
{
id: "quiz-2",
time: 221, // 3:41
title: "题目2",
prompt:
"黄炎培先生主导设计的课程与现在中职学校强调的理实一体这一理念相通是( )。",
type: "multiple",
options: [
{
value: "A",
label: "上午学机械原理,下午就去车间打铁、造台钳",
isCorrect: true,
},
{
value: "B",
label: "课堂上学记账、推销,课后就去街头卖国货",
isCorrect: true,
},
{
value: "C",
label: "学习四书五经、八股文",
explanation: "黄炎培认为教育不能只谈书本、脱离实际",
},
{
value: "D",
label: '在川沙小学堂采取"半天读书、半天做手工"的学习模式',
isCorrect: true,
},
],
},
{
id: "quiz-3",
time: 370, // 6:10
title: "题目3",
prompt:
'当年黄炎培先生有推广国货的爱国情怀,如今我们的"国货"已经自强自立,请将其中的国产企业/品牌放置到空格处',
type: "drag",
options: [
{ value: "特斯拉", label: "特斯拉" },
{ value: "比亚迪", label: "比亚迪" },
{ value: "华为", label: "华为" },
{ value: "上海电气", label: "上海电气" },
{ value: "光明食品", label: "光明食品" },
{ value: "拼多多", label: "拼多多" },
],
blanks: [
{ id: "blank-1", label: "国产企业1" },
{ id: "blank-2", label: "国产企业2" },
{ id: "blank-3", label: "国产企业3" },
{ id: "blank-4", label: "国产企业4" },
{ id: "blank-5", label: "国产企业5" },
],
},
{
id: "quiz-4",
time: 467, // 7:47
title: "题目4",
prompt:
'当年现在的中职教育一直在"因时求变",许多专业的新增和关联正确的是( )。',
type: "multiple",
options: [
{ value: "A", label: "汽修专业→学新能源汽车检测与维修", isCorrect: true },
{
value: "B",
label: "会计专业→要掌握大数据财务和智能记账",
isCorrect: true,
},
{ value: "C", label: "国货推销→跨境电商、直播带货", isCorrect: true },
{
value: "D",
label: "美术设计与制作→影像与影视技术",
explanation:
"两个专业为不同技能方向的学习,前者为平面绘画等,后者以视频为主",
},
],
},
];
const HuangyanpeiInternalization = () => {
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]: "" }));
// 特斯拉标签无法拖入答案框
const hasInvalidOption = val.some(
(v) => v.optionValue === "特斯拉"
);
if (hasInvalidOption) {
setDragQuizErrors((prev) => ({
...prev,
[item.id]: "特斯拉不是中国国产企业哦~",
}));
return;
}
if (val.length < item.blanks!.length) return;
// 检查是否都是正确答案(排除特斯拉)
const validAnswers = [
"比亚迪",
"华为",
"上海电气",
"光明食品",
"拼多多",
];
const filledAnswers = val.map((v) => v.optionValue);
const isCorrect = filledAnswers.every((answer) =>
validAnswers.includes(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/huangyanpei/huangyanpei_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="guide">
听完黄校长的讲解,相信你对黄炎培先生的职教精神有了更深的理解。现在,就请你把这份感悟写下来,既总结参观的收获,也给自己未来许下一个承诺吧!
</GuideStepsItem>
</GuideSteps>
{/* 信件组件 */}
<LetterToHuangyanpei
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 HuangyanpeiInternalization;