进度模拟面板写入 puzzleStore 后,App 启动时易班同步会将其覆盖; 现在在同步完成后按 sessionStorage 中保存的模式重放模拟进度, 确保测试包的"到达某故居/全流程完成"调试入口生效。 同时纳入近期未提交的页面、组件、资源与环境变量改动。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
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 { getUserDefineResult } from '@/lib/utils/api-common'
|
|
import { showYibanSyncErrorPrompt, showYibanUnauthenticatedPrompt } from '@/components'
|
|
import {
|
|
RONGDESHENG_KEY,
|
|
RONGDESHENG_LESSON_ID,
|
|
useRongdeshengStore,
|
|
} from '@/store/rongdeshengStore'
|
|
|
|
const deserializeVisitedExhibits = (
|
|
data: Record<string, number[]> | undefined
|
|
): Record<string, Set<number>> => {
|
|
if (!data || typeof data !== 'object') return {}
|
|
const visitedExhibits: Record<string, Set<number>> = {}
|
|
for (const [place, exhibits] of Object.entries(data)) {
|
|
visitedExhibits[place] = new Set(Array.isArray(exhibits) ? exhibits : [])
|
|
}
|
|
return visitedExhibits
|
|
}
|
|
|
|
const RongdeshengHome = () => {
|
|
const navigate = useNavigate()
|
|
const { moduleName } = useParams<{ moduleName: string }>()
|
|
|
|
// starName 固定为 rongdesheng
|
|
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])
|
|
|
|
useEffect(() => {
|
|
;(async () => {
|
|
try {
|
|
const result = await getUserDefineResult(RONGDESHENG_LESSON_ID, RONGDESHENG_KEY)
|
|
|
|
if (result.status !== 'success') {
|
|
useRongdeshengStore.setState((prev) => ({
|
|
...prev,
|
|
visitedExhibits: {},
|
|
lastVisitedPlace: null,
|
|
reflectionCompleted: {},
|
|
reflectionVideoViewed: {},
|
|
reflectionFinalCompleted: false,
|
|
reflectionGuideShown: false,
|
|
}))
|
|
|
|
if (result.status === 'unauthenticated') {
|
|
showYibanUnauthenticatedPrompt()
|
|
} else if (result.status === 'error') {
|
|
showYibanSyncErrorPrompt()
|
|
}
|
|
return
|
|
}
|
|
|
|
const data = JSON.parse(result.content) as {
|
|
visitedExhibits?: Record<string, number[]>
|
|
lastVisitedPlace?: string | null
|
|
reflectionCompleted?: Record<string, boolean>
|
|
reflectionVideoViewed?: Record<string, Record<string, boolean>>
|
|
reflectionFinalCompleted?: boolean
|
|
reflectionGuideShown?: boolean
|
|
}
|
|
|
|
useRongdeshengStore.setState((prev) => ({
|
|
...prev,
|
|
visitedExhibits: deserializeVisitedExhibits(data.visitedExhibits),
|
|
lastVisitedPlace: data.lastVisitedPlace ?? null,
|
|
reflectionCompleted: data.reflectionCompleted ?? {},
|
|
reflectionVideoViewed: data.reflectionVideoViewed ?? {},
|
|
reflectionFinalCompleted: data.reflectionFinalCompleted ?? false,
|
|
reflectionGuideShown: data.reflectionGuideShown ?? false,
|
|
}))
|
|
} catch (error) {
|
|
useRongdeshengStore.setState((prev) => ({
|
|
...prev,
|
|
visitedExhibits: {},
|
|
lastVisitedPlace: null,
|
|
reflectionCompleted: {},
|
|
reflectionVideoViewed: {},
|
|
reflectionFinalCompleted: false,
|
|
reflectionGuideShown: false,
|
|
}))
|
|
showYibanSyncErrorPrompt()
|
|
console.error('Failed to load Rongdesheng store', error)
|
|
}
|
|
})()
|
|
}, [])
|
|
|
|
if (!moduleName) return null
|
|
|
|
return (
|
|
<div
|
|
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
|
style={{
|
|
backgroundImage: getBackgroundImage(`${starName}/底图`),
|
|
}}
|
|
>
|
|
<Image
|
|
className="absolute right-37 top-22"
|
|
src={getStaticImage('components/革命先驱故居-返回')}
|
|
button
|
|
onClick={() => navigate(`/${moduleName}`)}
|
|
/>
|
|
|
|
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
|
{/* 对话 */}
|
|
<Image
|
|
button={dialogueUnlocked}
|
|
className={dialogueUnlocked ? undefined : 'opacity-50 grayscale cursor-not-allowed'}
|
|
src={getStaticImage('star/一-解锁')}
|
|
onClick={() => {
|
|
if (!dialogueUnlocked) return
|
|
navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)
|
|
}}
|
|
/>
|
|
|
|
{/* 体悟(占位) */}
|
|
<Image
|
|
button={reflectionUnlocked}
|
|
className={reflectionUnlocked ? undefined : 'opacity-50 grayscale cursor-not-allowed'}
|
|
src={getStaticImage('star/二-解锁')}
|
|
onClick={() => {
|
|
if (!reflectionUnlocked) return
|
|
navigate(`/${moduleName}/${starName}/reflection`)
|
|
}}
|
|
/>
|
|
|
|
{/* 内化(占位) */}
|
|
<Image
|
|
button={internalizationUnlocked}
|
|
className={
|
|
internalizationUnlocked ? undefined : 'opacity-50 grayscale cursor-not-allowed'
|
|
}
|
|
src={getStaticImage('star/三-解锁')}
|
|
onClick={() => {
|
|
if (!internalizationUnlocked) return
|
|
navigate(`/${moduleName}/${starName}/internalization`)
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default RongdeshengHome
|