Commit non-media application and asset changes while leaving audio, video, and environment files out of version control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
6.5 KiB
TypeScript
184 lines
6.5 KiB
TypeScript
import {
|
||
EndingOverlay,
|
||
ExhibitOverlay,
|
||
GuideSteps,
|
||
GuideStepsItem,
|
||
Image,
|
||
Title,
|
||
} from '@/components'
|
||
import Cursor from '@/components/cursor'
|
||
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||
import { useRef, useState } from 'react'
|
||
import { useNavigate, useParams } from 'react-router-dom'
|
||
import { useNieerStore, saveNieerStore } from '@/store/nieerStore'
|
||
import { useCursorController } from '@/hooks/useCursorController'
|
||
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
|
||
import { setCourseTimeProgress } from '@/lib/utils/api-common'
|
||
|
||
const NieerPlace6 = () => {
|
||
const navigate = useNavigate()
|
||
const { moduleName } = useParams<{ moduleName: string }>()
|
||
|
||
const { triggerPuzzleAnimation } = usePuzzleAnimation()
|
||
const starName = 'nieer'
|
||
const place = 'place6'
|
||
const { markExhibitVisited, getVisitedExhibits } = useNieerStore()
|
||
|
||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null)
|
||
const [guide1Visible, setGuide1Visible] = useState(false)
|
||
const [guide1Step, setGuide1Step] = useState(0)
|
||
|
||
const total = 2
|
||
const visitedExhibits = getVisitedExhibits(place)
|
||
const visited = new Set(visitedExhibits)
|
||
const progressCount = visited.size
|
||
const completed = progressCount >= total
|
||
|
||
const [endingVisible, setEndingVisible] = useState(false)
|
||
// 是否已展示过结束语(用于控制“离开场景”按钮出现时机)
|
||
// 若用户刷新页面且已完成,则默认视为结束语已展示,避免卡住无法离开
|
||
const [endingShown, setEndingShown] = useState(() => completed)
|
||
const endingShownRef = useRef(false)
|
||
const subtitle = completed ? '已完成' : `${progressCount}/${total}`
|
||
|
||
const { isEnabled } = useCursorController({
|
||
initialEnabled: [2],
|
||
dependencies: {
|
||
1: [2],
|
||
},
|
||
visited: visitedExhibits,
|
||
})
|
||
|
||
if (!moduleName) return null
|
||
|
||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||
1: 'nieer/place6/场景6展品1',
|
||
2: 'nieer/place6/场景6展品2',
|
||
}
|
||
|
||
const handleOpenExhibit = (id: 1 | 2) => {
|
||
setActiveExhibitId(id)
|
||
if (id === 1) {
|
||
setGuide1Step(0)
|
||
setGuide1Visible(true)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||
style={{
|
||
backgroundImage: getBackgroundImage(`${starName}/${place}/场景6`),
|
||
}}
|
||
>
|
||
<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={() => navigate(`/${moduleName}/${starName}/place5`)}
|
||
/>
|
||
|
||
<Cursor
|
||
className="absolute left-90 bottom-130"
|
||
onClick={() => handleOpenExhibit(1)}
|
||
read={visited.has(1)}
|
||
disabled={!isEnabled(1)}
|
||
/>
|
||
<Cursor
|
||
className="absolute right-240 bottom-135"
|
||
onClick={() => handleOpenExhibit(2)}
|
||
read={visited.has(2)}
|
||
disabled={!isEnabled(2)}
|
||
/>
|
||
|
||
{/* 最后一个场景:完成后不显示“前进”按钮 */}
|
||
{completed && endingShown && !endingVisible && (
|
||
<Image
|
||
className="absolute bottom-0 right-0"
|
||
src={getStaticImage('components/place/离开场景')}
|
||
button
|
||
onClick={() => {
|
||
saveNieerStore()
|
||
setCourseTimeProgress(13)
|
||
navigate(`/${moduleName}/${starName}/home`)
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
<ExhibitOverlay
|
||
visible={activeExhibitId !== null}
|
||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
|
||
onOpen={() => {
|
||
if (activeExhibitId === 1) {
|
||
triggerPuzzleAnimation(13)
|
||
}
|
||
if (activeExhibitId === 2) {
|
||
triggerPuzzleAnimation(12)
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
const closingId = activeExhibitId
|
||
setActiveExhibitId(null)
|
||
setGuide1Visible(false)
|
||
|
||
if (closingId !== null) {
|
||
markExhibitVisited(place, closingId)
|
||
saveNieerStore()
|
||
|
||
// 仅在“关闭展品”这一刻触发结束语:当本次关闭导致完成时,弹出 EndingOverlay
|
||
const nextVisitedSize = visited.has(closingId) ? visited.size : visited.size + 1
|
||
const willComplete = nextVisitedSize >= total
|
||
if (willComplete && !endingShownRef.current) {
|
||
endingShownRef.current = true
|
||
setEndingShown(true)
|
||
setEndingVisible(true)
|
||
}
|
||
}
|
||
}}
|
||
/>
|
||
|
||
{/* 光标1对话(图1) */}
|
||
<GuideSteps
|
||
visible={guide1Visible}
|
||
currentStep={guide1Step}
|
||
onStepChange={setGuide1Step}
|
||
onVisibleChange={setGuide1Visible}
|
||
showMask={false}
|
||
highlightSelectors={[]}
|
||
zIndex={10001}
|
||
>
|
||
<GuideStepsItem speaker="student" audioSrc={getPublicPath('audio/nieer/mp3_nieer_25.mp3')}>
|
||
小沪,这些不同场景下奏响国歌的画面太让人感动了!在和平年代,这首歌还有什么特别的意义呢?
|
||
</GuideStepsItem>
|
||
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/nieer/mp3_nieer_26_1.mp3')}>
|
||
和平年代里,它的精神力量依然不减。你看这张中国女排奥运夺金的照片,国歌奏响时,是民族自豪的宣告;神舟飞船升空时的国歌,是航天人攻坚克难的礼赞。
|
||
</GuideStepsItem>
|
||
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/nieer/mp3_nieer_26_2.mp3')}>
|
||
它就像一根精神纽带,无论在高光时刻还是艰难关头,都能凝聚起全国人民的力量,提醒我们不忘历史、砥砺前行。
|
||
</GuideStepsItem>
|
||
</GuideSteps>
|
||
|
||
{/* completed 后的结束语:统一使用 EndingOverlay */}
|
||
<EndingOverlay
|
||
visible={endingVisible}
|
||
audioSrc={getPublicPath('audio/nieer/mp3_nieer_27.mp3')}
|
||
text={
|
||
'从民族危机中觉醒的热血青年,到《义勇军进行曲》的谱曲者,聂耳用音符奏响了“救亡图存”的时代强音。' +
|
||
'而《义勇军进行曲》的影响远不止于此,接下来,请你化身国歌时空旅行者,踏入6个散落于岁月中的故事现场,' +
|
||
'一路聆听、一路共鸣,亲身体验这首旋律如何跨越时空,成为永远激荡在国人心中的精神号角吧!'
|
||
}
|
||
animationSpeed={200}
|
||
onContinue={() => setEndingVisible(false)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default NieerPlace6
|