feat: 清理无用的 MacOS 文件和更新组件,提升代码整洁性

- 删除多个无用的 MacOS 生成文件,减少项目冗余。
- 在 layout 组件中添加 ScaledPortal 和相关类型,增强组件功能。
- 更新 puzzleData 中的学校名称,确保信息准确性。
- 调整 internalization 组件中的提示文本,提升用户体验。
- 在多个场景中引入新的选择题和成功提示,增强学习互动性。
This commit is contained in:
zhongyidian committed 2026-02-01 14:37:14 +08:00
1 parent a365964f54
commit 45dd648d21
60 files changed
+554 -59

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"mcp__chrome-devtools__click"
]
}
}
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useScale } from "@/contexts/ScaleContext";
export interface ScaledPortalProps {
/** 是否启用(不启用则直接渲染 children) */
enabled?: boolean;
/** z-index 层级(建议 > ExhibitOverlay 的 9999) */
zIndex?: number;
/** 子元素 */
children: ReactNode;
}
/**
* ScaledPortal
* - 使用 Portal 渲染到 document.body,避免被其他 Portal/遮罩层挡住
* - 使用 useScale 的 offset/scale 做视觉对齐(与 ExhibitOverlay 一致)
*/
const ScaledPortal = ({
enabled = true,
zIndex = 10050,
children,
}: ScaledPortalProps) => {
const [mounted, setMounted] = useState(false);
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale();
useEffect(() => {
setMounted(true);
}, []);
if (!enabled) return <>{children}</>;
if (!mounted) return null;
const scaled = (
<div
style={{
position: "fixed",
inset: 0,
width: `${DESIGN_WIDTH}px`,
height: `${DESIGN_HEIGHT}px`,
left: `${offsetX}px`,
top: `${offsetY}px`,
transform: `scale(${scale})`,
transformOrigin: "top left",
pointerEvents: "auto",
zIndex,
}}
>
{children}
</div>
);
return createPortal(scaled, document.body);
};
export default ScaledPortal;
+2
View File
@@ -1,6 +1,8 @@
export { default as ResidenceLayout } from "./ResidenceLayout"; export { default as ResidenceLayout } from "./ResidenceLayout";
export { default as ClueOverlay } from "../../pages/chenyun/reflection/clue"; export { default as ClueOverlay } from "../../pages/chenyun/reflection/clue";
export type { ClueOverlayProps } from "../../pages/chenyun/reflection/clue"; export type { ClueOverlayProps } from "../../pages/chenyun/reflection/clue";
export { default as ScaledPortal } from "./ScaledPortal";
export type { ScaledPortalProps } from "./ScaledPortal";
export { default as Alert } from "./alert/title-alert"; export { default as Alert } from "./alert/title-alert";
export type { AlertProps } from "./alert/title-alert"; export type { AlertProps } from "./alert/title-alert";
export { default as SuccessAlert } from "./alert/success-alert"; export { default as SuccessAlert } from "./alert/success-alert";
+1 -1
View File
@@ -29,7 +29,7 @@ export const puzzleDataConfig: Record<
}, },
// 碎片2 // 碎片2
2: { 2: {
title: "陈云就读 青浦县乙种商业学校", title: "陈云就读 青浦县立乙种商业学校",
detailImage: "chenyun/details/2", detailImage: "chenyun/details/2",
route: "place2", route: "place2",
}, },
+68 -16
View File
@@ -7,7 +7,6 @@ import {
type VideoRef, type VideoRef,
type DragFillResult, type DragFillResult,
LetterToChenyun, LetterToChenyun,
StudyGuideModal,
EndingOverlay, EndingOverlay,
} from "@/components"; } from "@/components";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
@@ -37,11 +36,15 @@ type QuizAnchor = {
// 星名固定为 chenyun // 星名固定为 chenyun
const starName = "chenyun"; const starName = "chenyun";
// 通用提示文案
const MULTIPLE_DEFAULT_ERROR =
"你的选择还不够全面哦~请你再想想吧!";
// 锚点数据:可根据实际视频时长调整 time // 锚点数据:可根据实际视频时长调整 time
const QUIZ_ANCHORS: QuizAnchor[] = [ const QUIZ_ANCHORS: QuizAnchor[] = [
{ {
id: "quiz-1", id: "quiz-1",
time: Math.ceil(5.52 * 60), //5.52分钟 time: 60 * 5 + 52, //5.52分钟
title: "题目1", title: "题目1",
prompt: "陈云经济思想的核心内容指的是(\xa0\xa0\xa0\xa0)。", prompt: "陈云经济思想的核心内容指的是(\xa0\xa0\xa0\xa0)。",
type: "multiple", type: "multiple",
@@ -54,11 +57,11 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
{ value: "C", label: "正确的处理好计划与市场的关系", isCorrect: true }, { value: "C", label: "正确的处理好计划与市场的关系", isCorrect: true },
{ value: "D", label: "经济建设的目的论", isCorrect: true }, { value: "D", label: "经济建设的目的论", isCorrect: true },
], ],
explanation: "鸟笼比喻是正确的处理好计划与市场的关系的一个发展阶段", explanation: "鸟笼比喻是陈云提出的一个比喻。请你再想想吧!",
}, },
{ {
id: "quiz-2", id: "quiz-2",
time: Math.ceil(10.01 * 60), time: 60 * 10 + 1, //10.01分钟
title: "题目2", title: "题目2",
prompt: "陈云说过以下哪些话能着重体现出他的经济思想?", prompt: "陈云说过以下哪些话能着重体现出他的经济思想?",
type: "multiple", type: "multiple",
@@ -76,6 +79,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
isCorrect: true, isCorrect: true,
}, },
], ],
explanation: "你的选择还不够全面哦~请你再想想吧!",
}, },
{ {
id: "quiz-3", id: "quiz-3",
@@ -95,6 +99,39 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
{ id: "blank-3", label: "____" }, { id: "blank-3", label: "____" },
], ],
}, },
{
id: "quiz-4",
time: 60 * 5 + 21, // 5:21
title: "题目4",
prompt: "以下哪些陈云的生活小事值得我们学习?",
type: "multiple",
options: [
{ value: "A", label: "光盘行动,不浪费粮食", isCorrect: true },
{ value: "B", label: "打了 32 个补丁的棉坎肩他穿了 38 年", isCorrect: true },
{ value: "C", label: "2-3 厘米长的铅笔头仍然留用", isCorrect: true },
{ value: "D", label: "搓肥皂时关闭水龙头,节约用水", isCorrect: true },
],
explanation: MULTIPLE_DEFAULT_ERROR,
},
{
id: "quiz-5",
time: 60 * 16 + 23, // 16:23
title: "题目5",
prompt: "新时代的青年要从陈云身上学习哪些精神品质?",
type: "multiple",
options: [
{ value: "A", label: "谦虚谨慎,实事求是的工作作风", isCorrect: true },
{ value: "B", label: "勤俭朴素,艰苦奋斗的优良作风", isCorrect: true },
{ value: "C", label: "密切联系群众,关心群众生活", isCorrect: true },
{
value: "D",
label: "只唯上的工作作风",
isCorrect: false,
explanation: "\"只唯上\"的工作作风与陈云同志实事求是的精神相悖。请你再想想吧!",
},
],
explanation: MULTIPLE_DEFAULT_ERROR,
},
]; ];
const ChenyunInternalization = () => { const ChenyunInternalization = () => {
@@ -103,7 +140,6 @@ const ChenyunInternalization = () => {
// 获取碎片动画触发方法 // 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation(); const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeSuccess, setActiveSuccess] = useState<string | null>(null); const [activeSuccess, setActiveSuccess] = useState<string | null>(null);
const [successContent, setSuccessContent] = useState<string>("");
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>( const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(
null null
); );
@@ -171,6 +207,7 @@ const ChenyunInternalization = () => {
once: true, once: true,
afterResume: "remove", afterResume: "remove",
render: ({ resume }) => { render: ({ resume }) => {
// 拖拽题:处理选项拖拽与校验
const handleDragChange = (val: DragFillResult[]) => { const handleDragChange = (val: DragFillResult[]) => {
setDragQuizValues((prev) => ({ ...prev, [item.id]: val })); setDragQuizValues((prev) => ({ ...prev, [item.id]: val }));
setDragQuizErrors((prev) => ({ ...prev, [item.id]: "" })); setDragQuizErrors((prev) => ({ ...prev, [item.id]: "" }));
@@ -187,13 +224,12 @@ const ChenyunInternalization = () => {
if (isCorrect) { if (isCorrect) {
setActiveSuccess(item.id); setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
setResumeCallback(() => resume); setResumeCallback(() => resume);
} else { } else {
setDragQuizErrors((prev) => ({ setDragQuizErrors((prev) => ({
...prev, ...prev,
[item.id]: [item.id]:
"再想一想,不唯上才是陈云的工作作风,不是只唯上!", "这不是陈云的工作作风。请你再想想吧!",
})); }));
} }
}; };
@@ -217,10 +253,9 @@ const ChenyunInternalization = () => {
<SuccessAlert <SuccessAlert
visible={activeSuccess === item.id} visible={activeSuccess === item.id}
title="恭喜你, 选择正确!" title="恭喜你, 选择正确!"
content={successContent} content="回答正确,请继续探索。"
onContinue={() => { onContinue={() => {
setActiveSuccess(null); setActiveSuccess(null);
setSuccessContent("");
triggerPuzzleAnimation(16); triggerPuzzleAnimation(16);
setDragQuizValues((prev) => { setDragQuizValues((prev) => {
const next = { ...prev }; const next = { ...prev };
@@ -261,13 +296,32 @@ const ChenyunInternalization = () => {
label: `${opt.value}. ${opt.label}`, label: `${opt.value}. ${opt.label}`,
}))} }))}
correctAnswer={correctAnswers} correctAnswer={correctAnswers}
errorMessage={ errorMessage={(selectedValues) => {
item.explanation || "再思考一下,注意选择所有正确答案。" // 题目1:选到“鸟笼比喻(B)”给专属提示;否则未选全给“还不够全面”提示
if (item.id === "quiz-1") {
if (selectedValues.includes("B")) {
return (
item.explanation ||
"鸟笼比喻是陈云提出的一个比喻。请你再想想吧!"
);
} }
return MULTIPLE_DEFAULT_ERROR;
}
// 题目5:选到“D”给专属提示;否则未选全给“还不够全面”提示
if (item.id === "quiz-5") {
if (selectedValues.includes("D")) {
const pickedD = item.options.find(
(o) => o.value === "D"
);
return pickedD?.explanation || MULTIPLE_DEFAULT_ERROR;
}
return MULTIPLE_DEFAULT_ERROR;
}
return item.explanation || MULTIPLE_DEFAULT_ERROR;
}}
onSubmit={(values: string[], isCorrect: boolean) => { onSubmit={(values: string[], isCorrect: boolean) => {
if (isCorrect) { if (isCorrect) {
setActiveSuccess(item.id); setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
if (item.id === "quiz-1") triggerPuzzleAnimation(15); if (item.id === "quiz-1") triggerPuzzleAnimation(15);
setResumeCallback(() => resume); setResumeCallback(() => resume);
} }
@@ -275,11 +329,10 @@ const ChenyunInternalization = () => {
/> />
<SuccessAlert <SuccessAlert
visible={activeSuccess === item.id} visible={activeSuccess === item.id}
title="恭喜你, 选择正确!" // title="恭喜你, 选择正确!"
content={successContent} // content="回答正确,请继续探索。"
onContinue={() => { onContinue={() => {
setActiveSuccess(null); setActiveSuccess(null);
setSuccessContent("");
const cb = resumeCallback; const cb = resumeCallback;
setResumeCallback(null); setResumeCallback(null);
cb?.(); cb?.();
@@ -302,7 +355,6 @@ const ChenyunInternalization = () => {
[ [
activeSuccess, activeSuccess,
resumeCallback, resumeCallback,
successContent,
dragQuizValues, dragQuizValues,
dragQuizErrors, dragQuizErrors,
triggerPuzzleAnimation, triggerPuzzleAnimation,
+4 -4
View File
@@ -171,13 +171,13 @@ const ChenyunPlace2 = () => {
speaker="student" speaker="student"
audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_05.mp3")} audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_05.mp3")}
> >
那陈云是怎么有机会到图中的青浦县乙种商业学校读书的呀? 那陈云是怎么有机会到图中的青浦县立乙种商业学校读书的呀?
</GuideStepsItem> </GuideStepsItem>
<GuideStepsItem <GuideStepsItem
speaker="guide" speaker="guide"
audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_06.mp3")} audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_06.mp3")}
> >
因为家贫,初小读完的陈云便辍学在家。后在舅母亲戚的资助下,陈云才到青浦县乙种商业学校学习珠算和记账的,他在很短时间里就基本掌握了珠算,还初步学会写账,这为陈云日后从事经济工作打下了基础。 因为家贫,初小读完的陈云便辍学在家。后在舅母亲戚的资助下,陈云才到青浦县立乙种商业学校学习珠算和记账的,他在很短时间里就基本掌握了珠算,还初步学会写账,这为陈云日后从事经济工作打下了基础。
</GuideStepsItem> </GuideStepsItem>
<GuideStepsItem speaker="guide"> <GuideStepsItem speaker="guide">
陈云在党内以“铁算盘”著称,他在经济工作中能熟练使用算盘,就得益于少年时在青浦县立乙种商业学校的学习。 陈云在党内以“铁算盘”著称,他在经济工作中能熟练使用算盘,就得益于少年时在青浦县立乙种商业学校的学习。
@@ -198,13 +198,13 @@ const ChenyunPlace2 = () => {
speaker="student" speaker="student"
audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_07.mp3")} audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_07.mp3")}
> >
这里有一幅陈云提写的颜安小学,请问陈云在这所学校读过书吗? 这里有一幅陈云题写的颜安小学,请问陈云在这所学校读过书吗?
</GuideStepsItem> </GuideStepsItem>
<GuideStepsItem <GuideStepsItem
speaker="guide" speaker="guide"
audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_08.mp3")} audioSrc={getPublicPath("audio/chenyun/mp3_chenyun_08.mp3")}
> >
是的,陈云在青浦县乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。 是的,陈云在青浦县立乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。
</GuideStepsItem> </GuideStepsItem>
<GuideStepsItem speaker="guide"> <GuideStepsItem speaker="guide">
颜安小学实行的新式教育开阔了陈云的视野,五四运动后,颜安小学师生积极投入反帝爱国运动,为陈云日后加入中国共产党埋下了思想的种子。 颜安小学实行的新式教育开阔了陈云的视野,五四运动后,颜安小学师生积极投入反帝爱国运动,为陈云日后加入中国共产党埋下了思想的种子。
+72 -2
View File
@@ -6,12 +6,15 @@ import {
Cursor, Cursor,
GuideSteps, GuideSteps,
GuideStepsItem, GuideStepsItem,
MultipleChoiceQuizPanel,
} from "@/components"; } from "@/components";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore"; import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController"; import { useCursorController } from "@/hooks/useCursorController";
import SuccessAlert from "@/components/layout/alert/success-alert";
import ScaledPortal from "@/components/layout/ScaledPortal";
// starName 固定为 chenyun // starName 固定为 chenyun
const starName = "chenyun"; const starName = "chenyun";
@@ -34,8 +37,7 @@ const ChenyunPlace4 = () => {
// 使用持久化的访问状态 // 使用持久化的访问状态
const place = "place4"; const place = "place4";
const { isExhibitVisited, markExhibitVisited, getVisitedExhibits } = const { markExhibitVisited, getVisitedExhibits } = useChenyunStore();
useChenyunStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place); const visitedExhibits = getVisitedExhibits(place);
@@ -52,6 +54,10 @@ const ChenyunPlace4 = () => {
const [guideVisible2, setGuideVisible2] = useState(false); const [guideVisible2, setGuideVisible2] = useState(false);
const [guideStep2, setGuideStep2] = useState(0); const [guideStep2, setGuideStep2] = useState(0);
// 展品2关闭后的题目状态(多选)
const [quiz2Visible, setQuiz2Visible] = useState(false);
const [successAlert2Visible, setSuccessAlert2Visible] = useState(false);
const handleOpenExhibit = (index: 1 | 2) => { const handleOpenExhibit = (index: 1 | 2) => {
if (index === 1) { if (index === 1) {
// 展品1:点击时同时打开展品和引导对话 // 展品1:点击时同时打开展品和引导对话
@@ -148,6 +154,14 @@ const ChenyunPlace4 = () => {
} }
}} }}
onClose={() => { onClose={() => {
// 展品2:关闭后先进入答题,答对后再标记已读
if (activeExhibitId === 2) {
setActiveExhibitId(null);
setQuiz2Visible(true);
return;
}
// 其他展品:关闭即标记已读
if (activeExhibitId !== null) { if (activeExhibitId !== null) {
markExhibitVisited(place, activeExhibitId); markExhibitVisited(place, activeExhibitId);
} }
@@ -155,6 +169,62 @@ const ChenyunPlace4 = () => {
}} }}
/> />
{/* 展品2的题目 + 成功提示(使用 ScaledPortal 保证层级与缩放) */}
<ScaledPortal enabled={quiz2Visible || successAlert2Visible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quiz2Visible}
title="【多选】中央苏区时期,陈云开展经济工作的主要做法包括:"
options={[
{
value: "A",
label:
"A.从实际出发,实事求是地制定符合企业发展需求的劳动政策",
},
{
value: "B",
label:
"B.提议建立合作社,吸收失业工人并增强企业管理的民主性",
},
{
value: "C",
label: "C.照搬“左”倾经济政策,严格推行统一的劳动标准",
},
{
value: "D",
label: "D.审查企业、作坊的实际承受能力,避免政策脱离实际",
},
]}
correctAnswer={["A", "B", "D"]}
minSelections={1}
maxSelections={4}
errorMessage={(selectedValues) => {
// 选了 C:给出截图中的专属提示
if (selectedValues.includes("C")) {
return "陈云反对“左”倾错误。请你再想想吧!";
}
// 没选 C 但仍不正确:通常是“选不全”
return "你的选择还不够全面哦~请你再想想吧!";
}}
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuiz2Visible(false);
setSuccessAlert2Visible(true);
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlert2Visible}
onContinue={() => {
setSuccessAlert2Visible(false);
markExhibitVisited(place, 2);
}}
/>
</>
</ScaledPortal>
{/* 展品1的引导对话 */} {/* 展品1的引导对话 */}
<GuideSteps <GuideSteps
visible={guideVisible} visible={guideVisible}
+60 -9
View File
@@ -7,12 +7,15 @@ import {
Video, Video,
GuideSteps, GuideSteps,
GuideStepsItem, GuideStepsItem,
MultipleChoiceQuizPanel,
} from "@/components"; } from "@/components";
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore"; import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController"; import { useCursorController } from "@/hooks/useCursorController";
import SuccessAlert from "@/components/layout/alert/success-alert";
import ScaledPortal from "@/components/layout/ScaledPortal";
// starName 固定为 chenyun // starName 固定为 chenyun
const starName = "chenyun"; const starName = "chenyun";
@@ -23,12 +26,12 @@ const exhibitSrcMap: Record<1 | 2 | 3, string> = {
2: "chenyun/place5/场景5展品2", 2: "chenyun/place5/场景5展品2",
3: "chenyun/place5/场景5展品3", 3: "chenyun/place5/场景5展品3",
}; };
// 展品描述映射 // 展品描述映射(目前未使用,后续如需在展品弹窗中显示可恢复)
const exhibitDescriptionMap: Record<1 | 2 | 3, string> = { // const exhibitDescriptionMap: Record<1 | 2 | 3, string> = {
1: "", // 1: "",
2: "延安时期因为国民党的封锁,陕甘宁边区经济困难。陈云转战陕甘宁边区主持财经工作,他遇到了很多经济难题。1944年新春伊始,陈云开始担任西北财经办事处副主任,工作转到财经方面,这是陈云以主要精力从事领导经济工作的开始。当时,陕甘宁边区在国民党严密封锁下,财经状况严峻,边区物价猛涨,老百姓日常生活必需的布匹和粮食十分缺乏。国民党顽固派封锁边区,企图“不让一粒粮、一尺布进入边区”,同时千方百计阻止边区食盐出口。", // 2: "延安时期因为国民党的封锁,陕甘宁边区经济困难。陈云转战陕甘宁边区主持财经工作,他遇到了很多经济难题。1944年新春伊始,陈云开始担任西北财经办事处副主任,工作转到财经方面,这是陈云以主要精力从事领导经济工作的开始。当时,陕甘宁边区在国民党严密封锁下,财经状况严峻,边区物价猛涨,老百姓日常生活必需的布匹和粮食十分缺乏。国民党顽固派封锁边区,企图“不让一粒粮、一尺布进入边区”,同时千方百计阻止边区食盐出口。",
3: "", // 3: "",
}; // };
const ChenyunPlace5 = () => { const ChenyunPlace5 = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { moduleName } = useParams<{ const { moduleName } = useParams<{
@@ -44,8 +47,7 @@ const ChenyunPlace5 = () => {
// 使用持久化的访问状态 // 使用持久化的访问状态
const place = "place5"; const place = "place5";
const { isExhibitVisited, markExhibitVisited, getVisitedExhibits } = const { markExhibitVisited, getVisitedExhibits } = useChenyunStore();
useChenyunStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place); const visitedExhibits = getVisitedExhibits(place);
@@ -56,6 +58,8 @@ const ChenyunPlace5 = () => {
visited, visited,
}); });
const [videoVisible3, setVideoVisible3] = useState(false); const [videoVisible3, setVideoVisible3] = useState(false);
const [quiz3Visible, setQuiz3Visible] = useState(false);
const [successAlert3Visible, setSuccessAlert3Visible] = useState(false);
// 展品1引导对话 // 展品1引导对话
const [guideVisible, setGuideVisible] = useState(false); const [guideVisible, setGuideVisible] = useState(false);
@@ -182,11 +186,58 @@ const ChenyunPlace5 = () => {
showSkipButton showSkipButton
onEnded={() => { onEnded={() => {
setVideoVisible3(false); setVideoVisible3(false);
setQuiz3Visible(true);
}}
/>
)}
{/* 展品3视频后的题目 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quiz3Visible || successAlert3Visible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quiz3Visible}
title="【多选】陈云主持陕甘宁边区财经工作期间,提出的解决当时经济困难的应对之策包括:"
options={[
{ value: "A", label: "A.实行贸易统一" },
{
value: "B",
label: "B.事先准确估计情况,预测市场商情,以争取主动",
},
{
value: "C",
label:
"C.不断总结封锁状态下对外贸易的经验,摸索实现等价交换的途径",
},
{
value: "D",
label:
"D.贸易公司对边区内部贸易也要采取市场上做生意的办法",
},
{ value: "E", label: "E.做贸易工作的干部要精通业务" },
]}
correctAnswer={["A", "B", "C", "D", "E"]}
minSelections={1}
maxSelections={5}
errorMessage="你的选择还不够全面哦~请你再想想吧!"
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuiz3Visible(false);
setSuccessAlert3Visible(true);
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlert3Visible}
onContinue={() => {
setSuccessAlert3Visible(false);
markExhibitVisited(place, 3); markExhibitVisited(place, 3);
triggerPuzzleAnimation(8); triggerPuzzleAnimation(8);
}} }}
/> />
)} </>
</ScaledPortal>
{/* 遮罩层 */} {/* 遮罩层 */}
<ExhibitOverlay <ExhibitOverlay
+8 -8
View File
@@ -34,7 +34,7 @@ const QUIZ1_OPTIONS: SingleChoiceOption[] = [
}, },
{ {
value: "B", value: "B",
label: "B. 有利于稳定人民的发行和地位", label: "B.有利于稳定人民币的发行和地位",
}, },
{ {
value: "C", value: "C",
@@ -44,8 +44,8 @@ const QUIZ1_OPTIONS: SingleChoiceOption[] = [
// 题目1各选项对应的错误提示(原来写在选项括号中的内容) // 题目1各选项对应的错误提示(原来写在选项括号中的内容)
const QUIZ1_ERROR_HINTS: Record<string, string> = { const QUIZ1_ERROR_HINTS: Record<string, string> = {
A: "淮海战役于1949年1月胜利结束,发生在这次“银元之战”前,不存在因果关系。", A: "淮海战役于1949年1月胜利结束,发生在这次“银元之战”前,不存在因果关系。请你再想想吧!",
C: "1949年6月新中国政权尚未建立。", C: "1949年6月新中国政权尚未建立。请你再想想吧",
}; };
const ChenyunPlace7 = () => { const ChenyunPlace7 = () => {
@@ -169,7 +169,7 @@ const ChenyunPlace7 = () => {
// 显示成功提示 // 显示成功提示
setSuccessAlertVisible(true); setSuccessAlertVisible(true);
} else { } else {
setQuiz2Error("再想一想,石油当时不是主要调控对象~"); setQuiz2Error("再想一想,石油当时不是主要调控对象。请你再想想吧!");
} }
}; };
@@ -285,7 +285,7 @@ const ChenyunPlace7 = () => {
<SingleChoiceQuizPanel <SingleChoiceQuizPanel
visible={quizVisible} visible={quizVisible}
zIndex={10002} zIndex={10002}
title={`【单选】1949年6月,中共中央指示打击银元投机活动,金条、银元一律由人民银行挂牌收兑,铁路交通事业,税收一律收人民币。同时,上海军管会查封了证券大楼。这次“银元之战”(\xa0\xa0\xa0\xa0)。`} title={`【单选】1949年6月,中共中央指示打击银元投机活动,金条、银元一律由人民银行挂牌收兑,铁路交通事业,税收一律收人民币。同时,上海军管会查封了证券大楼。这次“银元之战”:`}
options={QUIZ1_OPTIONS} options={QUIZ1_OPTIONS}
correctAnswer={"B"} correctAnswer={"B"}
errorMessage={quizError} errorMessage={quizError}
@@ -296,7 +296,7 @@ const ChenyunPlace7 = () => {
// 显示成功提示 // 显示成功提示
setSuccessAlertVisible(true); setSuccessAlertVisible(true);
setSuccessAlertContent( setSuccessAlertContent(
"“银元之战”属于当时人民政府整顿金融的重要举措,黄金、银元投机不利于人民币币值稳定和流通,因此“银元之战”打击贵金属投机活动,有利于稳定人民币的发行和地位" "“银元之战”属于当时人民政府整顿金融的重要举措,黄金、银元投机不利于人民币币值稳定和流通,因此“银元之战”打击贵金属投机活动,有利于稳定人民币的发行和地位。"
); );
}} }}
onWrongSelect={(val) => { onWrongSelect={(val) => {
@@ -356,8 +356,8 @@ const ChenyunPlace7 = () => {
<DragQuizPanel <DragQuizPanel
visible={quiz2Visible} visible={quiz2Visible}
zIndex={10012} zIndex={10012}
questionPrefix='【拖拽】新中国成立后,为了稳定物价,中央政府组织了"两白一黑"的集中调运,"两白一黑"指的是(' questionPrefix='【拖拽】新中国成立后,为了稳定物价,中央政府组织了"两白一黑"的集中调运,"两白一黑"指的是'
questionSuffix=")。" questionSuffix="。"
options={QUIZ2_OPTIONS} options={QUIZ2_OPTIONS}
blanks={QUIZ2_BLANKS} blanks={QUIZ2_BLANKS}
value={quiz2Value} value={quiz2Value}
+101 -4
View File
@@ -6,12 +6,16 @@ import {
Cursor, Cursor,
GuideSteps, GuideSteps,
GuideStepsItem, GuideStepsItem,
MultipleChoiceQuizPanel,
SingleChoiceQuizPanel,
} from "@/components"; } from "@/components";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore"; import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController"; import { useCursorController } from "@/hooks/useCursorController";
import SuccessAlert from "@/components/layout/alert/success-alert";
import ScaledPortal from "@/components/layout/ScaledPortal";
// starName 固定为 chenyun // starName 固定为 chenyun
const starName = "chenyun"; const starName = "chenyun";
@@ -34,8 +38,7 @@ const ChenyunPlace8 = () => {
// 使用持久化的访问状态 // 使用持久化的访问状态
const place = "place8"; const place = "place8";
const { isExhibitVisited, markExhibitVisited, getVisitedExhibits } = const { markExhibitVisited, getVisitedExhibits } = useChenyunStore();
useChenyunStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place); const visitedExhibits = getVisitedExhibits(place);
@@ -50,12 +53,21 @@ const ChenyunPlace8 = () => {
const [guideStep, setGuideStep] = useState(0); const [guideStep, setGuideStep] = useState(0);
const [guideVisible2, setGuideVisible2] = useState(false); const [guideVisible2, setGuideVisible2] = useState(false);
const [guideStep2, setGuideStep2] = useState(0); const [guideStep2, setGuideStep2] = useState(0);
const [quizVisible, setQuizVisible] = useState(false);
const [successAlertVisible, setSuccessAlertVisible] = useState(false);
const [quizPassed, setQuizPassed] = useState(false);
// 关闭对话后出现的“统购统销”单选题
const [closeQuizVisible, setCloseQuizVisible] = useState(false);
const [closeSuccessAlertVisible, setCloseSuccessAlertVisible] =
useState(false);
const [closeQuizPassed, setCloseQuizPassed] = useState(false);
const handleOpenExhibit = (index: 1 | 2) => { const handleOpenExhibit = (index: 1 | 2) => {
setActiveExhibitId(index); setActiveExhibitId(index);
if (index === 1) { if (index === 1) {
setGuideVisible2(true); setGuideVisible2(true);
setGuideStep2(0); setGuideStep2(0);
setQuizPassed(false);
} }
// 如果是展品2,显示对话引导 // 如果是展品2,显示对话引导
if (index === 2) { if (index === 2) {
@@ -191,9 +203,23 @@ const ChenyunPlace8 = () => {
<GuideSteps <GuideSteps
visible={guideVisible2} visible={guideVisible2}
onVisibleChange={(visible) => setGuideVisible2(visible)} onVisibleChange={(visible) => {
setGuideVisible2(visible);
// 关闭对话后,如果还未完成该题,则弹出题目
if (!visible && !closeQuizPassed) {
setCloseQuizVisible(true);
}
}}
currentStep={guideStep2} currentStep={guideStep2}
onStepChange={(step) => setGuideStep2(step)} onStepChange={(step) => {
// 在第 3 句(guideStep2 === 2)结束后,进入下一句前先弹题
if (step === 3 && !quizPassed) {
setGuideStep2(2);
setQuizVisible(true);
return;
}
setGuideStep2(step);
}}
showMask={false} showMask={false}
highlightSelectors={[]} highlightSelectors={[]}
zIndex={10001} zIndex={10001}
@@ -217,6 +243,77 @@ const ChenyunPlace8 = () => {
第一个五年计划纲要草案的重点是优先发展重工业,特别是156项重点工程。当时陈云重点抓重工业建设和粮食保障,通过“统购统销”稳定粮价,为重点工程项目筑牢根基,亲手铺就了我国工业化的起步之路。 第一个五年计划纲要草案的重点是优先发展重工业,特别是156项重点工程。当时陈云重点抓重工业建设和粮食保障,通过“统购统销”稳定粮价,为重点工程项目筑牢根基,亲手铺就了我国工业化的起步之路。
</GuideStepsItem> </GuideStepsItem>
</GuideSteps> </GuideSteps>
{/* “这三个问题…”结束后的题目 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quizVisible}
title="【多选】陈云主持编制第一个五年计划纲要时提出计划编制工作中要注意的问题包括:"
options={[
{ value: "A", label: "A.工业投资金额" },
{ value: "B", label: "B.市场供应和人民生活" },
{ value: "C", label: "C.农业发展速度" },
]}
correctAnswer={["A", "B", "C"]}
minSelections={1}
maxSelections={3}
errorMessage="你的选择还不够全面哦~请你再想想吧!"
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuizVisible(false);
setSuccessAlertVisible(true);
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlertVisible}
onContinue={() => {
setSuccessAlertVisible(false);
setQuizPassed(true);
setGuideVisible2(true);
setGuideStep2(3);
}}
/>
</>
</ScaledPortal>
{/* 关闭对话后出现的题目(单选) + 成功提示(使用 ScaledPortal) */}
<ScaledPortal
enabled={closeQuizVisible || closeSuccessAlertVisible}
zIndex={10050}
>
<>
<SingleChoiceQuizPanel
visible={closeQuizVisible}
title="【单选】第一个五年计划纲要草案的重点是优先发展重工业,特别是156项重点工程。当时陈云重点抓重工业建设和粮食保障,通过“____”稳定粮价。"
options={[
{ value: "A", label: "A.优先分配" },
{ value: "B", label: "B.统购统销" },
{ value: "C", label: "C.依靠进口" },
]}
correctAnswer="B"
errorMessage="请你再想想吧!"
onWrongSelect={() => "这不是当时陈云稳定粮价的方法。请你再想想吧!"}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return;
setCloseQuizVisible(false);
setCloseSuccessAlertVisible(true);
}}
zIndex={10000}
/>
<SuccessAlert
visible={closeSuccessAlertVisible}
onContinue={() => {
setCloseSuccessAlertVisible(false);
// setCloseQuizPassed(true);
}}
/>
</>
</ScaledPortal>
</div> </div>
); );
}; };
+172 -15
View File
@@ -78,8 +78,114 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
}, },
{ {
id: "quiz-2", id: "quiz-2",
time: 60 * 5 + 40, time: 60 * 2, // 2分钟
title: "题目2", title: "题目2",
prompt:
"【多选】新时代青年传承“振兴中华”的精神,能够具体落实为( )。",
type: "multiple",
options: [
{
value: "A",
label: "专注学习,提升自身技能与素养",
isCorrect: true,
explanation:
"恭喜你,选择正确!青年通过专注学习、提升自身能力,用精益求精的精神服务于国家建设,才能成为“振兴中华”的践行者。",
},
{
value: "B",
label: "秉持精益求精的精神,在专业领域深耕",
isCorrect: true,
explanation:
"恭喜你,选择正确!青年在自己的专业领域深耕细作,才能以扎实的能力为国家发展贡献力量。",
},
{
value: "C",
label: "遇到困难不退缩,不因失败而灰心",
isCorrect: true,
explanation:
"恭喜你,选择正确!不因失败而灰心,也从不因困难而退缩,这种坚韧的态度是践行“振兴中华”过程中不可或缺的品质。",
},
{
value: "D",
label: "放弃个人职业规划,直接投身革命事业",
isCorrect: false,
explanation:
"振兴中华要把个人理想融入国家命运,在自己的领域里成为“振兴中华”的践行者。请你再想想吧!",
},
],
},
{
id: "quiz-3",
time: 60 * 4 + 14, // 4分14秒
title: "题目3",
prompt: "【多选】新时代青年涵养“天下为公”的情怀能够具体落实为:",
type: "multiple",
options: [
{
value: "A",
label: "做一名志愿者走进养老院服务老人",
isCorrect: true,
explanation:
"恭喜你,选择正确!对他人的关爱与奉献是传承“天下为公”情怀的具体行动哦!",
},
{
value: "B",
label: "只专注于个人学业与职业发展,不参与公共事务",
isCorrect: false,
explanation:
"这与“天下为公”的情怀完全相悖。请你再想想吧!",
},
{
value: "C",
label: "用自己所学的技能为企业、社区提供服务",
isCorrect: true,
explanation:
"恭喜你,选择正确!用自己所学的技能为企业、社区提供服务是青年将个人能力转化为公共服务、践行“天下为公”的重要方式。",
},
{
value: "D",
label: "在日常生活中关心身边有需要的人",
isCorrect: true,
explanation:
"恭喜你,选择正确!“天下为公”的情怀不需要宏大叙事,从身边小事做起就是具体落实。",
},
],
explanation: "请再仔细分析一下题目,做出更全面的选择吧!",
},
{
id: "quiz-4",
time: 60 * 5 + 40,
title: "题目4",
prompt: "【多选】新时代青年学习“与时俱进”的智慧能够具体落实为:",
type: "multiple",
options: [
{
value: "A",
label: "主动学习新知识,关注科技等前沿领域",
isCorrect: true,
explanation:
"恭喜你,选择正确!主动学习新知识、适应新趋势正是青年与时俱进、紧跟时代步伐的具体表现。",
},
{
value: "B",
label: "在工作中突破传统思维,用创新方法解决问题",
isCorrect: true,
explanation:
"恭喜你,选择正确!在工作中突破传统思维,用创新解决问题正是与时俱进精神在职业场景中的落地。",
},
{
value: "C",
label: "固执己见,拒绝接受新事物和新变化",
isCorrect: false,
explanation: "这与“与时俱进”的智慧完全相悖。请你再想想吧!",
},
],
explanation: "请再仔细分析一下题目,做出更全面的选择吧!",
},
{
id: "quiz-5",
time: 60 * 5 + 40,
title: "题目5",
prompt: "【多选】新时代的青年要从孙中山先生身上学习哪些精神?", prompt: "【多选】新时代的青年要从孙中山先生身上学习哪些精神?",
type: "multiple", type: "multiple",
options: [ options: [
@@ -106,7 +212,6 @@ const ChenyunInternalization = () => {
const { triggerPuzzleAnimation } = usePuzzleAnimation(); const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeSuccess, setActiveSuccess] = useState<string | null>(null); const [activeSuccess, setActiveSuccess] = useState<string | null>(null);
const [successContent, setSuccessContent] = useState<string>("");
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>( const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(
null null
); );
@@ -137,10 +242,11 @@ const ChenyunInternalization = () => {
// 组件卸载时清理背景音乐 // 组件卸载时清理背景音乐
useEffect(() => { useEffect(() => {
const audioEl = audioRef.current;
return () => { return () => {
if (audioRef.current) { if (audioEl) {
audioRef.current.pause(); audioEl.pause();
audioRef.current.currentTime = 0; audioEl.currentTime = 0;
} }
}; };
}, []); }, []);
@@ -195,7 +301,6 @@ const ChenyunInternalization = () => {
if (isCorrect) { if (isCorrect) {
setActiveSuccess(item.id); setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
setResumeCallback(() => resume); setResumeCallback(() => resume);
} else { } else {
setDragQuizErrors((prev) => ({ setDragQuizErrors((prev) => ({
@@ -214,7 +319,6 @@ const ChenyunInternalization = () => {
if (isCorrect) { if (isCorrect) {
setActiveSuccess(item.id); setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。");
setResumeCallback(() => resume); setResumeCallback(() => resume);
} else { } else {
setDragQuizErrors((prev) => ({ setDragQuizErrors((prev) => ({
@@ -246,10 +350,9 @@ const ChenyunInternalization = () => {
<SuccessAlert <SuccessAlert
visible={activeSuccess === item.id} visible={activeSuccess === item.id}
title="恭喜你, 选择正确!" title="恭喜你, 选择正确!"
content={successContent} content="回答正确,请继续探索。"
onContinue={() => { onContinue={() => {
setActiveSuccess(null); setActiveSuccess(null);
setSuccessContent("");
setDragQuizValues((prev) => { setDragQuizValues((prev) => {
const next = { ...prev }; const next = { ...prev };
delete next[item.id]; delete next[item.id];
@@ -291,23 +394,78 @@ const ChenyunInternalization = () => {
}))} }))}
correctAnswer={correctAnswers} correctAnswer={correctAnswers}
errorMessage={ errorMessage={
item.explanation || "再想一想,确保选择了所有正确答案~" item.id === "quiz-2"
? (selectedValues: string[]) => {
const picked = new Set(selectedValues);
const pickedD = item.options.find(
(o) => o.value === "D"
);
if (picked.has("D")) return pickedD?.explanation || "";
return "请再仔细分析一下题目,做出更全面的选择吧!";
}
: item.id === "quiz-3"
? (selectedValues: string[]) => {
const picked = new Set(selectedValues);
const pickedB = item.options.find(
(o) => o.value === "B"
);
if (picked.has("B")) return pickedB?.explanation || "";
return (
item.explanation ||
"请再仔细分析一下题目,做出更全面的选择吧!"
);
}
: item.id === "quiz-4"
? (selectedValues: string[]) => {
const picked = new Set(selectedValues);
const pickedC = item.options.find(
(o) => o.value === "C"
);
if (picked.has("C")) return pickedC?.explanation || "";
return (
item.explanation ||
"请再仔细分析一下题目,做出更全面的选择吧!"
);
}
: item.id === "quiz-5"
? (selectedValues: string[]) => {
const picked = new Set(selectedValues);
const pickedC = item.options.find(
(o) => o.value === "C"
);
if (picked.has("C")) return pickedC?.explanation || "";
return (
item.explanation ||
"请再仔细分析一下题目,做出更全面的选择吧!"
);
}
: item.explanation ||
"再想一想,确保选择了所有正确答案~"
} }
onSubmit={(values: string[], isCorrect: boolean) => { onSubmit={(values: string[], isCorrect: boolean) => {
if (isCorrect) { if (isCorrect) {
setActiveSuccess(item.id); setActiveSuccess(item.id);
setSuccessContent("回答正确,请继续探索。"); // if (item.id === "quiz-2") {
// const picked = new Set(values);
// const content = item.options
// .filter((o) => picked.has(o.value))
// .map((o) => o.explanation)
// .filter(Boolean)
// .join("\n");
// setSuccessContent(content || "回答正确,请继续探索。");
// } else {
// setSuccessContent("回答正确,请继续探索。");
// }
setResumeCallback(() => resume); setResumeCallback(() => resume);
} }
}} }}
/> />
<SuccessAlert <SuccessAlert
visible={activeSuccess === item.id} visible={activeSuccess === item.id}
title="恭喜你, 选择正确!" // title="恭喜你, 选择正确!"
content={successContent} // content="回答正确,请继续探索。"
onContinue={() => { onContinue={() => {
setActiveSuccess(null); setActiveSuccess(null);
setSuccessContent("");
const cb = resumeCallback; const cb = resumeCallback;
setResumeCallback(null); setResumeCallback(null);
triggerPuzzleAnimation(16); triggerPuzzleAnimation(16);
@@ -331,7 +489,6 @@ const ChenyunInternalization = () => {
[ [
activeSuccess, activeSuccess,
resumeCallback, resumeCallback,
successContent,
dragQuizValues, dragQuizValues,
dragQuizErrors, dragQuizErrors,
triggerPuzzleAnimation, triggerPuzzleAnimation,