feat: 添加全局键盘快捷键和更新故居数据,提升用户体验

- 在 main.tsx 中添加全局键盘快捷键功能,允许用户通过 Ctrl+Shift+R 重置所有缓存。
- 更新多个故居的拼图数据,增加黄炎培和鲁迅的相关信息,丰富学习内容。
- 调整 ResidenceLayout 组件以支持新的故居,确保数据一致性。
- 清理不再使用的拼图布局配置文件,提升代码整洁性。
This commit is contained in:
zhongyidian committed 2026-01-28 15:08:28 +08:00
1 parent 8ba36a4a1a
commit a365964f54
155 files changed
+1387 -348

No files matched your search

+32 -1
View File
@@ -14,6 +14,7 @@ import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { SuccessAlert, DragQuizPanel } from "@/components/layout";
import { MultipleChoiceQuizPanel } from "@/components/layout/quiz";
import { GuideSteps, GuideStepsItem } from "@/components";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
type QuizOption = {
value: string;
@@ -138,6 +139,8 @@ const HuangyanpeiInternalization = () => {
const navigate = useNavigate();
const { moduleName } = useParams<{ moduleName: string }>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [narrationVisible, setNarrationVisible] = useState(false);
const [activeSuccess, setActiveSuccess] = useState<string | null>(null);
const [successContent, setSuccessContent] = useState<string>("");
@@ -154,8 +157,10 @@ const HuangyanpeiInternalization = () => {
const [showVideo, setShowVideo] = useState(false);
const [postGuideVisible, setPostGuideVisible] = useState(false);
const [showLetter, setShowLetter] = useState(false);
const [showLetterVideo, setShowLetterVideo] = useState(false);
const [showStudyGuide, setShowStudyGuide] = useState(false);
const videoRef = useRef<VideoRef>(null);
const letterVideoRef = useRef<VideoRef>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
@@ -180,6 +185,15 @@ const HuangyanpeiInternalization = () => {
}
};
// 信件视频加载完成后自动播放
const handleLetterVideoCanPlay = () => {
if (showLetterVideo && letterVideoRef.current) {
letterVideoRef.current.play().catch((error) => {
console.warn("信件视频播放失败:", error);
});
}
};
const anchors: VideoAnchor[] = useMemo(
() =>
QUIZ_ANCHORS.map((item) => {
@@ -275,6 +289,7 @@ const HuangyanpeiInternalization = () => {
const cb = resumeCallback;
setResumeCallback(null);
cb?.();
triggerPuzzleAnimation(16);
}}
/>
</>
@@ -343,6 +358,7 @@ const HuangyanpeiInternalization = () => {
successContent,
dragQuizValues,
dragQuizErrors,
triggerPuzzleAnimation,
]
);
@@ -458,10 +474,25 @@ const HuangyanpeiInternalization = () => {
onSubmit={(values: string[]) => {
console.log("提交信件", values);
setShowLetter(false);
setShowStudyGuide(true);
setShowLetterVideo(true);
}}
/>
{/* 信件关闭后的视频 */}
{showLetterVideo && (
<Video
ref={letterVideoRef}
src={getPublicPath("video/letter/neihua_jixin.mp4")}
className="h-full w-full"
playsInline
onCanPlay={handleLetterVideoCanPlay}
onEnded={() => {
setShowLetterVideo(false);
setShowStudyGuide(true);
}}
/>
)}
<EndingOverlay
visible={showStudyGuide}
text="恭喜你已经顺利完成了黄炎培故居的学习,请继续前往下一个故居吧!"
+18 -7
View File
@@ -10,6 +10,8 @@ import { useNavigate, useParams } from "react-router-dom";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useState } from "react";
import Cursor from "@/components/cursor";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
// starName 固定为 huangyanpei
const starName = "huangyanpei";
@@ -25,6 +27,8 @@ const HuangyanpeiPlace1 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
// 学习引导弹窗
const [showGuideModal, setShowGuideModal] = useState(true);
// 对话结束后的学习引导弹窗
@@ -34,8 +38,14 @@ const HuangyanpeiPlace1 = () => {
const [visible, setVisible] = useState(false);
// 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识)
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
// 已浏览展品的 ID 集合
const [visited, setVisited] = useState<Set<number>>(new Set());
// 使用持久化的访问状态
const place = "place1";
const { markExhibitVisited, getVisitedExhibits } = useHuangyanpeiStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const handleOpenExhibit = (index: 1 | null) => {
setActiveExhibitId(index);
@@ -100,13 +110,14 @@ const HuangyanpeiPlace1 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onOpen={() => {
if (activeExhibitId === 1) {
triggerPuzzleAnimation(1);
}
}}
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
markExhibitVisited(place, activeExhibitId);
}
setActiveExhibitId(null);
}}
+20 -7
View File
@@ -10,6 +10,8 @@ import {
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useCursorController } from "@/hooks/useCursorController";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -24,12 +26,22 @@ const HuangyanpeiPlace2 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
const [visited, setVisited] = useState<Set<number>>(new Set());
// 使用持久化的访问状态
const place = "place2";
const { markExhibitVisited, getVisitedExhibits } = useHuangyanpeiStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
visited: visitedExhibits, // 传入数组,而不是 Set
});
// GuideSteps 当前步骤
@@ -116,13 +128,14 @@ const HuangyanpeiPlace2 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onOpen={() => {
if (activeExhibitId === 1) {
triggerPuzzleAnimation(2);
}
}}
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
markExhibitVisited(place, activeExhibitId);
}
setActiveExhibitId(null);
}}
+28 -13
View File
@@ -12,6 +12,8 @@ import { SuccessAlert } from "@/components/layout";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useCursorController } from "@/hooks/useCursorController";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -27,14 +29,24 @@ const HuangyanpeiPlace3 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(
null
);
const [visited, setVisited] = useState<Set<number>>(new Set());
// 使用持久化的访问状态
const place = "place3";
const { markExhibitVisited, getVisitedExhibits } = useHuangyanpeiStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [3],
dependencies: { 2: [3], 1: [2] },
visited,
visited: visitedExhibits, // 传入数组,而不是 Set
});
// 光标3的引导对话状态
@@ -158,6 +170,17 @@ const HuangyanpeiPlace3 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onOpen={() => {
if (activeExhibitId === 3) {
triggerPuzzleAnimation(3);
}
if (activeExhibitId === 2) {
triggerPuzzleAnimation(4);
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(5);
}
}}
onClose={() => {
if (activeExhibitId !== null) {
setActiveExhibitId(null);
@@ -165,11 +188,7 @@ const HuangyanpeiPlace3 = () => {
setShowQuiz3(true);
return;
}
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
markExhibitVisited(place, activeExhibitId);
} else {
setActiveExhibitId(null);
}
@@ -229,11 +248,7 @@ const HuangyanpeiPlace3 = () => {
visible={showSuccessAlert}
onContinue={() => {
setShowSuccessAlert(false);
setVisited((prev) => {
const next = new Set(prev);
next.add(3);
return next;
});
markExhibitVisited(place, 3);
}}
onClose={() => {
setShowSuccessAlert(false);
@@ -244,7 +259,7 @@ const HuangyanpeiPlace3 = () => {
<GuideSteps
visible={guide12Visible}
onVisibleChange={setGuide12Visible}
showMask={true}
showMask={false}
zIndex={10000}
>
<GuideStepsItem
+32 -8
View File
@@ -14,11 +14,13 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import type { VideoRef } from "@/components/video";
import { useCursorController } from "@/hooks/useCursorController";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
const exhibitSrcMap: Record<1 | 2 | 3 | 4 | 5, string> = {
1: "huangyanpei/place4/6_006",
1: "huangyanpei/place4/6_6&7",
2: "huangyanpei/place4/6_007",
3: "huangyanpei/place4/6_008",
4: "huangyanpei/place4/6_009",
@@ -31,10 +33,19 @@ const HuangyanpeiPlace4 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeExhibitId, setActiveExhibitId] = useState<1 | 3 | 4 | 5 | null>(
null
);
const [visited, setVisited] = useState<Set<number>>(new Set());
// 使用持久化的访问状态
const place = "place4";
const { markExhibitVisited, getVisitedExhibits } = useHuangyanpeiStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [3, 5],
dependencies: {
@@ -42,7 +53,7 @@ const HuangyanpeiPlace4 = () => {
4: [1],
// 2 已注释,不计入顺序
},
visited,
visited: visitedExhibits, // 传入数组,而不是 Set
});
// 光标1的引导对话状态
@@ -187,13 +198,23 @@ const HuangyanpeiPlace4 = () => {
// : undefined
// }
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onOpen={() => {
if (activeExhibitId === 3) {
triggerPuzzleAnimation(7);
}
if (activeExhibitId === 5) {
triggerPuzzleAnimation(6);
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(8);
}
if (activeExhibitId === 4) {
triggerPuzzleAnimation(10);
}
}}
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
markExhibitVisited(place, activeExhibitId);
}
setActiveExhibitId(null);
}}
@@ -205,6 +226,9 @@ const HuangyanpeiPlace4 = () => {
currentStep={guide1Step}
onStepChange={setGuide1Step}
onVisibleChange={(visible) => {
if (!visible) {
triggerPuzzleAnimation(9);
}
setGuide1Visible(visible);
}}
showMask={false}
+21 -12
View File
@@ -14,6 +14,8 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import type { VideoRef } from "@/components/video";
import { useCursorController } from "@/hooks/useCursorController";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -28,8 +30,9 @@ const HuangyanpeiPlace5 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
const [visited, setVisited] = useState<Set<number>>(new Set());
const [videoVisible, setVideoVisible] = useState(false);
const videoRef = useRef<VideoRef>(null);
// 光标1的引导对话状态
@@ -43,11 +46,19 @@ const HuangyanpeiPlace5 = () => {
// 成功提示状态
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
// 使用持久化的访问状态
const place = "place5";
const { markExhibitVisited, getVisitedExhibits } = useHuangyanpeiStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
// 光标顺序:1 -> 2(需完成1后解锁2)
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
visited: visitedExhibits, // 传入数组,而不是 Set
});
const handleOpenExhibit = (index: 1 | 2) => {
@@ -159,13 +170,14 @@ const HuangyanpeiPlace5 = () => {
// : undefined
// }
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onOpen={() => {
if (activeExhibitId === 1) {
triggerPuzzleAnimation(11);
}
}}
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
markExhibitVisited(place, activeExhibitId);
}
setActiveExhibitId(null);
}}
@@ -268,11 +280,8 @@ const HuangyanpeiPlace5 = () => {
visible={showSuccessAlert}
onContinue={() => {
setShowSuccessAlert(false);
setVisited((prev) => {
const next = new Set(prev);
next.add(2);
return next;
});
markExhibitVisited(place, 2);
triggerPuzzleAnimation(12);
}}
onClose={() => {
setShowSuccessAlert(false);
+29 -16
View File
@@ -10,6 +10,8 @@ import {
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { EndingOverlay } from "@/components/overlay/EndingOverlay";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -23,8 +25,9 @@ const HuangyanpeiPlace6 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
const [visited, setVisited] = useState<Set<number>>(new Set());
// 对话引导状态
const [guideVisible, setGuideVisible] = useState(false);
// 结束窗口状态
@@ -32,6 +35,14 @@ const HuangyanpeiPlace6 = () => {
// 是否已经显示过结束窗口(防止重复弹出)
const [hasShownEnding, setHasShownEnding] = useState(false);
// 使用持久化的访问状态
const place = "place6";
const { markExhibitVisited, getVisitedExhibits } = useHuangyanpeiStore();
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const [Leave, setLeave] = useState(false);
const handleOpenExhibit = (index: 1) => {
@@ -41,16 +52,14 @@ const HuangyanpeiPlace6 = () => {
};
// 当 cursor 已浏览后,显示结束窗口(只显示一次)
useEffect(() => {
if (visited.has(1) && !hasShownEnding && !endingVisible) {
// 延迟一下显示,确保展品关闭动画完成
const timer = setTimeout(() => {
setEndingVisible(true);
setHasShownEnding(true);
}, 500);
return () => clearTimeout(timer);
}
}, [visited, hasShownEnding, endingVisible]);
// useEffect(() => {
// if (visited.has(1) && !hasShownEnding && !endingVisible) {
// // 延迟一下显示,确保展品关闭动画完成
// const timer = setTimeout(() => {
// }, 500);
// return () => clearTimeout(timer);
// }
// }, [visited, hasShownEnding, endingVisible]);
const progressCount = visited.size;
const total = 1;
@@ -107,15 +116,19 @@ const HuangyanpeiPlace6 = () => {
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
onOpen={() => {
if (activeExhibitId === 1) {
triggerPuzzleAnimation(13);
}
}}
onClose={() => {
if (activeExhibitId !== null) {
setVisited((prev) => {
const next = new Set(prev);
next.add(activeExhibitId);
return next;
});
markExhibitVisited(place, activeExhibitId);
}
setActiveExhibitId(null);
setEndingVisible(true);
setHasShownEnding(true);
}}
/>
+21 -20
View File
@@ -1,9 +1,10 @@
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useEffect, useMemo } from "react";
import { Title, Image, Cursor, StudyGuideModal } from "@/components";
import { BambooPuzzleGame, QuestionModal } from "./components";
import { SuccessAlert } from "@/components/layout";
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -13,16 +14,22 @@ const HuangyanpeiReflectionScene1 = () => {
moduleName: string;
}>();
const [visited, setVisited] = useState<Set<number>>(new Set());
const [showGuideModal, setShowGuideModal] = useState(true);
const [showQuestionModal, setShowQuestionModal] = useState(false);
const [questionCompleted, setQuestionCompleted] = useState(false);
const [showGame, setShowGame] = useState(false);
const [gameCompleted, setGameCompleted] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [showNextGuide, setShowNextGuide] = useState(false);
const [nextGuideShown, setNextGuideShown] = useState(false);
// 使用持久化的reflection cursor状态
const scene = "scene1";
const { markReflectionCursorVisited, getVisitedReflectionCursors } =
useHuangyanpeiStore();
// 从 store 获取已完成的cursor,转换为 Set 用于兼容现有代码
const visitedCursors = getVisitedReflectionCursors(scene);
const visited = useMemo(() => new Set(visitedCursors), [visitedCursors]);
const progressCount = visited.size;
const total = 2;
const completed = progressCount >= total;
@@ -35,14 +42,14 @@ const HuangyanpeiReflectionScene1 = () => {
// 两个任务都完成后 500ms 弹出下一站提示(仅一次)
useEffect(() => {
if (questionCompleted && gameCompleted && !nextGuideShown) {
if (visited.has(1) && visited.has(2) && !nextGuideShown) {
const timer = setTimeout(() => {
setShowNextGuide(true);
setNextGuideShown(true);
}, 500);
return () => clearTimeout(timer);
}
}, [questionCompleted, gameCompleted, nextGuideShown]);
}, [visited, nextGuideShown]);
return (
<div
@@ -86,29 +93,28 @@ const HuangyanpeiReflectionScene1 = () => {
<Cursor
className="absolute left-68 top-55"
onClick={() => {
if (!questionCompleted) {
if (!visited.has(1)) {
setShowQuestionModal(true);
}
}}
read={questionCompleted}
read={visited.has(1)}
/>
<Cursor
className="absolute left-160 bottom-50"
onClick={() => {
// 如果游戏已完成,不再打开
if (!gameCompleted) {
if (!visited.has(2)) {
setShowGame(true);
}
}}
read={gameCompleted}
read={visited.has(2)}
/>
<QuestionModal
visible={showQuestionModal}
onSuccess={() => {
setShowQuestionModal(false);
setQuestionCompleted(true);
setVisited((prev) => new Set(prev).add(1));
markReflectionCursorVisited(scene, 1);
}}
onClose={() => setShowQuestionModal(false)}
/>
@@ -135,7 +141,7 @@ const HuangyanpeiReflectionScene1 = () => {
// 游戏完成后,显示成功提示
setShowGame(false);
setShowSuccessAlert(true);
setVisited((prev) => new Set(prev).add(2));
markReflectionCursorVisited(scene, 2);
}}
/>
@@ -144,13 +150,8 @@ const HuangyanpeiReflectionScene1 = () => {
title="拼接毛竹单杠成功,同学们可以在课间之余强身健体!"
onContinue={useCallback(() => {
setShowSuccessAlert(false);
setGameCompleted(true);
setVisited((prev) => {
const next = new Set(prev);
next.add(2);
return next;
});
}, [])}
markReflectionCursorVisited(scene, 2);
}, [markReflectionCursorVisited, scene])}
/>
<StudyGuideModal
+21 -20
View File
@@ -4,6 +4,7 @@ import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import ChoiceDialog1 from "./scene2/ChoiceDialog1";
import ChoiceDialog2 from "./scene2/ChoiceDialog2";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -16,14 +17,19 @@ const HuangyanpeiReflectionScene2 = () => {
const [showGuideModal, setShowGuideModal] = useState(true);
const [showDialog1, setShowDialog1] = useState(false);
const [showDialog2, setShowDialog2] = useState(false);
const [visited, setVisited] = useState<Set<number>>(new Set());
const [choice1, setChoice1] = useState<
"读书" | "没出息" | "女子不能上学" | null
>(null);
const [choice2, setChoice2] = useState<"铁工科" | "商科" | null>(null);
const [showCompleteGuide, setShowCompleteGuide] = useState(false);
const [completeGuideShown, setCompleteGuideShown] = useState(false);
// 使用持久化的reflection cursor状态
const scene = "scene2";
const { markReflectionCursorVisited, getVisitedReflectionCursors } =
useHuangyanpeiStore();
// 从 store 获取已完成的cursor,转换为 Set 用于兼容现有代码
const visitedCursors = getVisitedReflectionCursors(scene);
const visited = new Set(visitedCursors);
const progressCount = visited.size;
const total = 2;
const completed = progressCount >= total;
@@ -105,28 +111,27 @@ const HuangyanpeiReflectionScene2 = () => {
{/* 互动展品光标1:显示第一个选择弹窗 */}
<Cursor
className="absolute left-118 bottom-105"
onClick={() => setShowDialog1(true)}
read={choice1 !== null}
onClick={() => {
setShowDialog1(true);
}}
read={visited.has(1)}
/>
{/* 互动展品光标2:显示第二个选择弹窗 */}
<Cursor
className="absolute right-74 bottom-68"
onClick={() => setShowDialog2(true)}
read={choice2 !== null}
onClick={() => {
setShowDialog2(true);
}}
read={visited.has(2)}
/>
{/* 第一个选择弹窗 */}
<ChoiceDialog1
visible={showDialog1}
onClose={() => setShowDialog1(false)}
onSelect={(choice) => {
setChoice1(choice);
setVisited((prev) => {
const next = new Set(prev);
next.add(1);
return next;
});
onSelect={() => {
markReflectionCursorVisited(scene, 1);
}}
/>
@@ -136,11 +141,7 @@ const HuangyanpeiReflectionScene2 = () => {
onClose={() => setShowDialog2(false)}
onSelect={(choice) => {
setChoice2(choice);
setVisited((prev) => {
const next = new Set(prev);
next.add(2);
return next;
});
markReflectionCursorVisited(scene, 2);
}}
/>
@@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { cn, getStaticImage } from "@/lib/utils";
import { Image, StudyGuideModal } from "@/components";
import { SuccessAlert, ErrorAlert } from "@/components/layout";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
export interface ChoiceDialog1Props {
/** 是否可见 */
@@ -30,6 +31,8 @@ const ChoiceDialog1 = ({
const choice2Src = getStaticImage("huangyanpei/reflection/scene2/10_006");
const choice3Src = getStaticImage("huangyanpei/reflection/scene2/10_007");
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [mounted, setMounted] = useState(false);
const [showGuide, setShowGuide] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
@@ -113,7 +116,10 @@ const ChoiceDialog1 = ({
<StudyGuideModal
visible={showGuide}
onContinue={() => setShowGuide(false)}
onContinue={() => {
setShowGuide(false);
triggerPuzzleAnimation(14);
}}
verticalAlign="center"
>
黄校长来考你啦!他设计这所学校,就是为了让学生摆脱旧观念,你知道是哪种吗?
+18 -17
View File
@@ -7,6 +7,7 @@ import { useNavigate, useParams } from "react-router-dom";
import ViseAssemblyDialog from "./scene3/ViseAssemblyDialog";
import type { VideoRef } from "@/components/video";
import { QuizDialog } from "./components";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -27,9 +28,17 @@ const HuangyanpeiReflectionScene3 = () => {
const [showErrorAlertForQuiz, setShowErrorAlertForQuiz] = useState(false);
const [showCompleteGuide, setShowCompleteGuide] = useState(false);
const [showVideo, setShowVideo] = useState(false);
const [visited, setVisited] = useState<Set<number>>(new Set());
const videoRef = useRef<VideoRef>(null);
// 使用持久化的reflection cursor状态
const scene = "scene3";
const { markReflectionCursorVisited, getVisitedReflectionCursors } =
useHuangyanpeiStore();
// 从 store 获取已完成的cursor,转换为 Set 用于兼容现有代码
const visitedCursors = getVisitedReflectionCursors(scene);
const visited = new Set(visitedCursors);
if (!moduleName) {
return null;
}
@@ -100,12 +109,10 @@ const HuangyanpeiReflectionScene3 = () => {
<Cursor
className="absolute right-100 bottom-55"
onClick={() => {
if (!showGuide1) {
if (visited.has(2)) {
setShowDialog(true);
} else {
setShowErrorAlert(true);
}
if (visited.has(2)) {
setShowDialog(true);
} else {
setShowErrorAlert(true);
}
}}
read={visited.has(1)}
@@ -115,9 +122,7 @@ const HuangyanpeiReflectionScene3 = () => {
<Cursor
className="absolute left-145 top-68"
onClick={() => {
if (!showGuide1) {
setShowQuiz(true);
}
setShowQuiz(true);
}}
read={visited.has(2)}
/>
@@ -129,11 +134,7 @@ const HuangyanpeiReflectionScene3 = () => {
onComplete={() => {
setShowDialog(false);
setShowCompleteGuide(true);
setVisited((prev) => {
const next = new Set(prev);
next.add(1);
return next;
});
markReflectionCursorVisited(scene, 1);
}}
/>
@@ -173,7 +174,7 @@ const HuangyanpeiReflectionScene3 = () => {
content='黄先生说"手脑并用",懂了理论,也要动手实践。'
onContinue={() => {
setShowSuccessAlert(false);
setVisited((prev) => new Set(prev).add(2));
markReflectionCursorVisited(scene, 2);
}}
onClose={() => {
setShowSuccessAlert(false);
@@ -234,7 +235,7 @@ const HuangyanpeiReflectionScene3 = () => {
onClose={() => setShowQuizDialog(false)}
onComplete={() => {
setShowQuizDialog(false);
setVisited((prev) => new Set(prev).add(3));
markReflectionCursorVisited(scene, 3);
}}
/>
</div>
+27 -21
View File
@@ -7,6 +7,8 @@ import { useNavigate, useParams } from "react-router-dom";
import TowelSalesDialog from "./scene4/TowelSalesDialog";
import type { VideoRef } from "@/components/video";
import { QuizDialog } from "./components";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useHuangyanpeiStore } from "@/store/huangyanpeiStore";
const starName = "huangyanpei";
@@ -16,8 +18,9 @@ const HuangyanpeiReflectionScene4 = () => {
moduleName: string;
}>();
const { triggerPuzzleAnimation } = usePuzzleAnimation();
const [_activeExhibitId, _setActiveExhibitId] = useState<1 | 2 | null>(null);
const [visited, setVisited] = useState<Set<number>>(new Set());
const [showQuiz1, setShowQuiz1] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [showTowelDialog, setShowTowelDialog] = useState(false);
@@ -26,6 +29,15 @@ const HuangyanpeiReflectionScene4 = () => {
const [showGuide1, setShowGuide1] = useState(true);
const [showErrorAlertForQuiz, setShowErrorAlertForQuiz] = useState(false);
// 使用持久化的reflection cursor状态
const scene = "scene4";
const { markReflectionCursorVisited, getVisitedReflectionCursors } =
useHuangyanpeiStore();
// 从 store 获取已完成的cursor,转换为 Set 用于兼容现有代码
const visitedCursors = getVisitedReflectionCursors(scene);
const visited = new Set(visitedCursors);
//喇叭考题弹窗
const [showQuizDialog, setShowQuizDialog] = useState(false);
@@ -33,22 +45,14 @@ const HuangyanpeiReflectionScene4 = () => {
const handleSuccessAlertContinue = useCallback(() => {
setShowSuccessAlert(false);
// 更新visited状态
setVisited((prev) => {
const next = new Set(prev);
next.add(1);
return next;
});
}, []);
markReflectionCursorVisited(scene, 1);
}, [markReflectionCursorVisited, scene]);
const handleSuccessAlertClose = useCallback(() => {
setShowSuccessAlert(false);
// 更新visited状态
setVisited((prev) => {
const next = new Set(prev);
next.add(1);
return next;
});
}, []);
markReflectionCursorVisited(scene, 1);
}, [markReflectionCursorVisited, scene]);
if (!moduleName) {
return null;
@@ -106,14 +110,18 @@ const HuangyanpeiReflectionScene4 = () => {
{/* 互动展品光标1 */}
<Cursor
className="absolute left-100 bottom-88"
onClick={() => setShowQuiz1(true)}
onClick={() => {
setShowQuiz1(true);
}}
read={visited.has(1)}
/>
{/* 互动展品光标2 */}
<Cursor
className="absolute right-82 bottom-88"
onClick={() => setShowTowelDialog(true)}
onClick={() => {
setShowTowelDialog(true);
}}
read={visited.has(2)}
/>
@@ -166,11 +174,9 @@ const HuangyanpeiReflectionScene4 = () => {
onClose={() => setShowTowelDialog(false)}
onComplete={() => {
// 完成拖拽后更新visited状态
setVisited((prev) => {
const next = new Set(prev);
next.add(2);
return next;
});
markReflectionCursorVisited(scene, 2);
triggerPuzzleAnimation(15);
}}
/>
@@ -218,7 +224,7 @@ const HuangyanpeiReflectionScene4 = () => {
onClose={() => setShowQuizDialog(false)}
onComplete={() => {
setShowQuizDialog(false);
setVisited((prev) => new Set(prev).add(3));
markReflectionCursorVisited(scene, 3);
}}
/>