Files
shanghai-residences/src/pages/maozedong/place/place2.tsx
T
2026-04-30 09:17:31 +08:00

321 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
Image,
Title,
MultipleChoiceQuizPanel,
Video,
type VideoRef,
} from '@/components'
import { useNavigate, useParams } from 'react-router-dom'
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
import { useMemo, useRef, useState } from 'react'
import Cursor from '@/components/cursor'
import { useCursorController } from '@/hooks/useCursorController'
import SuccessAlert from '@/components/layout/alert/success-alert'
import ScaledPortal from '@/components/layout/ScaledPortal'
import { useMaozedongStore, saveMaozedongStore } from '@/store/maozedongStore'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
// starName 固定为 maozedong
const starName = 'maozedong'
// 展品 ID 与资源路径映射,ID 用来做唯一标识
const exhibitSrcMap: Record<1 | 2, string> = {
1: 'maozedong/place2/场景2展品1',
2: 'maozedong/place2/场景2展品2',
}
const MaozedongPlace2 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
// 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null)
// 持久化接入
const markVisited = useMaozedongStore((s) => s.markExhibitVisited)
const getVisited = useMaozedongStore((s) => s.getVisitedExhibits)
const setLastVisitedPlace = useMaozedongStore((s) => s.setLastVisitedPlace)
const persistedVisited = useMemo(() => new Set(getVisited('place2') as (1 | 2)[]), [getVisited])
const [visited, setVisited] = useState<Set<1 | 2>>(() => new Set(persistedVisited))
// 展品1的对话引导状态
const [guideVisible1, setGuideVisible1] = useState(false)
const [guideStep1, setGuideStep1] = useState(0)
// 展品2的对话引导状态
const [guideVisible2, setGuideVisible2] = useState(false)
const [guideStep2, setGuideStep2] = useState(0)
// 展品1对话结束后播放的视频状态(播完后再出题目)
const [showVideo, setShowVideo] = useState(false)
const videoRef = useRef<VideoRef>(null)
// 展品1对话结束后的多选题
const [quizVisible, setQuizVisible] = useState(false)
const [quizPassed, setQuizPassed] = useState(false)
const [successAlertVisible, setSuccessAlertVisible] = useState(false)
// Cursor 顺序控制:1 -> 2
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: {
2: [1],
},
visited,
})
const handleOpenExhibit = (id: 1 | 2) => {
setActiveExhibitId(id)
if (id === 1) {
setGuideVisible1(true)
setGuideStep1(0)
// 重新进入时重置题目完成状态
setQuizPassed(false)
} else if (id === 2) {
// 展品2:先看展品,关闭展品后再显示对话
setGuideVisible2(false)
setGuideStep2(0)
}
}
// 展品1对话步骤变化
const handleGuideStepChange1 = (step: number) => {
setGuideStep1(step)
}
// 展品1对话完成
const handleGuideVisibleChange1 = (visible: boolean) => {
setGuideVisible1(visible)
if (!visible && !quizPassed) {
setShowVideo(true)
}
}
// 展品2对话步骤变化
const handleGuideStepChange2 = (step: number) => {
setGuideStep2(step)
}
// 展品2对话完成
const handleGuideVisibleChange2 = (visible: boolean) => {
setGuideVisible2(visible)
}
const progressCount = visited.size
const total = 2
const completed = progressCount >= total
const subtitle = completed ? '已完成' : `${progressCount}/${total}`
if (!moduleName) return null
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/place2/场景2`),
}}
>
{/* 标题 */}
<Title
title="播撒红色火种"
subtitle={subtitle}
className="absolute top-10 left-1/2 -translate-x-1/2"
/>
<Image
className="absolute right-5 top-5"
src={getStaticImage('components/离开')}
button
onClick={() => {
if (moduleName && starName) {
navigate(`/${moduleName}/${starName}/place1`)
}
}}
/>
{/* 光标1 */}
<Cursor
className="absolute left-217 bottom-145"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute right-92 bottom-105"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
{completed && (
<Image
className="absolute bottom-0 right-0"
src={getStaticImage('components/place/继续前进')}
button
onClick={() => {
setLastVisitedPlace('place3')
saveMaozedongStore()
navigate(`/${moduleName}/${starName}/place3`)
}}
/>
)}
{/* 展品遮罩层 */}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 1) {
triggerPuzzleAnimation(3)
}
if (activeExhibitId === 2) {
triggerPuzzleAnimation(4)
}
}}
onClose={() => {
const currentId = activeExhibitId
if (currentId !== null) {
setVisited((prev) => {
const next = new Set(prev)
next.add(currentId)
markVisited('place2', currentId)
saveMaozedongStore()
return next
})
}
setActiveExhibitId(null)
// 展品2:关闭后显示对话
if (currentId === 2) {
setGuideStep2(0)
setGuideVisible2(true)
}
}}
/>
{/* 展品1对话结束后播放的视频:播放完毕后出现题目 */}
{showVideo && (
<ScaledPortal zIndex={20000}>
<Video
ref={videoRef}
src={getPublicPath('video/maozedong/mzd_002.mp4')}
className="absolute inset-0 w-full h-full"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo(false)
setQuizVisible(true)
}}
/>
</ScaledPortal>
)}
{/* 展品1的对话引导(文字+语音播报占位) */}
<GuideSteps
visible={guideVisible1}
onVisibleChange={handleGuideVisibleChange1}
currentStep={guideStep1}
onStepChange={handleGuideStepChange1}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_05.mp3')}
>
中共一大定了“推翻资产阶级政权”的目标,但中共二大提
出了“反帝反封建的民主革命纲领”,为什么会有这样的转变呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_06.mp3')}>
中共一大时我们中国共产党刚成立,像刚学走路的孩子,
目标很宏大——“建立无产阶级专政”“消灭阶级”。但毛主席回到湖南后,
做了实际调查发现必须先解决“眼前的敌人”。
</GuideStepsItem>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_07.mp3')}
>
“眼前的敌人”?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_08.mp3')}>
对!想想看1922年长沙的码头工人罢工,安源的路矿工人罢工,他们的敌人是谁?是外国资本家,是军阀。中共二大时毛主席虽然没有出席,但完全赞同“最低纲领”——打倒军阀、推翻帝国主义压迫,实现民族独立和人民民主。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
这就像盖房子,得先把地基打好;等打倒了军阀和帝国主义,再谈社会主义也不迟。
</GuideStepsItem>
</GuideSteps>
{/* 展品2的对话引导 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={handleGuideVisibleChange2}
currentStep={guideStep2}
onStepChange={handleGuideStepChange2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_09.mp3')}>
后来喊出的“打倒列强,除军阀”的口号,就是从这儿来的。
</GuideStepsItem>
</GuideSteps>
{/* 展品1对话结束后的多选题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quizVisible}
title="【多选】1922年7月16日至23日,中国共产党第二次全国代表大会在上海南成都路辅德里625号召开。大会制定了党的最低纲领和最高纲领,党的最低纲领,即党在民主革命阶段的纲领是:"
options={[
{ value: 'A', label: 'A.打倒军阀,建立国内和平' },
{
value: 'B',
label: 'B.推翻国际帝国主义的压迫,达到中华民族的完全独立',
},
{ value: 'C', label: 'C.统一中国为真正的民主共和国' },
]}
correctAnswer={['A', 'B', 'C']}
minSelections={1}
maxSelections={3}
errorMessage={(selectedValues) => {
if (selectedValues.length < 3) {
return '你还没有全面了解中共二大提出的党的最低纲领哦。请你再想想吧!'
}
return '还不太对,再仔细对照对话内容想一想吧~'
}}
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuizVisible(false)
setQuizPassed(true)
setSuccessAlertVisible(true)
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlertVisible}
onContinue={() => {
setSuccessAlertVisible(false)
}}
/>
</>
</ScaledPortal>
</div>
)
}
export default MaozedongPlace2