diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b0d4783..c7cdc5b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -45,7 +45,15 @@ "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 show main:src/pages/zhangleping/home.tsx)", "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 show main:src/store/storeNames.ts)", "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"getUserDefine\" main -- src/pages/zhangleping src/store/zhanglepingStore.ts src/App.tsx)", - "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"setDrawArtwork\\\\|clearDrawArtwork\\\\|drawArtworkDataUrl\" main -- src)" + "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"setDrawArtwork\\\\|clearDrawArtwork\\\\|drawArtworkDataUrl\" main -- src)", + "Bash(git grep *)", + "Bash(git commit -m ' *)", + "Bash(bun ls-files *)", + "Bash(bunx prettier *)", + "Bash(awk 'NR>=216 && NR<=380' src/config/puzzleData.ts)", + "Bash(awk 'NR>=380 && NR<=540' src/config/puzzleData.ts)", + "Bash(awk '/^worktree / {wt=$2} /^branch / {br=$2; if \\(wt ~ /\\\\.claude\\\\/worktrees\\\\/agent-/\\) print wt\"\\\\t\"br}')", + "Read(//tmp/**)" ], "deny": [ "Read(public/**)", diff --git a/src/App.tsx b/src/App.tsx index 188964f..7759780 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,16 +38,19 @@ function AppContent() { ;(async () => { try { const raw = await getUserDefine(PUZZLE_LESSON_ID, PUZZLE_KEY) - if (!raw) return - const data = JSON.parse(raw) as { puzzles?: Record } - if (data.puzzles) { - usePuzzleStore.setState((prev) => ({ - ...prev, - puzzles: deserializePuzzles(data.puzzles), - })) + if (raw) { + const data = JSON.parse(raw) as { puzzles?: Record } + if (data.puzzles) { + usePuzzleStore.setState((prev) => ({ + ...prev, + puzzles: deserializePuzzles(data.puzzles), + })) + } } } catch (e) { console.error('Failed to load puzzle store', e) + } finally { + usePuzzleStore.getState().setLoaded(true) } })() }, []) diff --git a/src/assets/images/entrance/孙中山故居入口-未解锁.png b/src/assets/images/entrance/孙中山-未解锁.png similarity index 100% rename from src/assets/images/entrance/孙中山故居入口-未解锁.png rename to src/assets/images/entrance/孙中山-未解锁.png diff --git a/src/assets/images/entrance/孙中山故居入口-解锁.png b/src/assets/images/entrance/孙中山-解锁.png similarity index 100% rename from src/assets/images/entrance/孙中山故居入口-解锁.png rename to src/assets/images/entrance/孙中山-解锁.png diff --git a/src/assets/images/entrance/张乐平暗.png b/src/assets/images/entrance/张乐平-未解锁.png similarity index 100% rename from src/assets/images/entrance/张乐平暗.png rename to src/assets/images/entrance/张乐平-未解锁.png diff --git a/src/assets/images/entrance/张乐平亮.png b/src/assets/images/entrance/张乐平-解锁.png similarity index 100% rename from src/assets/images/entrance/张乐平亮.png rename to src/assets/images/entrance/张乐平-解锁.png diff --git a/src/assets/images/entrance/毛泽东故居入口.png b/src/assets/images/entrance/毛泽东-解锁.png similarity index 100% rename from src/assets/images/entrance/毛泽东故居入口.png rename to src/assets/images/entrance/毛泽东-解锁.png diff --git a/src/assets/images/entrance/聂耳暗.png b/src/assets/images/entrance/聂耳-未解锁.png similarity index 100% rename from src/assets/images/entrance/聂耳暗.png rename to src/assets/images/entrance/聂耳-未解锁.png diff --git a/src/assets/images/entrance/聂耳亮.png b/src/assets/images/entrance/聂耳-解锁.png similarity index 100% rename from src/assets/images/entrance/聂耳亮.png rename to src/assets/images/entrance/聂耳-解锁.png diff --git a/src/assets/images/entrance/陈云故居-未解锁.png b/src/assets/images/entrance/陈云-未解锁.png similarity index 100% rename from src/assets/images/entrance/陈云故居-未解锁.png rename to src/assets/images/entrance/陈云-未解锁.png diff --git a/src/assets/images/entrance/陈云故居-解锁.png b/src/assets/images/entrance/陈云-解锁.png similarity index 100% rename from src/assets/images/entrance/陈云故居-解锁.png rename to src/assets/images/entrance/陈云-解锁.png diff --git a/src/assets/images/entrance/鲁迅故居-未解锁.png b/src/assets/images/entrance/鲁迅-未解锁.png similarity index 100% rename from src/assets/images/entrance/鲁迅故居-未解锁.png rename to src/assets/images/entrance/鲁迅-未解锁.png diff --git a/src/assets/images/entrance/鲁迅故居-解锁.png b/src/assets/images/entrance/鲁迅-解锁.png similarity index 100% rename from src/assets/images/entrance/鲁迅故居-解锁.png rename to src/assets/images/entrance/鲁迅-解锁.png diff --git a/src/assets/images/entrance/黄元培-未解锁.png b/src/assets/images/entrance/黄炎培-未解锁.png similarity index 100% rename from src/assets/images/entrance/黄元培-未解锁.png rename to src/assets/images/entrance/黄炎培-未解锁.png diff --git a/src/assets/images/entrance/黄元培-解锁.png b/src/assets/images/entrance/黄炎培-解锁.png similarity index 100% rename from src/assets/images/entrance/黄元培-解锁.png rename to src/assets/images/entrance/黄炎培-解锁.png diff --git a/src/assets/images/menu/文化完成.png b/src/assets/images/menu/文化完成.png new file mode 100644 index 0000000..f377935 Binary files /dev/null and b/src/assets/images/menu/文化完成.png differ diff --git a/src/assets/images/menu/经济完成.png b/src/assets/images/menu/经济完成.png new file mode 100644 index 0000000..89af00d Binary files /dev/null and b/src/assets/images/menu/经济完成.png differ diff --git a/src/assets/images/menu/艺术完成.png b/src/assets/images/menu/艺术完成.png new file mode 100644 index 0000000..20e1b7a Binary files /dev/null and b/src/assets/images/menu/艺术完成.png differ diff --git a/src/assets/images/menu/革命完成.png b/src/assets/images/menu/革命完成.png new file mode 100644 index 0000000..84a6120 Binary files /dev/null and b/src/assets/images/menu/革命完成.png differ diff --git a/src/assets/images/puzzle/home/底图.png b/src/assets/images/puzzle/home/底图.png index b3083cc..d5a4425 100644 Binary files a/src/assets/images/puzzle/home/底图.png and b/src/assets/images/puzzle/home/底图.png differ diff --git a/src/assets/images/puzzle/home/张乐平.png b/src/assets/images/puzzle/home/张乐平.png index d25a52e..669bf27 100644 Binary files a/src/assets/images/puzzle/home/张乐平.png and b/src/assets/images/puzzle/home/张乐平.png differ diff --git a/src/assets/images/puzzle/home/聂耳.png b/src/assets/images/puzzle/home/聂耳.png index 806aeeb..4075cae 100644 Binary files a/src/assets/images/puzzle/home/聂耳.png and b/src/assets/images/puzzle/home/聂耳.png differ diff --git a/src/assets/images/puzzle/home/荣德生.png b/src/assets/images/puzzle/home/荣德生.png index ab9b1b4..f12cece 100644 Binary files a/src/assets/images/puzzle/home/荣德生.png and b/src/assets/images/puzzle/home/荣德生.png differ diff --git a/src/assets/images/puzzle/home/追光入口.png b/src/assets/images/puzzle/home/追光入口.png new file mode 100644 index 0000000..1f85749 Binary files /dev/null and b/src/assets/images/puzzle/home/追光入口.png differ diff --git a/src/components/exhibit/ExhibitDescriptionNarration.tsx b/src/components/exhibit/ExhibitDescriptionNarration.tsx index 7b50318..122146a 100644 --- a/src/components/exhibit/ExhibitDescriptionNarration.tsx +++ b/src/components/exhibit/ExhibitDescriptionNarration.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { cn, getBackgroundImage } from '@/lib/utils' import { AnimatePresence, motion } from 'framer-motion' import { useAudioManager } from '@/hooks/useAudioManager' @@ -40,19 +40,35 @@ const ExhibitDescriptionNarration = ({ // 当前组件对应的音频是否正在播放 const isCurrentAudioPlaying = audioSrc && currentSrc === audioSrc && isPlaying + // 标记本实例是否发起过播放,用于在不可见/卸载时可靠停止 + // 不依赖 currentSrc,避免 audio.play() Promise 未 resolve 前关闭弹窗导致漏停 + const playInitiatedRef = useRef(false) + // 自动播放音频 useEffect(() => { if (visible && autoPlay && audioSrc) { + playInitiatedRef.current = true play(audioSrc) } }, [visible, autoPlay, audioSrc, play]) - // 组件不可见时停止音频(仅当当前播放的是该组件的 audioSrc) + // 组件不可见时停止音频 useEffect(() => { - if (!visible && audioSrc && currentSrc === audioSrc) { + if (!visible && playInitiatedRef.current) { stop() + playInitiatedRef.current = false } - }, [visible, audioSrc, currentSrc, stop]) + }, [visible, stop]) + + // 组件卸载时兜底停止 + useEffect(() => { + return () => { + if (playInitiatedRef.current) { + stop() + playInitiatedRef.current = false + } + } + }, [stop]) if (!text) return null @@ -98,7 +114,10 @@ const ExhibitDescriptionNarration = ({
{!isCurrentAudioPlaying && (
diff --git a/src/components/layout/ResidenceLayout.tsx b/src/components/layout/ResidenceLayout.tsx index b245226..63cd12a 100644 --- a/src/components/layout/ResidenceLayout.tsx +++ b/src/components/layout/ResidenceLayout.tsx @@ -136,10 +136,17 @@ const ResidenceLayoutContent = () => { return true }, - [ANIMATION_CONFIG.FLY_DURATION, ANIMATION_CONFIG.OPACITY, ANIMATION_CONFIG.SCALE, cleanupAnimation, puzzleBoxRef, residence, unlockPuzzle] + [ + ANIMATION_CONFIG.FLY_DURATION, + ANIMATION_CONFIG.OPACITY, + ANIMATION_CONFIG.SCALE, + cleanupAnimation, + puzzleBoxRef, + residence, + unlockPuzzle, + ] ) - // 等待元素准备就绪并执行动画 const waitForElementsAndAnimate = useCallback( async (pieceId: number, retryCount = 0): Promise => { @@ -165,7 +172,15 @@ const ResidenceLayoutContent = () => { unlockPuzzle(residence, pieceId) cleanupAnimation() }, - [ANIMATION_CONFIG.MAX_RETRIES, ANIMATION_CONFIG.RETRY_DELAY, cleanupAnimation, executeFlyAnimation, puzzleBoxRef, residence, unlockPuzzle] + [ + ANIMATION_CONFIG.MAX_RETRIES, + ANIMATION_CONFIG.RETRY_DELAY, + cleanupAnimation, + executeFlyAnimation, + puzzleBoxRef, + residence, + unlockPuzzle, + ] ) // 处理碎片动画触发(仅支持拼图功能的故居) @@ -215,7 +230,14 @@ const ResidenceLayoutContent = () => { }) }) }, - [ANIMATION_CONFIG.APPEAR_DURATION, ANIMATION_CONFIG.GLOW_DURATION, hasPuzzleFeature, isPuzzleUnlocked, residence, waitForElementsAndAnimate] + [ + ANIMATION_CONFIG.APPEAR_DURATION, + ANIMATION_CONFIG.GLOW_DURATION, + hasPuzzleFeature, + isPuzzleUnlocked, + residence, + waitForElementsAndAnimate, + ] ) // 获取当前路由的视频路径 diff --git a/src/components/letter/LetterTemplate.tsx b/src/components/letter/LetterTemplate.tsx index 52c7fd9..339d47c 100644 --- a/src/components/letter/LetterTemplate.tsx +++ b/src/components/letter/LetterTemplate.tsx @@ -442,19 +442,28 @@ const LetterTemplate = ({
e.stopPropagation()} - style={{ perspective: '1200px' }} + style={{ + perspective: '1200px', + WebkitPerspective: '1200px', + }} >
@@ -510,8 +519,10 @@ const LetterTemplate = ({
diff --git a/src/components/puzzle/MenuPuzzleCenter.tsx b/src/components/puzzle/MenuPuzzleCenter.tsx index cd9a4bf..a458d79 100644 --- a/src/components/puzzle/MenuPuzzleCenter.tsx +++ b/src/components/puzzle/MenuPuzzleCenter.tsx @@ -204,7 +204,7 @@ const MenuPuzzleCenter = ({ /> )} 导图 { diff --git a/src/config/progression.ts b/src/config/progression.ts new file mode 100644 index 0000000..ba95b58 --- /dev/null +++ b/src/config/progression.ts @@ -0,0 +1,79 @@ +import { puzzleDataConfig } from './puzzleData' +import { PUZZLE_RESIDENCES } from './puzzleResidences' + +export type PuzzleProgress = Record> + +const getResidenceIndex = (residence: string) => + PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence) + +const getConfiguredPieceIds = (residence: string) => { + const pieces = puzzleDataConfig[residence] + if (!pieces) return [] + return Object.keys(pieces).map(Number) +} + +const arePiecesUnlocked = (residence: string, puzzles: PuzzleProgress, pieceIds: number[]) => { + if (pieceIds.length === 0) return false + const unlockedPieces = puzzles[residence] + if (!unlockedPieces) return false + return pieceIds.every((pieceId) => unlockedPieces.has(pieceId)) +} + +export const isResidenceStarted = (residence: string, puzzles: PuzzleProgress) => { + return (puzzles[residence]?.size ?? 0) > 0 +} + +export const isResidenceComplete = (residence: string, puzzles: PuzzleProgress) => { + return arePiecesUnlocked(residence, puzzles, getConfiguredPieceIds(residence)) +} + +export const isResidenceUnlocked = (residence: string, puzzles: PuzzleProgress) => { + const index = getResidenceIndex(residence) + if (index === 0) return true + if (index < 0) return false + + const previousResidence = PUZZLE_RESIDENCES[index - 1] + if (!previousResidence) return false + return isResidenceComplete(previousResidence.residence, puzzles) +} + +export const isMenuModuleUnlocked = (moduleName: string, puzzles: PuzzleProgress) => { + return PUZZLE_RESIDENCES.some( + (item) => item.moduleName === moduleName && isResidenceUnlocked(item.residence, puzzles) + ) +} + +export const isMenuModuleStarted = (moduleName: string, puzzles: PuzzleProgress) => { + return PUZZLE_RESIDENCES.some( + (item) => item.moduleName === moduleName && isResidenceStarted(item.residence, puzzles) + ) +} + +export const isMenuModuleComplete = (moduleName: string, puzzles: PuzzleProgress) => { + const residences = PUZZLE_RESIDENCES.filter((item) => item.moduleName === moduleName) + if (residences.length === 0) return false + return residences.every((item) => isResidenceComplete(item.residence, puzzles)) +} + +const getPieceIdsByRoute = (residence: string, routePrefix: string) => { + const pieces = puzzleDataConfig[residence] + if (!pieces) return [] + + return Object.entries(pieces) + .filter(([, piece]) => piece.route.startsWith(routePrefix)) + .map(([pieceId]) => Number(pieceId)) +} + +export const getResidenceStageUnlocks = (residence: string, puzzles: PuzzleProgress) => { + const residenceUnlocked = isResidenceUnlocked(residence, puzzles) + + return { + dialogueUnlocked: residenceUnlocked, + reflectionUnlocked: + residenceUnlocked && + arePiecesUnlocked(residence, puzzles, getPieceIdsByRoute(residence, 'place')), + internalizationUnlocked: + residenceUnlocked && + arePiecesUnlocked(residence, puzzles, getPieceIdsByRoute(residence, 'reflection')), + } +} diff --git a/src/lib/utils/api-common.ts b/src/lib/utils/api-common.ts index d6e5fe8..d471aab 100644 --- a/src/lib/utils/api-common.ts +++ b/src/lib/utils/api-common.ts @@ -9,6 +9,11 @@ declare function getFutureDa(days: number): string declare function getCurrentDa(str: string): void declare function formatDateFromMilliseconds(date: Date): string +// 本地或非易班环境下,外部全局变量可能未注入;统一用安全访问 +const isYibanRuntime = (): boolean => { + return typeof iscallyb !== 'undefined' && iscallyb === true +} + interface UserDefineParam { lessonId: string key?: string | number @@ -72,8 +77,8 @@ $.extend({ const LESSON_PREFIX = 'hsgrl' -const videoajax: VideoAjax = videoAjax() -videoajax.setHost('/') +const videoajax: VideoAjax | null = typeof videoAjax !== 'undefined' ? videoAjax() : null +videoajax?.setHost('/') function normalizeLessonId(lessonId: string | number): string { const raw = String(lessonId) @@ -82,7 +87,7 @@ function normalizeLessonId(lessonId: string | number): string { // 设置我的自定义数据(所有的自定义数据都设置在第一个课时ID上,以key来区分不同的类型) function setLtDefine(key: string | number, value: string): void { - if (iscallyb) { + if (isYibanRuntime()) { setUserDefine(1, key, value) } else { sessionStorage.setItem(`${LESSON_PREFIX}_${key}`, value) @@ -93,7 +98,7 @@ let mycstr9 = '' // 获取定义数据(由于底层是异步接口,这里返回 Promise) async function getLtDefine(key: string | number): Promise { - if (iscallyb) { + if (isYibanRuntime()) { if (mycstr9 === '') { const all = await getUsersDefine(1, emykey) if (all === undefined || all === null || all === 'null' || all === '') { @@ -137,6 +142,11 @@ export function setUserDefine( key: string | number, value: string ): void { + if (!isYibanRuntime() || !videoajax) { + sessionStorage.setItem(`${normalizeLessonId(lessonId)}_${key}`, value) + return + } + const param: UserDefineParam = { lessonId: normalizeLessonId(lessonId), key, @@ -150,14 +160,20 @@ function getUserDefineAsyncInternal( lessonId: string | number, key: string | number ): Promise { + if (!isYibanRuntime() || !videoajax) { + const value = sessionStorage.getItem(`${normalizeLessonId(lessonId)}_${key}`) + return Promise.resolve(value === null || value === 'null' ? '' : value) + } + const param: UserDefineParam = { lessonId: normalizeLessonId(lessonId), order: key, } + const ajax = videoajax return new Promise((resolve) => { let content = '' - videoajax.getUserDefineInfoByUser(param, (res) => { + ajax.getUserDefineInfoByUser(param, (res) => { // 保留原有 console 行为 console.log(res) if (res && res.code === 200) { @@ -185,14 +201,19 @@ function getUsersDefineAsyncInternal( lessonId: string | number, key: string | number ): Promise { + if (!isYibanRuntime() || !videoajax) { + return Promise.resolve('') + } + const param: UserDefineParam = { lessonId: normalizeLessonId(lessonId), order: key, } + const ajax = videoajax return new Promise((resolve) => { let content: string | VideoAjaxResponseItem[] = '' - videoajax.getUserDefineInfo(param, (data) => { + ajax.getUserDefineInfo(param, (data) => { if (data && data.code === 200) { const json = (data.data.value || []) as VideoAjaxResponseItem[] if (json.length > 1) { @@ -232,7 +253,13 @@ export async function setCourseTimeProgress(order: number): Promise { // var courseid = getCookie(lesson+"_course_id");//获取url链接的课程ID // const courseid = await getLtDefine('10000') console.info(`courseid:${courseid} order:${order}`) - if (iscallyb && courseid !== undefined && courseid !== null && courseid !== '') { + if ( + isYibanRuntime() && + videoajax && + courseid !== undefined && + courseid !== null && + courseid !== '' + ) { if (order > 0) { const param2 = { order, co: courseid, process: 100 } videoajax.finishCourse(param2) diff --git a/src/pages/Menu.tsx b/src/pages/Menu.tsx index defe434..aadc3e5 100644 --- a/src/pages/Menu.tsx +++ b/src/pages/Menu.tsx @@ -4,11 +4,15 @@ import { Image, PuzzleSystem } from '@/components' import { useNavigate } from 'react-router-dom' import MenuPuzzleCenter from '@/components/puzzle/MenuPuzzleCenter' import { DEFAULT_PUZZLE_RESIDENCE } from '@/config/puzzleResidences' +import { usePuzzleStore } from '@/store/puzzleStore' +import { isMenuModuleComplete } from '@/config/progression' const Menu = () => { const navigate = useNavigate() const [isPuzzleCenterVisible, setIsPuzzleCenterVisible] = useState(false) const [activeResidence, setActiveResidence] = useState(null) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) /** * 类型:1:革命先驱、文化民人、艺术名家、经济巨擘 @@ -84,23 +88,41 @@ const Menu = () => { )}
- {Category.map((item) => ( -
navigate(`/entrance/${encodeURIComponent(item.moduleName)}`)} - > -
- + {Category.map((item) => { + const isComplete = isLoaded && isMenuModuleComplete(item.moduleName, puzzles) + const completeImageMap: Record = { + revolutionary: 'menu/革命完成', + cultural: 'menu/文化完成', + artistic: 'menu/艺术完成', + economic: 'menu/经济完成', + } -
- - + return ( +
navigate(`/entrance/${encodeURIComponent(item.moduleName)}`)} + > +
+ + +
+ {isComplete ? ( + + ) : ( +
+ + +
+ )} +
-
- ))} + ) + })}
{/* 学习地图 */} diff --git a/src/pages/chenyun/home.tsx b/src/pages/chenyun/home.tsx index d9d9561..89dfe5c 100644 --- a/src/pages/chenyun/home.tsx +++ b/src/pages/chenyun/home.tsx @@ -2,6 +2,8 @@ import { useEffect } from 'react' import { getBackgroundImage, getStaticImage } from '@/lib/utils' import { useNavigate, useParams } from 'react-router-dom' import { Image } from '@/components' +import { usePuzzleStore } from '@/store/puzzleStore' +import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression' import { getUserDefine } from '@/lib/utils/api-common' import { useChenyunStore, CHENYUN_LESSON_ID, CHENYUN_KEY } from '@/store/chenyunStore' @@ -27,6 +29,17 @@ const Chenyun = () => { const lastVisitedPlace = useChenyunStore((state) => state.lastVisitedPlace) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) + const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } = + getResidenceStageUnlocks(starName, puzzles) + + useEffect(() => { + if (isLoaded && !isResidenceUnlocked(starName, puzzles)) { + navigate('/menu', { replace: true }) + } + }, [isLoaded, navigate, puzzles, starName]) + useEffect(() => { ;(async () => { try { @@ -77,21 +90,35 @@ const Chenyun = () => {
{/* 对话 */} navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)} + onClick={() => { + if (!dialogueUnlocked) return + navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`) + }} /> {/* 体悟 */} navigate(`/${moduleName}/${starName}/reflection`)} + onClick={() => { + if (!reflectionUnlocked) return + navigate(`/${moduleName}/${starName}/reflection`) + }} /> {/* 内化 */} navigate(`/${moduleName}/${starName}/internalization`)} + onClick={() => { + if (!internalizationUnlocked) return + navigate(`/${moduleName}/${starName}/internalization`) + }} />
diff --git a/src/pages/chenyun/internalization.tsx b/src/pages/chenyun/internalization.tsx index 7edf43b..8d5e53d 100644 --- a/src/pages/chenyun/internalization.tsx +++ b/src/pages/chenyun/internalization.tsx @@ -378,6 +378,7 @@ const ChenyunInternalization = () => { { setNarrationVisible(false) setVisible(true) diff --git a/src/pages/chenyun/place/place1.tsx b/src/pages/chenyun/place/place1.tsx index d5e5a95..c136836 100644 --- a/src/pages/chenyun/place/place1.tsx +++ b/src/pages/chenyun/place/place1.tsx @@ -9,7 +9,7 @@ import { } from '@/components' import { useNavigate, useParams } from 'react-router-dom' import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' -import { useState, useEffect } from 'react' +import { useState } from 'react' import Cursor from '@/components/cursor' import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext' import { useChenyunStore, saveChenyunStore } from '@/store/chenyunStore' diff --git a/src/pages/entrance/index.tsx b/src/pages/entrance/index.tsx index 5b60373..dcd4d34 100644 --- a/src/pages/entrance/index.tsx +++ b/src/pages/entrance/index.tsx @@ -1,11 +1,14 @@ import { getBackgroundImage, getStaticImage } from '@/lib/utils' import { useNavigate, useParams } from 'react-router-dom' import { Image } from '@/components' +import { isResidenceStarted, isResidenceUnlocked } from '@/config/progression' +import { usePuzzleStore } from '@/store/puzzleStore' //英文命名 "革命先驱" | "文化名人" | "艺术名家" | "经济巨擘" type ModuleName = 'revolutionary' | 'cultural' | 'artistic' | 'economic' +type EntranceActionState = 'locked' | 'unvisited' | 'visited' +type EntranceSide = 'left' | 'right' interface Star { - image: string name: string starName: string } @@ -19,6 +22,36 @@ interface EntranceBackground { } } +const entranceActionConfig: Record< + EntranceActionState, + { + image: string + position: Record + } +> = { + locked: { + image: 'entrance/暂未解锁', + position: { + left: 'absolute left-95 bottom-58', + right: 'absolute right-106 bottom-58', + }, + }, + unvisited: { + image: 'entrance/点击前往浏览', + position: { + left: 'absolute left-90.5 bottom-37', + right: 'absolute right-90.5 bottom-37', + }, + }, + visited: { + image: 'entrance/该故居已访问', + position: { + left: 'absolute left-95 bottom-58', + right: 'absolute right-102 bottom-58', + }, + }, +} + /** * 故居入口页 * 根据模块名称显示不同的背景图 @@ -34,12 +67,10 @@ const EntranceBackgroundMap: Record = { backgroundImage: getBackgroundImage('entrance/革命先驱'), Star: { Star1: { - image: getStaticImage('entrance/毛泽东故居入口'), name: '毛泽东', starName: 'maozedong', }, Star2: { - image: getStaticImage('entrance/孙中山故居入口-解锁'), name: '孙中山', starName: 'sunzhongshan', }, @@ -49,12 +80,10 @@ const EntranceBackgroundMap: Record = { backgroundImage: getBackgroundImage('entrance/文教名人'), Star: { Star1: { - image: getStaticImage('entrance/鲁迅故居-解锁'), name: '鲁迅', starName: 'luxun', }, Star2: { - image: getStaticImage('entrance/黄元培-解锁'), name: '黄炎培', starName: 'huangyanpei', }, @@ -64,12 +93,10 @@ const EntranceBackgroundMap: Record = { backgroundImage: getBackgroundImage('entrance/艺术名家'), Star: { Star1: { - image: getStaticImage('entrance/聂耳亮'), name: '聂耳', starName: 'nieer', }, Star2: { - image: getStaticImage('entrance/张乐平亮'), name: '张乐平', starName: 'zhangleping', }, @@ -79,12 +106,10 @@ const EntranceBackgroundMap: Record = { backgroundImage: getBackgroundImage('entrance/经济巨擎'), Star: { Star1: { - image: getStaticImage('entrance/荣德生-解锁'), name: '荣德生', starName: 'rongdesheng', }, Star2: { - image: getStaticImage('entrance/陈云故居-解锁'), name: '陈云', starName: 'chenyun', }, @@ -95,12 +120,39 @@ const EntranceBackgroundMap: Record = { const Entrance = () => { const { moduleName } = useParams() const navigate = useNavigate() + const puzzles = usePuzzleStore((state) => state.puzzles) // const decodedModuleName = decodeURIComponent(moduleName || ""); const { backgroundImage, Star } = EntranceBackgroundMap[moduleName as ModuleName] || {} if (!Star) { return null } const { Star1, Star2 } = Star + const isStar1Unlocked = isResidenceUnlocked(Star1.starName, puzzles) + const isStar2Unlocked = isResidenceUnlocked(Star2.starName, puzzles) + const isStar1Started = isResidenceStarted(Star1.starName, puzzles) + const isStar2Started = isResidenceStarted(Star2.starName, puzzles) + const getStarImage = (star: Star, isUnlocked: boolean) => + getStaticImage(`entrance/${star.name}-${isUnlocked ? '解锁' : '未解锁'}`) + const getEntranceActionState = (isUnlocked: boolean, isStarted: boolean): EntranceActionState => { + if (!isUnlocked) return 'locked' + return isStarted ? 'visited' : 'unvisited' + } + const getEntranceAction = (side: EntranceSide, state: EntranceActionState) => { + const config = entranceActionConfig[state] + return { + className: config.position[side], + image: config.image, + } + } + const star1Action = getEntranceAction( + 'left', + getEntranceActionState(isStar1Unlocked, isStar1Started) + ) + const star2Action = getEntranceAction( + 'right', + getEntranceActionState(isStar2Unlocked, isStar2Started) + ) + return (
{ onClick={() => navigate('/menu')} /> {/* 名人1 */} - - {Star1.image && ( - navigate(`/${moduleName}/${Star1.starName}/home`)} - /> - )} + + { + if (!isStar1Unlocked) return + navigate(`/${moduleName}/${Star1.starName}/home`) + }} + /> {/* 名人2 */} + { + if (!isStar2Unlocked) return + navigate(`/${moduleName}/${Star2.starName}/home`) + }} /> - {Star2.image && ( - navigate(`/${moduleName}/${Star2.starName}/home`)} - /> - )} {/* { const lastVisitedPlace = useHuangyanpeiStore((state) => state.lastVisitedPlace) const reflectionRoute = useHuangyanpeiStore((state) => state.reflectionRoute) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) + const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } = + getResidenceStageUnlocks(starName, puzzles) + + useEffect(() => { + if (isLoaded && !isResidenceUnlocked(starName, puzzles)) { + navigate('/menu', { replace: true }) + } + }, [isLoaded, navigate, puzzles, starName]) + useEffect(() => { ;(async () => { try { @@ -72,21 +85,35 @@ const Huangyanpei = () => {
{/* 对话 */} navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)} + onClick={() => { + if (!dialogueUnlocked) return + navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`) + }} /> {/* 体悟 */} navigate(`/${moduleName}/${starName}/${reflectionRoute || 'reflection'}`)} + onClick={() => { + if (!reflectionUnlocked) return + navigate(`/${moduleName}/${starName}/${reflectionRoute || 'reflection'}`) + }} /> {/* 内化 */} navigate(`/${moduleName}/${starName}/internalization`)} + onClick={() => { + if (!internalizationUnlocked) return + navigate(`/${moduleName}/${starName}/internalization`) + }} />
diff --git a/src/pages/huangyanpei/reflection/scene2.tsx b/src/pages/huangyanpei/reflection/scene2.tsx index fa867e5..738b242 100644 --- a/src/pages/huangyanpei/reflection/scene2.tsx +++ b/src/pages/huangyanpei/reflection/scene2.tsx @@ -163,6 +163,7 @@ const HuangyanpeiReflectionScene2 = () => { visible={showGuideModal} onContinue={() => setShowGuideModal(false)} audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_24.mp3')} + firstLineIndent="2em" > 欢迎来到浦东中学!{'\n'} 这是近代上海第一所完全中学,黄炎培先生当首任校长,在这里能学到更多实用知识,还能树立成才的理想哦。 diff --git a/src/pages/huangyanpei/reflection/scene3.tsx b/src/pages/huangyanpei/reflection/scene3.tsx index 2a61b6a..1c4431b 100644 --- a/src/pages/huangyanpei/reflection/scene3.tsx +++ b/src/pages/huangyanpei/reflection/scene3.tsx @@ -106,7 +106,7 @@ const HuangyanpeiReflectionScene3 = () => { > 终于到中华职业学校啦!这里是中国近代职教的摇篮,“免学费、包分配”,还能学真手艺,快来练就一技之长。 {'\n'} -         你选了学技术,那就来挑战制作中华职校的“明星产品”——台钳吧!当年这台钳还拿过巴拿马万国博览会金奖呢! + 你选了学技术,那就来挑战制作中华职校的“明星产品”——台钳吧!当年这台钳还拿过巴拿马万国博览会金奖呢! {/* 互动展品光标:组装台钳互动 */} @@ -173,7 +173,7 @@ const HuangyanpeiReflectionScene3 = () => { { setShowSuccessAlert(false) markReflectionCursorVisited(scene, 2) diff --git a/src/pages/huangyanpei/reflection/scene4.tsx b/src/pages/huangyanpei/reflection/scene4.tsx index d4d2d35..6da31c4 100644 --- a/src/pages/huangyanpei/reflection/scene4.tsx +++ b/src/pages/huangyanpei/reflection/scene4.tsx @@ -209,7 +209,7 @@ const HuangyanpeiReflectionScene4 = () => { > 终于到中华职业学校啦!这里是中国近代职教的摇篮,“免学费、包分配”,还能学真手艺,快来练就一技之长。 {'\n'} -         现在要完成你的第一个任务:推销中华职校生产的“国货精品”——手工纽扣和纯棉毛巾!当年学生们就是带着这些国货,走街串巷做推销呢! + 现在要完成你的第一个任务:推销中华职校生产的“国货精品”——手工纽扣和纯棉毛巾!当年学生们就是带着这些国货,走街串巷做推销呢! {showVideo && ( diff --git a/src/pages/luxun/home.tsx b/src/pages/luxun/home.tsx index 2add52f..025cb19 100644 --- a/src/pages/luxun/home.tsx +++ b/src/pages/luxun/home.tsx @@ -2,6 +2,8 @@ import { useEffect } from 'react' import { getBackgroundImage, getStaticImage } from '@/lib/utils' import { useNavigate, useParams } from 'react-router-dom' import { Image } from '@/components' +import { usePuzzleStore } from '@/store/puzzleStore' +import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression' import { getUserDefine } from '@/lib/utils/api-common' import { useLuxunStore, LUXUN_LESSON_ID, LUXUN_KEY } from '@/store/luxunStore' @@ -28,6 +30,17 @@ const Luxun = () => { const lastVisitedPlace = useLuxunStore((state) => state.lastVisitedPlace) const reflectionRoute = useLuxunStore((state) => state.reflectionRoute) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) + const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } = + getResidenceStageUnlocks(starName, puzzles) + + useEffect(() => { + if (isLoaded && !isResidenceUnlocked(starName, puzzles)) { + navigate('/menu', { replace: true }) + } + }, [isLoaded, navigate, puzzles, starName]) + useEffect(() => { ;(async () => { try { @@ -78,21 +91,35 @@ const Luxun = () => {
{/* 对话 */} navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)} + onClick={() => { + if (!dialogueUnlocked) return + navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`) + }} /> {/* 体悟 */} navigate(`/${moduleName}/${starName}/${reflectionRoute || 'reflection'}`)} + onClick={() => { + if (!reflectionUnlocked) return + navigate(`/${moduleName}/${starName}/${reflectionRoute || 'reflection'}`) + }} /> {/* 内化 */} navigate(`/${moduleName}/${starName}/internalization`)} + onClick={() => { + if (!internalizationUnlocked) return + navigate(`/${moduleName}/${starName}/internalization`) + }} />
diff --git a/src/pages/luxun/place/place1.tsx b/src/pages/luxun/place/place1.tsx index 58f5769..556f83a 100644 --- a/src/pages/luxun/place/place1.tsx +++ b/src/pages/luxun/place/place1.tsx @@ -235,6 +235,7 @@ const LuxunPlace1 = () => { visible={showAfterDialogGuide} onContinue={() => setShowAfterDialogGuide(false)} audioSrc={getPublicPath('audio/luxun/mp3_luxun_01.mp3')} + firstLineIndent="2em" > {`同学你好,欢迎来到“走进故居·对话”学习模块! 此刻你正立在鲁迅故居的一楼客厅里,接下来请随旧时光的纹路,一同翻开先生思想长卷里的5段深刻轨迹:风雨前夕的思想淬炼、杂文集里的匕首与投枪、历史新编中的现实呐喊、为亡友编纂的精神丰碑、6平方米的红色庇护所。准备好共赴这场笔尖与灵魂的滚烫相遇了吗?`} @@ -279,10 +280,10 @@ const LuxunPlace1 = () => { { id: '4', label: '1927年3-9月' }, ]} rightItems={[ - { id: 'a', label: '在绍兴三味书屋求学积淀' }, { id: 'b', label: '于北京八道湾参加新文化运动、酝酿《阿Q正传》' }, - { id: 'c', label: '赴厦门大学任教,聚焦底层创作杂文' }, + { id: 'a', label: '在绍兴三味书屋求学积淀' }, { id: 'd', label: '在广州白云楼整理旧作定稿《朝花夕拾》部分篇章' }, + { id: 'c', label: '赴厦门大学任教,聚焦底层创作杂文' }, ]} correctMatches={[ { leftId: '1', rightId: 'a' }, diff --git a/src/pages/luxun/place/place4.tsx b/src/pages/luxun/place/place4.tsx index 313b722..6e07113 100644 --- a/src/pages/luxun/place/place4.tsx +++ b/src/pages/luxun/place/place4.tsx @@ -112,7 +112,7 @@ const LuxunPlace5 = () => { <> { setShowGuideModal(false) }} audioSrc={getPublicPath('audio/luxun/mp3_luxun_31.mp3')} - contentPaddingLeft="2em" - // firstLineIndent="2em" + firstLineIndent="2em" > {`同学你好,欢迎来到“跨越时空·体悟”学习模块! 请登陆“时空回溯・亲历时代”体验仓,下一站:1892年,绍兴三味书屋。 diff --git a/src/pages/maozedong/home.tsx b/src/pages/maozedong/home.tsx index c938426..8e0b06d 100644 --- a/src/pages/maozedong/home.tsx +++ b/src/pages/maozedong/home.tsx @@ -1,6 +1,9 @@ +import { useEffect } from 'react' import { getBackgroundImage, getStaticImage } from '@/lib/utils' import { useNavigate, useParams } from 'react-router-dom' import { Image } from '@/components' +import { usePuzzleStore } from '@/store/puzzleStore' +import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression' import { useMaozedongStore } from '@/store/maozedongStore' const Maozedong = () => { @@ -13,6 +16,17 @@ const Maozedong = () => { const starName = 'maozedong' const lastVisitedPlace = useMaozedongStore((state) => state.lastVisitedPlace) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) + const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } = + getResidenceStageUnlocks(starName, puzzles) + + useEffect(() => { + if (isLoaded && !isResidenceUnlocked(starName, puzzles)) { + navigate('/menu', { replace: true }) + } + }, [isLoaded, navigate, puzzles, starName]) + if (!moduleName) return null return ( @@ -31,19 +45,33 @@ const Maozedong = () => {
navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)} + onClick={() => { + if (!dialogueUnlocked) return + navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`) + }} /> navigate(`/${moduleName}/${starName}/reflection`)} + onClick={() => { + if (!reflectionUnlocked) return + navigate(`/${moduleName}/${starName}/reflection`) + }} /> navigate(`/${moduleName}/${starName}/internalization`)} + onClick={() => { + if (!internalizationUnlocked) return + navigate(`/${moduleName}/${starName}/internalization`) + }} />
diff --git a/src/pages/maozedong/internalization.tsx b/src/pages/maozedong/internalization.tsx index 94ab715..810a6db 100644 --- a/src/pages/maozedong/internalization.tsx +++ b/src/pages/maozedong/internalization.tsx @@ -386,6 +386,7 @@ const MaozedongInternalization = () => { {/* 过渡语 */} { setShowTransition(false) setIntroDialogVisible(true) @@ -464,7 +465,7 @@ const MaozedongInternalization = () => { speaker="student" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_24.mp3')} > - 关于“实事求是、群众路线、独立自主”这十二个字我还是有些不理解,总感觉这十二个字离我们很遥远。 + 小沪,关于“实事求是、群众路线、独立自主”这十二个字我还是有些不理解,总感觉这十二个字离我们很遥远。 “实事求是、群众路线、独立自主”这十二个字,可以说是中国共产党人认识世界、改造世界的根本方法与价值遵循。今天我们有幸邀请到了华东师范大学马克思主义学院马立政副教授。关于你的问题,我想马教授会给你指点迷津! diff --git a/src/pages/maozedong/reflection.tsx b/src/pages/maozedong/reflection.tsx index c2886d8..7268603 100644 --- a/src/pages/maozedong/reflection.tsx +++ b/src/pages/maozedong/reflection.tsx @@ -10,7 +10,7 @@ import { SuccessAlert } from '@/components/layout' import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext' import { cn, getStaticImage } from '@/lib/utils' import { getPublicPath } from '@/lib/utils' -import { useMemo, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' // 光标组件 - 带旋转动画 @@ -222,17 +222,36 @@ const Home = () => { const bgmRef = useRef(null) const videoRef = useRef(null) const navigate = useNavigate() - const [showGuideModal, setShowGuideModal] = useState(true) const [endingVisible, setEndingVisible] = useState(false) const moduleName = 'maozedong' - const playBGM = () => { + const playBGM = useCallback(() => { bgmRef.current?.play().catch(() => {}) - } + }, []) const anchors: VideoAnchor[] = useMemo( () => [ + { + id: 'guide-modal', + time: 5, + afterResume: 'hide', + render: ({ resume }) => ( + { + playBGM() + resume() + }} + firstLineIndent="2em" + > + {`同学你好,欢迎来到“跨越时空·体悟”学习模块! +请登陆“时空回溯・亲历时代”体验仓,下一站:1924年,上海。 +1924年的上海,革命浪潮暗涌,中共四大筹备工作关乎革命前途。无产阶级领导权、工农联盟等核心议题亟待明确,毛泽东同志在筹备关键节点面临诸多重要抉择。请你化身历史见证者,站在毛主席的视角做出选择,解锁四大筹备的关键密码,感受革命先辈的坚定与智慧!完成三次选择后,可进入下一阶段的学习。时空门已开,请做好准备迎接考验吧!`} + + ), + }, { id: 'anchor-0', time: 3, @@ -461,7 +480,7 @@ const Home = () => { render: ({ resume, unmute }) => , }, ], - [triggerPuzzleAnimation] + [playBGM, triggerPuzzleAnimation] ) return ( @@ -472,7 +491,7 @@ const Home = () => { src={getPublicPath('video/maozedong/mzd_7_8_9_tiwu.mp4')} className="w-full h-full aspect-video" anchors={anchors} - autoPlay={false} + autoPlay muted playsInline onEnded={() => { @@ -480,21 +499,6 @@ const Home = () => { }} /> - { - setShowGuideModal(false) - playBGM() - videoRef.current?.play() - }} - firstLineIndent="2em" - > - {`同学你好,欢迎来到“跨越时空·体悟”学习模块! -请登陆“时空回溯・亲历时代”体验仓,下一站:1924年,上海。 -1924年的上海,革命浪潮暗涌,中共四大筹备工作关乎革命前途。无产阶级领导权、工农联盟等核心议题亟待明确,毛泽东同志在筹备关键节点面临诸多重要抉择。请你化身历史见证者,站在毛主席的视角做出选择,解锁四大筹备的关键密码,感受革命先辈的坚定与智慧!完成三次选择后,可进入下一阶段的学习。时空门已开,请做好准备迎接考验吧!`} - - { @@ -11,6 +14,17 @@ const NieerHome = () => { const starName = 'nieer' const lastVisitedPlace = useNieerStore((s) => s.lastVisitedPlace) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) + const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } = + getResidenceStageUnlocks(starName, puzzles) + + useEffect(() => { + if (isLoaded && !isResidenceUnlocked(starName, puzzles)) { + navigate('/menu', { replace: true }) + } + }, [isLoaded, navigate, puzzles, starName]) + if (!moduleName) return null return ( @@ -30,20 +44,34 @@ const NieerHome = () => {
{/* 对话 */} navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)} + onClick={() => { + if (!dialogueUnlocked) return + navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`) + }} /> {/* 体悟 / 内化 预留 */} navigate(`/${moduleName}/${starName}/reflection`)} + onClick={() => { + if (!reflectionUnlocked) return + navigate(`/${moduleName}/${starName}/reflection`) + }} /> navigate(`/${moduleName}/${starName}/internalization`)} + onClick={() => { + if (!internalizationUnlocked) return + navigate(`/${moduleName}/${starName}/internalization`) + }} />
diff --git a/src/pages/nieer/internalization.tsx b/src/pages/nieer/internalization.tsx index 06910e8..05bd6d2 100644 --- a/src/pages/nieer/internalization.tsx +++ b/src/pages/nieer/internalization.tsx @@ -333,17 +333,28 @@ const NieerInternalization = () => { /> )} - { - setShowPostNarration(false) - setShowLetter(true) + onVisibleChange={(visible) => { + setShowPostNarration(visible) + if (!visible) { + setShowLetter(true) + } }} - text="的确如戏!这份精神,不仅是音乐创作的指引,更是广大青年学子成长路上的精神灯塔,指引大家在专业学习中精益求精,在青春岁月里奋勇前行。此刻,我们仿佛依然站在聂耳旧居的亭子间里,仿佛还能看到先生熬夜打磨旋律的身影,感受到他心中滚烫的家国情怀。现在,就请你静下心来,把对聂耳的崇敬、对国歌精神的体悟和对自己未来的期许,一字一句写成一封信,完成与先生跨越时空的对话吧!" - /> + showMask={false} + zIndex={60} + audioChannelId="nieer-internalization-narration-post" + > + + 的确如戏!这份精神,不仅是音乐创作的指引,更是广大青年学子成长路上的精神灯塔,指引大家在专业学习中精益求精,在青春岁月里奋勇前行。 + + + 此刻,我们仿佛依然站在聂耳旧居的亭子间里,仿佛还能看到先生熬夜打磨旋律的身影,感受到他心中滚烫的家国情怀。 + + + 现在,就请你静下心来,把对聂耳的崇敬、对国歌精神的体悟和对自己未来的期许,一字一句写成一封信,完成与先生跨越时空的对话吧! + + {/* 写信组件 */} { ref={video2Ref} className="fixed inset-0 z-20000" src={getPublicPath( - video2Step === 1 ? 'video/chenyun/chenyun_01.mp4' : 'video/nieer/nieer_2.mp4' + video2Step === 1 ? 'video/nieer/nieer_01.mp4' : 'video/nieer/nieer_2.mp4' )} autoPlay playsInline @@ -404,6 +404,7 @@ const NieerPlace1 = () => { visible={showAfterDialogGuide} onContinue={() => setShowAfterDialogGuide(false)} audioSrc={getPublicPath('audio/nieer/mp3_nieer_01.mp3')} + firstLineIndent="2em" > 同学你好,欢迎来到“走进故居·对话”学习模块!{'\n'} 此刻你正位立于国歌展示馆的入口,请随时光的脚步,在聂耳作品的“时光声浪”中聆听五段炽热的时代回响:民族危亡的怒吼声、时代呼唤的最强音、民族解放的冲锋号、强我中华的进行曲、伟大复兴的主旋律。你准备好跟随聂耳的音符,踏上这段炽热征程了吗? diff --git a/src/pages/nieer/place/place3.tsx b/src/pages/nieer/place/place3.tsx index 697accf..5120be3 100644 --- a/src/pages/nieer/place/place3.tsx +++ b/src/pages/nieer/place/place3.tsx @@ -240,7 +240,7 @@ const NieerPlace3 = () => { src={ videoForExhibitId === 1 ? getPublicPath('video/nieer/nieer_5.mp4') - : getPublicPath('video/chenyun/chenyun_01.mp4') //正确地址nieer_02目前没有,暂时使用其他代替 + : getPublicPath('video/nieer/nieer_02.mp4') //正确地址nieer_02目前没有,暂时使用其他代替 } autoPlay playsInline diff --git a/src/pages/nieer/reflection/index.tsx b/src/pages/nieer/reflection/index.tsx index c78e43b..961bf12 100644 --- a/src/pages/nieer/reflection/index.tsx +++ b/src/pages/nieer/reflection/index.tsx @@ -1,6 +1,6 @@ -import { Image, StudyGuideModal } from '@/components' +import { EndingOverlay, Image, StudyGuideModal } from '@/components' import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils' -import { useMemo } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { LOBBY_COLUMNS, LOBBY_LAYOUT, LOBBY_TIMELINE } from './lobby.config' import { useNieerStore } from '@/store/nieerStore' @@ -50,6 +50,20 @@ const NieerReflection = () => { [placeCompletion] ) + const [endingVisible, setEndingVisible] = useState(false) + const prevDialogModeRef = useRef(dialogState.mode) + + useEffect(() => { + if ( + prevDialogModeRef.current === 'result' && + dialogState.mode === 'idle' && + allPlacesCompleted + ) { + setEndingVisible(true) + } + prevDialogModeRef.current = dialogState.mode + }, [dialogState.mode, allPlacesCompleted]) + const handleResultShare = (sceneId: number, payload?: Record) => { handleShare({ payload, @@ -244,13 +258,23 @@ const NieerReflection = () => { visible={showGuideModal} onContinue={markReflectionGuideSeen} audioSrc={getPublicPath('audio/nieer/mp3_nieer_28.mp3')} - contentPaddingLeft="2em" - // firstLineIndent="2em" + firstLineIndent="2em" > {`同学你好,欢迎来到“跨越时空·体悟”学习模块! 请登陆“时空回溯・亲历时代”体验仓,下一站:1936年,上海。 请带上你的旅行包,穿越时空解锁6个国歌故事,集齐精神徽章,绘制属于你的国歌精神图谱吧~时空门已开,请出发吧!`} + + { + setEndingVisible(false) + navigate(`/${moduleName}/${starName}/home`) + }} + />
) } diff --git a/src/pages/rongdesheng/home.tsx b/src/pages/rongdesheng/home.tsx index 0c4ee37..4c797e0 100644 --- a/src/pages/rongdesheng/home.tsx +++ b/src/pages/rongdesheng/home.tsx @@ -1,5 +1,8 @@ +import { useEffect } from 'react' import { getBackgroundImage, getStaticImage } from '@/lib/utils' import { Image } from '@/components' +import { usePuzzleStore } from '@/store/puzzleStore' +import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression' import { useNavigate, useParams } from 'react-router-dom' import { useRongdeshengStore } from '@/store/rongdeshengStore' @@ -11,6 +14,17 @@ const RongdeshengHome = () => { const starName = 'rongdesheng' const lastVisitedPlace = useRongdeshengStore((s) => s.lastVisitedPlace) + const puzzles = usePuzzleStore((state) => state.puzzles) + const isLoaded = usePuzzleStore((state) => state.isLoaded) + const { dialogueUnlocked, reflectionUnlocked, internalizationUnlocked } = + getResidenceStageUnlocks(starName, puzzles) + + useEffect(() => { + if (isLoaded && !isResidenceUnlocked(starName, puzzles)) { + navigate('/menu', { replace: true }) + } + }, [isLoaded, navigate, puzzles, starName]) + if (!moduleName) return null return ( @@ -30,23 +44,37 @@ const RongdeshengHome = () => {
{/* 对话 */} navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)} + onClick={() => { + if (!dialogueUnlocked) return + navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`) + }} /> {/* 体悟(占位) */} navigate(`/${moduleName}/${starName}/reflection`)} + onClick={() => { + if (!reflectionUnlocked) return + navigate(`/${moduleName}/${starName}/reflection`) + }} /> {/* 内化(占位) */} navigate(`/${moduleName}/${starName}/internalization`)} + onClick={() => { + if (!internalizationUnlocked) return + navigate(`/${moduleName}/${starName}/internalization`) + }} />
diff --git a/src/pages/rongdesheng/place/place1.tsx b/src/pages/rongdesheng/place/place1.tsx index 6fbc616..f5a25eb 100644 --- a/src/pages/rongdesheng/place/place1.tsx +++ b/src/pages/rongdesheng/place/place1.tsx @@ -272,6 +272,7 @@ const RongdeshengPlace1 = () => { visible={showAfterDialogGuide} onContinue={() => setShowAfterDialogGuide(false)} audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_01.mp3')} + firstLineIndent="2em" > {` 同学你好,欢迎来到“走进故居·对话”学习模块! 此刻你正伫立在荣德生旧居的石库门廊下,接下来请循着实业的足迹,一同探寻先生传奇人生里的4段壮阔征程:金融启蒙·资本筑基、实业拓疆·双业兴邦、山河危难·气节如钢、风雨坚守·初心不改。准备好共赴这场面粉与棉纱交织的民族工业史诗了吗?`} diff --git a/src/pages/rongdesheng/place/place2.tsx b/src/pages/rongdesheng/place/place2.tsx index f1bfef0..a35f464 100644 --- a/src/pages/rongdesheng/place/place2.tsx +++ b/src/pages/rongdesheng/place/place2.tsx @@ -222,7 +222,7 @@ const RongdeshengPlace2 = () => { )} {showVideo && videoForExhibitId === 2 && (