diff --git a/src/pages/luxun/place/place1.tsx b/src/pages/luxun/place/place1.tsx index b3038eb..9a520c9 100644 --- a/src/pages/luxun/place/place1.tsx +++ b/src/pages/luxun/place/place1.tsx @@ -14,6 +14,7 @@ import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useState } from "react"; import { useCursorController } from "@/hooks/useCursorController"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useLuxunStore } from "@/store/luxunStore"; // starName 固定为 luxun const starName = "luxun"; @@ -47,8 +48,13 @@ const LuxunPlace1 = () => { const [afterExhibitDialogStep, setAfterExhibitDialogStep] = useState(0); // 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识) const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null); - // 已浏览展品的 ID 集合 - const [visited, setVisited] = useState>(new Set()); + // 使用持久化的访问状态 + const place = "place1"; + const { markExhibitVisited, getVisitedExhibits } = useLuxunStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // 连线题显示状态(展品1关闭后) const [showMatchQuiz, setShowMatchQuiz] = useState(false); // 连线题成功提示状态 @@ -171,11 +177,7 @@ const LuxunPlace1 = () => { setShowMatchQuiz(true); } else { // 展品2正常标记为已访问 - setVisited((prev) => { - const next = new Set(prev); - next.add(activeExhibitId); - return next; - }); + markExhibitVisited(place, activeExhibitId); } } setActiveExhibitId(null); @@ -321,20 +323,12 @@ const LuxunPlace1 = () => { onContinue={() => { setShowMatchQuizSuccess(false); // 标记展品1为已访问 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); }} onClose={() => { setShowMatchQuizSuccess(false); // 标记展品1为已访问 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); }} /> diff --git a/src/pages/luxun/place/place2.tsx b/src/pages/luxun/place/place2.tsx index 6eb7a86..a5b2ff1 100644 --- a/src/pages/luxun/place/place2.tsx +++ b/src/pages/luxun/place/place2.tsx @@ -15,6 +15,7 @@ import { useState } from "react"; import { useCursorController } from "@/hooks/useCursorController"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; import type { DragFillResult } from "@/components"; +import { useLuxunStore } from "@/store/luxunStore"; // starName 固定为 luxun const starName = "luxun"; @@ -38,7 +39,14 @@ const LuxunPlace2 = () => { const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>( null ); - const [visited, setVisited] = useState>(new Set()); + + // 使用持久化的访问状态 + const place = "place2"; + const { markExhibitVisited, getVisitedExhibits } = useLuxunStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // Cursor 顺序控制:1 -> 2 -> 3 const { isEnabled } = useCursorController({ @@ -214,11 +222,7 @@ const LuxunPlace2 = () => { // } onClose={() => { if (activeExhibitId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(activeExhibitId); - return next; - }); + markExhibitVisited(place, activeExhibitId); } setActiveExhibitId(null); }} diff --git a/src/pages/luxun/place/place3.tsx b/src/pages/luxun/place/place3.tsx index 140fba3..048b127 100644 --- a/src/pages/luxun/place/place3.tsx +++ b/src/pages/luxun/place/place3.tsx @@ -13,6 +13,7 @@ import { useState } from "react"; import SuccessAlert from "@/components/layout/alert/success-alert"; import { useCursorController } from "@/hooks/useCursorController"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useLuxunStore } from "@/store/luxunStore"; // starName 固定为 luxun const starName = "luxun"; @@ -55,7 +56,14 @@ const LuxunPlace3 = () => { const [activeExhibitId, setActiveExhibitId] = useState< 1 | 2 | 3 | 4 | 5 | null >(null); - const [visited, setVisited] = useState>(new Set()); + + // 使用持久化的访问状态 + const place = "place3"; + const { markExhibitVisited, getVisitedExhibits } = useLuxunStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // Cursor 顺序控制:1 -> 2 -> 3 -> 4 -> 5 const { isEnabled } = useCursorController({ @@ -259,11 +267,7 @@ const LuxunPlace3 = () => { } else { // 其他光标:正常关闭并标记为已读 if (activeExhibitId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(activeExhibitId); - return next; - }); + markExhibitVisited(place, activeExhibitId); } setActiveExhibitId(null); } @@ -277,11 +281,7 @@ const LuxunPlace3 = () => { setGuide1Visible(visible); if (!visible) { // 对话关闭时,标记光标1为已读 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); } }} currentStep={guide1Step} @@ -383,11 +383,7 @@ const LuxunPlace3 = () => { onContinue={() => { setSuccessAlert2Visible(false); // 标记光标2为已读 - setVisited((prev) => { - const next = new Set(prev); - next.add(2); - return next; - }); + markExhibitVisited(place, 2); }} /> @@ -398,11 +394,7 @@ const LuxunPlace3 = () => { setGuide3Visible(visible); if (!visible) { // 对话关闭时,标记光标3为已读 - setVisited((prev) => { - const next = new Set(prev); - next.add(3); - return next; - }); + markExhibitVisited(place, 3); } }} currentStep={guide3Step} @@ -432,11 +424,7 @@ const LuxunPlace3 = () => { setGuide4Visible(visible); if (!visible) { // 对话关闭时,标记光标4为已读 - setVisited((prev) => { - const next = new Set(prev); - next.add(4); - return next; - }); + markExhibitVisited(place, 4); } }} currentStep={guide4Step} @@ -496,11 +484,7 @@ const LuxunPlace3 = () => { onContinue={() => { setSuccessAlert5Visible(false); // 标记光标5为已读 - setVisited((prev) => { - const next = new Set(prev); - next.add(5); - return next; - }); + markExhibitVisited(place, 5); // 打开光标5的引导对话 setGuide5Visible(true); setGuide5Step(0); diff --git a/src/pages/luxun/place/place4.tsx b/src/pages/luxun/place/place4.tsx index 2a0235a..92da226 100644 --- a/src/pages/luxun/place/place4.tsx +++ b/src/pages/luxun/place/place4.tsx @@ -16,6 +16,7 @@ import { useState, useRef, useMemo } from "react"; import SuccessAlert from "@/components/layout/alert/success-alert"; import { useCursorController } from "@/hooks/useCursorController"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useLuxunStore } from "@/store/luxunStore"; // starName 固定为 luxun const starName = "luxun"; @@ -54,7 +55,14 @@ const LuxunPlace5 = () => { const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>( null ); - const [visited, setVisited] = useState>(new Set()); + + // 使用持久化的访问状态 + const place = "place4"; + const { markExhibitVisited, getVisitedExhibits } = useLuxunStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // Cursor 顺序控制:1 -> 2 -> 3 const { isEnabled } = useCursorController({ @@ -252,13 +260,9 @@ const LuxunPlace5 = () => { setActiveExhibitId(null); return; } - setVisited((prev) => { - const next = new Set(prev); - if (activeExhibitId !== null) { - next.add(activeExhibitId); - } - return next; - }); + if (activeExhibitId !== null) { + markExhibitVisited(place, activeExhibitId); + } setActiveExhibitId(null); }} /> @@ -327,11 +331,7 @@ const LuxunPlace5 = () => { content="1933年,瞿秋白夫妇第三次到鲁迅家避难,就住进了这间仅6平方米的客房,与鲁迅常常彻夜长谈革命与文学。" onContinue={() => { setSuccessAlertVisible(false); - setVisited((prev) => { - const next = new Set(prev); - next.add(2); - return next; - }); + markExhibitVisited(place, 2); }} /> @@ -341,11 +341,7 @@ const LuxunPlace5 = () => { onVisibleChange={(visible) => { setGuide3Visible(visible); if (!visible) { - setVisited((prev) => { - const next = new Set(prev); - next.add(3); - return next; - }); + markExhibitVisited(place, 3); } }} currentStep={guide3Step} diff --git a/src/pages/sunzhongshan/place/place1.tsx b/src/pages/sunzhongshan/place/place1.tsx index cbda4fa..e7c8789 100644 --- a/src/pages/sunzhongshan/place/place1.tsx +++ b/src/pages/sunzhongshan/place/place1.tsx @@ -17,6 +17,7 @@ import { SuccessAlert, DragQuizPanel } from "@/components/layout"; import { MultipleChoiceQuizPanel } from "@/components/layout/quiz"; import { useCursorController } from "@/hooks/useCursorController"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; // starName 固定为 sunzhongshan const starName = "sunzhongshan"; @@ -50,8 +51,13 @@ const SunzhongshanPlace1 = () => { const [activeExhibitId, setActiveExhibitId] = useState< 1 | 2 | 3 | 4 | 5 | null >(null); - // 已浏览展品的 ID 集合 - const [visited, setVisited] = useState>(new Set()); + // 使用持久化的访问状态 + const place = "place1"; + const { markExhibitVisited, getVisitedExhibits } = useSunzhongshanStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // 展品2窗口关闭的对话状态 const [guide2CloseVisible, setGuide2CloseVisible] = useState(false); @@ -256,11 +262,7 @@ const SunzhongshanPlace1 = () => { return; } if (currentId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(currentId); - return next; - }); + markExhibitVisited(place, currentId); } setActiveExhibitId(null); }} @@ -328,11 +330,7 @@ const SunzhongshanPlace1 = () => { onVisibleChange={(nextVisible) => { setGuide2Visible(nextVisible); if (!nextVisible) { - setVisited((prev) => { - const next = new Set(prev); - next.add(2); - return next; - }); + markExhibitVisited(place, 2); } }} showMask={false} @@ -386,11 +384,7 @@ const SunzhongshanPlace1 = () => { onVisibleChange={(nextVisible) => { setGuide5Visible(nextVisible); if (!nextVisible) { - setVisited((prev) => { - const next = new Set(prev); - next.add(5); - return next; - }); + markExhibitVisited(place, 5); } }} showMask={false} @@ -514,11 +508,7 @@ const SunzhongshanPlace1 = () => { onContinue={() => { setSuccessAlert4Visible(false); // setShowVideo4Second(true); - setVisited((prev) => { - const next = new Set(prev); - next.add(4); - return next; - }); + markExhibitVisited(place, 4); }} onClose={() => { setSuccessAlert4Visible(false); @@ -536,8 +526,8 @@ const SunzhongshanPlace1 = () => { showSkipButton onEnded={() => { setShowVideo4Second(false); - - setVisited((prev) => new Set([...prev, 3])); + // 视频播放结束后,标记展品3为已访问 + markExhibitVisited(place, 3); }} /> )} @@ -645,20 +635,12 @@ const SunzhongshanPlace1 = () => { onContinue={() => { setSuccessAlert1Visible(false); // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); }} onClose={() => { setSuccessAlert1Visible(false); // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); }} /> diff --git a/src/pages/sunzhongshan/place/place2.tsx b/src/pages/sunzhongshan/place/place2.tsx index bed4d29..0e3872f 100644 --- a/src/pages/sunzhongshan/place/place2.tsx +++ b/src/pages/sunzhongshan/place/place2.tsx @@ -13,6 +13,7 @@ import { useCursorController } from "@/hooks/useCursorController"; import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; @@ -35,7 +36,14 @@ const SunzhongshanPlace2 = () => { const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>( null ); - const [visited, setVisited] = useState>(new Set()); + + // 使用持久化的访问状态 + const place = "place2"; + const { markExhibitVisited, getVisitedExhibits } = useSunzhongshanStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // Cursor 顺序控制:1 -> 2 -> 3 -> 4 const { isEnabled } = useCursorController({ @@ -186,11 +194,7 @@ const SunzhongshanPlace2 = () => { // 光标4:正常关闭,更新visited状态 } if (currentId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(currentId); - return next; - }); + markExhibitVisited(place, currentId); } setActiveExhibitId(null); }} @@ -237,11 +241,7 @@ const SunzhongshanPlace2 = () => { onEnded={() => { setShowVideo1(false); // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); }} /> )} @@ -287,11 +287,7 @@ const SunzhongshanPlace2 = () => { onEnded={() => { setShowVideo2(false); // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(2); - return next; - }); + markExhibitVisited(place, 2); }} /> )} diff --git a/src/pages/sunzhongshan/place/place3.tsx b/src/pages/sunzhongshan/place/place3.tsx index 498d83d..7c711ea 100644 --- a/src/pages/sunzhongshan/place/place3.tsx +++ b/src/pages/sunzhongshan/place/place3.tsx @@ -15,6 +15,7 @@ import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { SuccessAlert, DragQuizPanel } from "@/components/layout"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; @@ -37,7 +38,14 @@ const SunzhongshanPlace3 = () => { const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>( null ); - const [visited, setVisited] = useState>(new Set()); + + // 使用持久化的访问状态 + const place = "place3"; + const { markExhibitVisited, getVisitedExhibits } = useSunzhongshanStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // Cursor 顺序控制:1 -> 2 -> 3 -> 4 const { isEnabled } = useCursorController({ @@ -195,11 +203,7 @@ const SunzhongshanPlace3 = () => { } if (currentId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(currentId); - return next; - }); + markExhibitVisited(place, currentId); } setActiveExhibitId(null); }} @@ -338,22 +342,14 @@ const SunzhongshanPlace3 = () => { { - setSuccessAlert2Visible(false); - // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(2); - return next; - }); + setSuccessAlert2Visible(false); + // 修改cursor状态 + markExhibitVisited(place, 2); }} onClose={() => { setSuccessAlert2Visible(false); // 修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(2); - return next; - }); + markExhibitVisited(place, 2); }} /> diff --git a/src/pages/sunzhongshan/place/place4.tsx b/src/pages/sunzhongshan/place/place4.tsx index a17e2e7..a053719 100644 --- a/src/pages/sunzhongshan/place/place4.tsx +++ b/src/pages/sunzhongshan/place/place4.tsx @@ -14,6 +14,7 @@ import { useCursorController } from "@/hooks/useCursorController"; import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; @@ -39,7 +40,14 @@ const SunzhongshanPlace4 = () => { const [showInitialDialog, setShowInitialDialog] = useState(true); const [initialDialogStep, setInitialDialogStep] = useState(0); const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null); - const [visited, setVisited] = useState>(new Set()); + + // 使用持久化的访问状态 + const place = "place4"; + const { markExhibitVisited, getVisitedExhibits } = useSunzhongshanStore(); + + // 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码 + const visitedExhibits = getVisitedExhibits(place); + const visited = new Set(visitedExhibits); // Cursor 顺序控制:1 -> 2 const { isEnabled } = useCursorController({ @@ -143,11 +151,7 @@ const SunzhongshanPlace4 = () => { } if (currentId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(currentId); - return next; - }); + markExhibitVisited(place, currentId); } setActiveExhibitId(null); }} @@ -177,11 +181,7 @@ const SunzhongshanPlace4 = () => { onContinue={() => { setShowVideo1Guide(false); // 学习引导关闭后,修改cursor状态 - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); + markExhibitVisited(place, 1); }} animationSpeed={200} audioSrc={getPublicPath("audio/sunzhongshan/mp3_sunzhongshan_46.mp3")} diff --git a/src/pages/sunzhongshan/reflection/index.tsx b/src/pages/sunzhongshan/reflection/index.tsx index 513cbcf..dcf913f 100644 --- a/src/pages/sunzhongshan/reflection/index.tsx +++ b/src/pages/sunzhongshan/reflection/index.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Image, StudyGuideModal } from "@/components"; import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; const GUIDE_STORAGE_KEY = `${starName}-reflection-guide-shown`; @@ -24,17 +25,19 @@ const SunzhongshanReflection = () => { scene3: false, }); + const { getReflectionFlag, setReflectionFlag } = useSunzhongshanStore(); + // 检查场景完成状态 - 每次组件挂载或从场景页面返回时重新检查 const checkSceneCompleted = () => { - const scene1Completed = - localStorage.getItem(`${starName}-reflection-scene1-completed`) === - "true"; - const scene2Completed = - localStorage.getItem(`${starName}-reflection-scene2-completed`) === - "true"; - const scene3Completed = - localStorage.getItem(`${starName}-reflection-scene3-completed`) === - "true"; + const scene1Completed = getReflectionFlag( + `${starName}-reflection-scene1-completed` + ); + const scene2Completed = getReflectionFlag( + `${starName}-reflection-scene2-completed` + ); + const scene3Completed = getReflectionFlag( + `${starName}-reflection-scene3-completed` + ); setSceneCompleted({ scene1: scene1Completed, @@ -55,15 +58,16 @@ const SunzhongshanReflection = () => { return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 检查是否是第一次进入,显示学习引导 useEffect(() => { - const hasShownGuide = localStorage.getItem(GUIDE_STORAGE_KEY); + const hasShownGuide = getReflectionFlag(GUIDE_STORAGE_KEY); if (!hasShownGuide) { setShowGuide(true); } - }, []); + }, [getReflectionFlag]); // 检查场景是否可解锁 const isSceneUnlocked = (sceneNumber: number): boolean => { @@ -82,7 +86,7 @@ const SunzhongshanReflection = () => { const handleGuideClose = () => { setShowGuide(false); - localStorage.setItem(GUIDE_STORAGE_KEY, "true"); + setReflectionFlag(GUIDE_STORAGE_KEY, true); }; const handleStartExplore = () => { diff --git a/src/pages/sunzhongshan/reflection/scene1.tsx b/src/pages/sunzhongshan/reflection/scene1.tsx index 175a129..2837f02 100644 --- a/src/pages/sunzhongshan/reflection/scene1.tsx +++ b/src/pages/sunzhongshan/reflection/scene1.tsx @@ -15,6 +15,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { useClueStore } from "@/store/clueStore"; import type { ClueHint } from "@/components/clue"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; const sceneId = `${starName}/reflection/scene1`; @@ -69,9 +70,10 @@ const SunzhongshanReflectionScene1 = () => { const { moduleName } = useParams<{ moduleName: string; }>(); - + // 获取碎片动画触发方法 const { triggerPuzzleAnimation } = usePuzzleAnimation(); + const { getReflectionFlag, setReflectionFlag } = useSunzhongshanStore(); const [showClueHint, setShowClueHint] = useState(false); const [showClueBook, setShowClueBook] = useState(false); @@ -179,7 +181,7 @@ const SunzhongshanReflectionScene1 = () => { const handleVideoEnd = () => { setShowVideo(false); // 检查是否需要显示学习引导 - const hasShownGuide = localStorage.getItem(GUIDE_STORAGE_KEY); + const hasShownGuide = getReflectionFlag(GUIDE_STORAGE_KEY); if (!hasShownGuide) { setShowGuide(true); } @@ -188,7 +190,7 @@ const SunzhongshanReflectionScene1 = () => { // 处理学习引导关闭 const handleGuideClose = () => { setShowGuide(false); - localStorage.setItem(GUIDE_STORAGE_KEY, "true"); + setReflectionFlag(GUIDE_STORAGE_KEY, true); }; // 检查是否所有线索都已收集 @@ -240,7 +242,7 @@ const SunzhongshanReflectionScene1 = () => { const handleCompleteGuideClose = () => { setShowCompleteGuide(false); // 保存场景1完成状态 - localStorage.setItem(`${starName}-reflection-scene1-completed`, "true"); + setReflectionFlag(`${starName}-reflection-scene1-completed`, true); if (moduleName && starName) { navigate(`/${moduleName}/${starName}/reflection2`); } diff --git a/src/pages/sunzhongshan/reflection/scene2.tsx b/src/pages/sunzhongshan/reflection/scene2.tsx index 504b68c..e0de422 100644 --- a/src/pages/sunzhongshan/reflection/scene2.tsx +++ b/src/pages/sunzhongshan/reflection/scene2.tsx @@ -15,6 +15,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { useClueStore } from "@/store/clueStore"; import type { ClueHint } from "@/components/clue"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; const sceneId = `${starName}/reflection/scene2`; @@ -82,6 +83,7 @@ const SunzhongshanReflectionScene2 = () => { // 获取碎片动画触发方法 const { triggerPuzzleAnimation } = usePuzzleAnimation(); + const { getReflectionFlag, setReflectionFlag } = useSunzhongshanStore(); const [showClueHint, setShowClueHint] = useState(false); const [showClueBook, setShowClueBook] = useState(false); @@ -189,7 +191,7 @@ const SunzhongshanReflectionScene2 = () => { const handleVideoEnd = () => { setShowVideo(false); // 检查是否需要显示学习引导 - const hasShownGuide = localStorage.getItem(GUIDE_STORAGE_KEY); + const hasShownGuide = getReflectionFlag(GUIDE_STORAGE_KEY); if (!hasShownGuide) { setShowGuide(true); } @@ -198,7 +200,7 @@ const SunzhongshanReflectionScene2 = () => { // 处理学习引导关闭 const handleGuideClose = () => { setShowGuide(false); - localStorage.setItem(GUIDE_STORAGE_KEY, "true"); + setReflectionFlag(GUIDE_STORAGE_KEY, true); }; // 检查是否所有线索都已收集 @@ -250,7 +252,7 @@ const SunzhongshanReflectionScene2 = () => { const handleCompleteGuideClose = () => { setShowCompleteGuide(false); // 保存场景2完成状态 - localStorage.setItem(`${starName}-reflection-scene2-completed`, "true"); + setReflectionFlag(`${starName}-reflection-scene2-completed`, true); if (moduleName && starName) { navigate(`/${moduleName}/${starName}/reflection3`); } diff --git a/src/pages/sunzhongshan/reflection/scene3.tsx b/src/pages/sunzhongshan/reflection/scene3.tsx index 4de3c8a..4b2b178 100644 --- a/src/pages/sunzhongshan/reflection/scene3.tsx +++ b/src/pages/sunzhongshan/reflection/scene3.tsx @@ -16,6 +16,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils"; import { useClueStore } from "@/store/clueStore"; import type { ClueHint } from "@/components/clue"; import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext"; +import { useSunzhongshanStore } from "@/store/sunzhongshanStore"; const starName = "sunzhongshan"; const sceneId = `${starName}/reflection/scene3`; @@ -73,6 +74,7 @@ const SunzhongshanReflectionScene3 = () => { // 获取碎片动画触发方法 const { triggerPuzzleAnimation } = usePuzzleAnimation(); + const { getReflectionFlag, setReflectionFlag } = useSunzhongshanStore(); const [showClueHint, setShowClueHint] = useState(false); const [showClueBook, setShowClueBook] = useState(false); @@ -202,7 +204,7 @@ const SunzhongshanReflectionScene3 = () => { const handleVideoEnd = () => { setShowVideo(false); // 检查是否需要显示学习引导 - const hasShownGuide = localStorage.getItem(GUIDE_STORAGE_KEY); + const hasShownGuide = getReflectionFlag(GUIDE_STORAGE_KEY); if (!hasShownGuide) { setShowGuide(true); } @@ -211,7 +213,7 @@ const SunzhongshanReflectionScene3 = () => { // 处理学习引导关闭 const handleGuideClose = () => { setShowGuide(false); - localStorage.setItem(GUIDE_STORAGE_KEY, "true"); + setReflectionFlag(GUIDE_STORAGE_KEY, true); }; // 检查是否所有线索都已收集 @@ -262,7 +264,7 @@ const SunzhongshanReflectionScene3 = () => { const handleCompleteGuideClose = () => { setShowCompleteGuide(false); // 保存场景3完成状态 - localStorage.setItem(`${starName}-reflection-scene3-completed`, "true"); + setReflectionFlag(`${starName}-reflection-scene3-completed`, true); if (moduleName && starName) { navigate(`/${moduleName}/${starName}/home`); } diff --git a/src/store/index.ts b/src/store/index.ts index fc73786..20f8cd4 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,6 @@ export { useChenyunStore } from "./chenyunStore"; export { useHuangyanpeiStore } from "./huangyanpeiStore"; +export { useLuxunStore } from "./luxunStore"; +export { useSunzhongshanStore } from "./sunzhongshanStore"; export { useClueStore } from "./clueStore"; export { usePuzzleStore } from "./puzzleStore"; diff --git a/src/store/luxunStore.ts b/src/store/luxunStore.ts new file mode 100644 index 0000000..40c5914 --- /dev/null +++ b/src/store/luxunStore.ts @@ -0,0 +1,112 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface LuxunStore { + // 存储每个场景(place)的已访问展品ID集合 + // 格式:{ place1: Set<1, 2>, place2: Set<1, 2>, ... } + visitedExhibits: Record>; + + // 标记展品为已访问 + markExhibitVisited: (place: string, exhibitId: number) => void; + + // 检查展品是否已访问 + isExhibitVisited: (place: string, exhibitId: number) => boolean; + + // 获取某个场景已访问的展品ID数组 + getVisitedExhibits: (place: string) => number[]; + + // 重置某个场景的访问记录 + resetPlace: (place: string) => void; + + // 重置所有访问记录 + resetAll: () => void; +} + +// 将 Set 转换为数组以便序列化 +const serializeVisitedExhibits = ( + visitedExhibits: Record> +): Record => { + const serialized: Record = {}; + for (const [place, exhibits] of Object.entries(visitedExhibits)) { + serialized[place] = Array.from(exhibits); + } + return serialized; +}; + +// 将数组转换回 Set +const deserializeVisitedExhibits = ( + serialized: Record | undefined +): Record> => { + if (!serialized || typeof serialized !== "object") { + return {}; + } + const visitedExhibits: Record> = {}; + for (const [place, exhibits] of Object.entries(serialized)) { + if (Array.isArray(exhibits)) { + visitedExhibits[place] = new Set(exhibits); + } else { + visitedExhibits[place] = new Set(); + } + } + return visitedExhibits; +}; + +export const useLuxunStore = create()( + persist( + (set, get) => ({ + visitedExhibits: {}, + + markExhibitVisited: (place: string, exhibitId: number) => { + set((state) => { + const currentExhibits = + state.visitedExhibits[place] || new Set(); + if (!currentExhibits.has(exhibitId)) { + const newExhibits = { + ...state.visitedExhibits, + [place]: new Set([...currentExhibits, exhibitId]), + }; + return { visitedExhibits: newExhibits }; + } + return state; + }); + }, + + isExhibitVisited: (place: string, exhibitId: number) => { + const { visitedExhibits } = get(); + return visitedExhibits[place]?.has(exhibitId) || false; + }, + + getVisitedExhibits: (place: string) => { + const { visitedExhibits } = get(); + return Array.from(visitedExhibits[place] || []); + }, + + resetPlace: (place: string) => { + set((state) => { + const newExhibits = { ...state.visitedExhibits }; + delete newExhibits[place]; + return { visitedExhibits: newExhibits }; + }); + }, + + resetAll: () => { + set({ visitedExhibits: {} }); + }, + }), + { + name: "luxun-storage", + partialize: (state) => ({ + visitedExhibits: serializeVisitedExhibits(state.visitedExhibits), + }), + onRehydrateStorage: () => (state) => { + if (state) { + state.visitedExhibits = deserializeVisitedExhibits( + state.visitedExhibits as any + ); + } + }, + } + ) +); + + diff --git a/src/store/sunzhongshanStore.ts b/src/store/sunzhongshanStore.ts new file mode 100644 index 0000000..fc8f8f2 --- /dev/null +++ b/src/store/sunzhongshanStore.ts @@ -0,0 +1,143 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface SunzhongshanStore { + // 存储每个场景(place)的已访问展品ID集合 + // 格式:{ place1: Set<1, 2>, place2: Set<1, 2>, ... } + visitedExhibits: Record>; + + // 体悟模块通用布尔状态(引导是否展示、场景是否完成等) + reflectionFlags: Record; + + // 标记展品为已访问 + markExhibitVisited: (place: string, exhibitId: number) => void; + + // 检查展品是否已访问 + isExhibitVisited: (place: string, exhibitId: number) => boolean; + + // 获取某个场景已访问的展品ID数组 + getVisitedExhibits: (place: string) => number[]; + + // 重置某个场景的访问记录 + resetPlace: (place: string) => void; + + // 重置所有访问记录 + resetAll: () => void; + + // 设置体悟模块布尔状态 + setReflectionFlag: (key: string, value: boolean) => void; + + // 获取体悟模块布尔状态 + getReflectionFlag: (key: string) => boolean; +} + +// 将 Set 转换为数组以便序列化 +const serializeVisitedExhibits = ( + visitedExhibits: Record> +): Record => { + const serialized: Record = {}; + for (const [place, exhibits] of Object.entries(visitedExhibits)) { + serialized[place] = Array.from(exhibits); + } + return serialized; +}; + +// 将数组转换回 Set +const deserializeVisitedExhibits = ( + serialized: Record | undefined +): Record> => { + if (!serialized || typeof serialized !== "object") { + return {}; + } + const visitedExhibits: Record> = {}; + for (const [place, exhibits] of Object.entries(serialized)) { + if (Array.isArray(exhibits)) { + visitedExhibits[place] = new Set(exhibits); + } else { + visitedExhibits[place] = new Set(); + } + } + return visitedExhibits; +}; + +export const useSunzhongshanStore = create()( + persist( + (set, get) => ({ + visitedExhibits: {}, + reflectionFlags: {}, + + markExhibitVisited: (place: string, exhibitId: number) => { + set((state) => { + const currentExhibits = + state.visitedExhibits[place] || new Set(); + if (!currentExhibits.has(exhibitId)) { + const newExhibits = { + ...state.visitedExhibits, + [place]: new Set([...currentExhibits, exhibitId]), + }; + return { visitedExhibits: newExhibits }; + } + return state; + }); + }, + + isExhibitVisited: (place: string, exhibitId: number) => { + const { visitedExhibits } = get(); + return visitedExhibits[place]?.has(exhibitId) || false; + }, + + getVisitedExhibits: (place: string) => { + const { visitedExhibits } = get(); + return Array.from(visitedExhibits[place] || []); + }, + + resetPlace: (place: string) => { + set((state) => { + const newExhibits = { ...state.visitedExhibits }; + delete newExhibits[place]; + return { visitedExhibits: newExhibits }; + }); + }, + + resetAll: () => { + set({ visitedExhibits: {}, reflectionFlags: {} }); + }, + + setReflectionFlag: (key: string, value: boolean) => { + set((state) => ({ + reflectionFlags: { + ...state.reflectionFlags, + [key]: value, + }, + })); + }, + + getReflectionFlag: (key: string) => { + const { reflectionFlags } = get(); + return !!reflectionFlags[key]; + }, + }), + { + name: "sunzhongshan-storage", + partialize: (state) => ({ + visitedExhibits: serializeVisitedExhibits(state.visitedExhibits), + reflectionFlags: state.reflectionFlags, + }), + onRehydrateStorage: () => (state) => { + if (state) { + state.visitedExhibits = deserializeVisitedExhibits( + state.visitedExhibits as any + ); + if ( + !state.reflectionFlags || + typeof state.reflectionFlags !== "object" + ) { + state.reflectionFlags = {}; + } + } + }, + } + ) +); + +