542 lines
18 KiB
TypeScript
542 lines
18 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react'
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
import {
|
|
Image,
|
|
ClueHintModal,
|
|
ClueBookModal,
|
|
flyToAnimation,
|
|
Video,
|
|
StudyGuideModal,
|
|
EndingOverlay,
|
|
} from '@/components'
|
|
import { SuccessAlert } from '@/components/layout'
|
|
import { SingleChoiceQuizPanel } from '@/components/layout/quiz'
|
|
import { getBackgroundImage, getStaticImage, getPublicPath } from '@/lib/utils'
|
|
import type { ClueHint } from '@/components/clue'
|
|
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
|
|
import { useSunzhongshanStore, saveSunzhongshanStore } from '@/store/sunzhongshanStore'
|
|
|
|
const starName = 'sunzhongshan'
|
|
const sceneId = `${starName}/reflection/scene2`
|
|
const GUIDE_STORAGE_KEY = `${sceneId}-guide-shown`
|
|
|
|
// 线索配置 - 根据实际图片调整位置
|
|
const clueConfigs: ClueHint[] = [
|
|
{
|
|
id: `${sceneId}/clue1`,
|
|
name: '线索1',
|
|
position: {
|
|
top: '75%',
|
|
left: '95.5%',
|
|
width: '0%',
|
|
height: '0%',
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue2`,
|
|
name: '线索2',
|
|
position: {
|
|
top: '42%',
|
|
left: '26%',
|
|
width: '18%',
|
|
height: '22%',
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue3`,
|
|
name: '线索3',
|
|
position: {
|
|
top: '30%',
|
|
left: '6%',
|
|
width: '10%',
|
|
height: '22%',
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue4`,
|
|
name: '线索4',
|
|
position: {
|
|
top: '67%',
|
|
left: '8%',
|
|
width: '9%',
|
|
height: '7%',
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue5`,
|
|
name: '线索5',
|
|
position: {
|
|
top: '60%',
|
|
left: '44%',
|
|
width: '42%',
|
|
height: '30%',
|
|
},
|
|
},
|
|
]
|
|
|
|
const SunzhongshanReflectionScene2 = () => {
|
|
const navigate = useNavigate()
|
|
const { moduleName } = useParams<{
|
|
moduleName: string
|
|
}>()
|
|
|
|
// 获取碎片动画触发方法
|
|
const { triggerPuzzleAnimation } = usePuzzleAnimation()
|
|
const { getReflectionFlag, setReflectionFlag } = useSunzhongshanStore()
|
|
|
|
const [showClueHint, setShowClueHint] = useState(false)
|
|
const [showClueBook, setShowClueBook] = useState(false)
|
|
const [showSuccessAlert, setShowSuccessAlert] = useState(false)
|
|
const [successAlertContent, setSuccessAlertContent] = useState('')
|
|
const [showVideo, setShowVideo] = useState(true)
|
|
const [showGuide, setShowGuide] = useState(false)
|
|
const [showQuiz1, setShowQuiz1] = useState(false)
|
|
const [showQuiz2, setShowQuiz2] = useState(false)
|
|
const [showQuiz3, setShowQuiz3] = useState(false)
|
|
const [showCompleteGuide, setShowCompleteGuide] = useState(false)
|
|
const [pendingQuiz, setPendingQuiz] = useState<'quiz2' | 'quiz3' | 'complete' | null>(null)
|
|
|
|
const { collectClue, isClueCollected } = useSunzhongshanStore()
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const clueBookButtonRef = useRef<HTMLDivElement>(null)
|
|
// 为每个线索创建 ref
|
|
const clueRefs = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(new Map())
|
|
|
|
// 获取或创建线索的 ref
|
|
const getClueRef = (clueId: string): React.RefObject<HTMLDivElement | null> => {
|
|
if (!clueRefs.current.has(clueId)) {
|
|
clueRefs.current.set(clueId, React.createRef<HTMLDivElement>())
|
|
}
|
|
return clueRefs.current.get(clueId)!
|
|
}
|
|
|
|
// 处理线索点击
|
|
const handleClueClick = (clueId: string, clueName: string) => {
|
|
// 如果已收集,不重复收集
|
|
if (isClueCollected(clueId)) {
|
|
setSuccessAlertContent('该线索已收集!')
|
|
setShowSuccessAlert(true)
|
|
return
|
|
}
|
|
|
|
// 收集线索
|
|
collectClue(clueId, clueName)
|
|
|
|
// 获取线索元素的 ref
|
|
const clueRef = getClueRef(clueId)
|
|
|
|
// 使用函数式 API 触发飞行动画
|
|
if (clueRef.current && clueBookButtonRef.current) {
|
|
flyToAnimation({
|
|
fromRef: clueRef as React.RefObject<HTMLElement>,
|
|
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
|
targetPosition: 'center',
|
|
duration: 1200,
|
|
onComplete: () => {
|
|
// 动画完成后的回调
|
|
setSuccessAlertContent('获得1条线索,已获得线索可在线索簿中查看!')
|
|
setShowSuccessAlert(true)
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// 处理背包点击 - 只收集线索1,不打开线索提示框
|
|
const handleBackpackClick = () => {
|
|
const clue1Id = `${sceneId}/clue1`
|
|
const clue1Name = '线索1'
|
|
|
|
// 如果线索1已收集,显示提示
|
|
if (isClueCollected(clue1Id)) {
|
|
setSuccessAlertContent('该线索已收集!')
|
|
setShowSuccessAlert(true)
|
|
return
|
|
}
|
|
|
|
// 收集线索1
|
|
collectClue(clue1Id, clue1Name)
|
|
|
|
// 获取线索1的 ref
|
|
const clue1Ref = getClueRef(clue1Id)
|
|
|
|
// 执行飞行动画
|
|
if (clue1Ref.current && clueBookButtonRef.current) {
|
|
flyToAnimation({
|
|
fromRef: clue1Ref as React.RefObject<HTMLElement>,
|
|
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
|
targetPosition: 'center',
|
|
duration: 1200,
|
|
onComplete: () => {
|
|
// 动画完成后显示成功提示
|
|
setSuccessAlertContent('获得1条线索,已获得线索可在线索簿中查看!')
|
|
setShowSuccessAlert(true)
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// 处理线索提示按钮点击 - 打开线索提示框
|
|
const handleClueHintClick = () => {
|
|
setShowClueHint(true)
|
|
}
|
|
|
|
// 处理视频播放结束
|
|
const handleVideoEnd = () => {
|
|
setShowVideo(false)
|
|
// 检查是否需要显示学习引导
|
|
const hasShownGuide = getReflectionFlag(GUIDE_STORAGE_KEY)
|
|
if (!hasShownGuide) {
|
|
setShowGuide(true)
|
|
}
|
|
}
|
|
|
|
// 处理学习引导关闭
|
|
const handleGuideClose = () => {
|
|
setShowGuide(false)
|
|
setReflectionFlag(GUIDE_STORAGE_KEY, true)
|
|
}
|
|
|
|
// 检查是否所有线索都已收集
|
|
const areAllCluesCollected = () => {
|
|
return clueConfigs.every((clue) => isClueCollected(clue.id))
|
|
}
|
|
|
|
// 处理线索簿关闭
|
|
const handleClueBookClose = () => {
|
|
setShowClueBook(false)
|
|
// 如果所有线索都已收集,显示题目1
|
|
if (areAllCluesCollected()) {
|
|
setShowQuiz1(true)
|
|
}
|
|
}
|
|
|
|
// 处理题目1提交
|
|
const handleQuiz1Submit = (_value: string, isCorrect: boolean) => {
|
|
if (isCorrect) {
|
|
setShowQuiz1(false)
|
|
setSuccessAlertContent('回答正确!')
|
|
setShowSuccessAlert(true)
|
|
setPendingQuiz('quiz2')
|
|
}
|
|
}
|
|
|
|
// 处理题目2提交
|
|
const handleQuiz2Submit = (_value: string, isCorrect: boolean) => {
|
|
if (isCorrect) {
|
|
setShowQuiz2(false)
|
|
setSuccessAlertContent('回答正确!')
|
|
setShowSuccessAlert(true)
|
|
setPendingQuiz('quiz3')
|
|
}
|
|
}
|
|
|
|
// 处理题目3提交
|
|
const handleQuiz3Submit = (_value: string, isCorrect: boolean) => {
|
|
if (isCorrect) {
|
|
setShowQuiz3(false)
|
|
setSuccessAlertContent('回答正确!')
|
|
setShowSuccessAlert(true)
|
|
// 设置待显示的完成提示
|
|
setPendingQuiz('complete')
|
|
}
|
|
}
|
|
|
|
// 处理完成提示关闭
|
|
const handleCompleteGuideClose = () => {
|
|
setShowCompleteGuide(false)
|
|
// 保存场景2完成状态
|
|
setReflectionFlag(`${starName}-reflection-scene2-completed`, true)
|
|
if (moduleName && starName) {
|
|
const { setReflectionRoute } = useSunzhongshanStore.getState()
|
|
setReflectionRoute('reflection3')
|
|
saveSunzhongshanStore()
|
|
navigate(`/${moduleName}/${starName}/reflection3`)
|
|
}
|
|
}
|
|
|
|
// 获取线索图片
|
|
const getClueImage = (clueId: string) => {
|
|
const clueNumber = clueId.split('clue')[1]
|
|
return getStaticImage(`${sceneId}/线索${clueNumber}`)
|
|
}
|
|
|
|
// 获取线索详情图片
|
|
const getClueDetailImage = (clueId: string) => {
|
|
const clueNumber = clueId.split('clue')[1]
|
|
// 线索3没有详情图片,返回线索图片
|
|
// if (clueNumber === "4") {
|
|
// return getStaticImage(`${sceneId}/线索${clueNumber}`);
|
|
// }
|
|
return getStaticImage(`${sceneId}/线索${clueNumber}-详情`)
|
|
}
|
|
|
|
// 获取线索提示图片
|
|
const getClueHintImage = (clueId: string): string | null => {
|
|
const clueNumber = clueId.split('clue')[1]
|
|
// 场景2中线索1、2、3、4有提示
|
|
if (clueNumber && ['1', '2', '3', '4'].includes(clueNumber)) {
|
|
return getStaticImage(`${sceneId}/线索${clueNumber}-详情的详情`)
|
|
}
|
|
return null
|
|
}
|
|
|
|
// 初始化线索(如果还没有)
|
|
useEffect(() => {
|
|
clueConfigs.forEach((clue) => {
|
|
if (!isClueCollected(clue.id)) {
|
|
// 不自动收集,只是确保线索存在
|
|
}
|
|
})
|
|
}, [isClueCollected])
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="w-full h-full bg-cover bg-center bg-no-repeat relative"
|
|
style={{
|
|
backgroundImage: getBackgroundImage(`${starName}/reflection/scene2/bg`),
|
|
}}
|
|
>
|
|
{/* 视频播放 - 覆盖在页面内容之上 */}
|
|
{showVideo && (
|
|
<div className="fixed inset-0 z-9999 bg-black">
|
|
<Video
|
|
src={getPublicPath(`video/${starName}/sunzhongshan_11.mp4`)}
|
|
className="w-full h-full"
|
|
onEnded={handleVideoEnd}
|
|
autoPlay={true}
|
|
showSkipButton={true}
|
|
onSkip={handleVideoEnd}
|
|
/>
|
|
</div>
|
|
)}
|
|
{/* 关闭按钮 */}
|
|
<Image
|
|
className="absolute right-0 top-0 z-10"
|
|
src={getStaticImage(`${starName}/reflection/关闭`)}
|
|
button
|
|
onClick={() => {
|
|
if (moduleName && starName) {
|
|
navigate(`/${moduleName}/${starName}/reflection`)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* 线索点击区域 */}
|
|
{clueConfigs.map((clue) => {
|
|
const isCollected = isClueCollected(clue.id)
|
|
const clueRef = getClueRef(clue.id)
|
|
return (
|
|
<div key={clue.id}>
|
|
{/* 线索元素(用于克隆和飞行) */}
|
|
<div
|
|
ref={clueRef}
|
|
className="absolute pointer-events-none opacity-0"
|
|
style={{
|
|
top: clue.position.top,
|
|
left: clue.position.left,
|
|
width: clue.position.width,
|
|
height: clue.position.height,
|
|
}}
|
|
>
|
|
<div className="relative">
|
|
<Image
|
|
src={getClueImage(clue.id)}
|
|
className="w-20 h-20 border-2 border-yellow-400 rounded shadow-lg"
|
|
/>
|
|
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2 bg-yellow-400 text-black px-2 py-1 rounded text-xs font-bold whitespace-nowrap">
|
|
{clue.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* 点击区域 */}
|
|
<div
|
|
className={`
|
|
absolute cursor-pointer transition-all duration-200
|
|
${isCollected ? 'opacity-30 hover:opacity-50' : 'opacity-0 hover:opacity-20'}
|
|
hover:bg-yellow-400/20
|
|
`}
|
|
style={{
|
|
top: clue.position.top,
|
|
left: clue.position.left,
|
|
width: clue.position.width,
|
|
height: clue.position.height,
|
|
}}
|
|
onClick={() => handleClueClick(clue.id, clue.name)}
|
|
title={isCollected ? `${clue.name}(已收集)` : `点击收集${clue.name}`}
|
|
/>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
{/* 右下角-功能处 */}
|
|
<div className="absolute right-6 bottom-12 flex flex-col items-center gap-4 z-10">
|
|
<Image
|
|
src={getStaticImage(`${starName}/reflection/线索提示`)}
|
|
button
|
|
onClick={handleClueHintClick}
|
|
/>
|
|
<Image
|
|
src={getStaticImage(`${starName}/reflection/背包`)}
|
|
button
|
|
onClick={handleBackpackClick}
|
|
/>
|
|
<div ref={clueBookButtonRef} className="relative" data-clue-book-button="true">
|
|
<Image
|
|
src={getStaticImage(`${starName}/reflection/线索簿`)}
|
|
button
|
|
onClick={() => setShowClueBook(true)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 线索提示窗 */}
|
|
<ClueHintModal
|
|
visible={showClueHint}
|
|
backgroundImage={getStaticImage(`${starName}/reflection/scene2/bg`)}
|
|
clues={clueConfigs}
|
|
onClose={() => setShowClueHint(false)}
|
|
sourceContainerRef={containerRef}
|
|
/>
|
|
|
|
{/* 线索簿窗口 */}
|
|
<ClueBookModal
|
|
visible={showClueBook}
|
|
clues={clueConfigs.map((c) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
collected: isClueCollected(c.id),
|
|
}))}
|
|
onClose={handleClueBookClose}
|
|
getClueImage={getClueImage}
|
|
getClueDetailImage={getClueDetailImage}
|
|
getClueHintImage={getClueHintImage}
|
|
/>
|
|
|
|
{/* 成功提示弹窗 */}
|
|
<SuccessAlert
|
|
visible={showSuccessAlert}
|
|
title="恭喜你!"
|
|
content={successAlertContent}
|
|
audioSrc={
|
|
successAlertContent === '获得1条线索,已获得线索可在线索簿中查看!'
|
|
? getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_49.mp3')
|
|
: undefined
|
|
}
|
|
onContinue={() => {
|
|
setShowSuccessAlert(false)
|
|
setSuccessAlertContent('')
|
|
// 检查是否有待显示的题目或完成提示
|
|
if (pendingQuiz === 'quiz2') {
|
|
setShowQuiz2(true)
|
|
setPendingQuiz(null)
|
|
} else if (pendingQuiz === 'quiz3') {
|
|
setShowQuiz3(true)
|
|
setPendingQuiz(null)
|
|
} else if (pendingQuiz === 'complete') {
|
|
triggerPuzzleAnimation(13)
|
|
setShowCompleteGuide(true)
|
|
setPendingQuiz(null)
|
|
}
|
|
}}
|
|
onClose={() => {
|
|
setShowSuccessAlert(false)
|
|
setSuccessAlertContent('')
|
|
// 检查是否有待显示的题目或完成提示
|
|
if (pendingQuiz === 'quiz2') {
|
|
setShowQuiz2(true)
|
|
setPendingQuiz(null)
|
|
} else if (pendingQuiz === 'quiz3') {
|
|
setShowQuiz3(true)
|
|
setPendingQuiz(null)
|
|
} else if (pendingQuiz === 'complete') {
|
|
setShowCompleteGuide(true)
|
|
setPendingQuiz(null)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* 学习引导弹窗 */}
|
|
<StudyGuideModal
|
|
visible={showGuide}
|
|
onContinue={handleGuideClose}
|
|
zIndex={10000}
|
|
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_51.mp3')}
|
|
>
|
|
1921年7月,中国共产党第一次全国代表大会在上海召开,中国革命的面貌自此焕然一新。
|
|
<br />
|
|
1921-1923的两年间,你顺利完成了一、二年级的在校学习任务,并在三年级进入工厂实习。身为学子,你专注于业务精进,同时亦非常关注时局的变化。一些同学和工友加入了党组织,你也对此心生向往……
|
|
<br />
|
|
你需要在本场景中找出5条线索,并正确回答所有问题后,才能拨开时局迷雾,进入下一个场景。
|
|
</StudyGuideModal>
|
|
|
|
{/* 题目1:当下无产阶级工人运动面对的敌人主要有哪些?*/}
|
|
<SingleChoiceQuizPanel
|
|
visible={showQuiz1}
|
|
title="【单选】当下无产阶级工人运动面对的敌人主要有哪些?"
|
|
options={[
|
|
{ value: 'A', label: 'A: 地主和旧官僚' },
|
|
{ value: 'B', label: 'B: 军阀势力和帝国主义列强' },
|
|
]}
|
|
correctAnswer="B"
|
|
errorMessage="回答错误,请你再仔细思考一下吧。"
|
|
onSubmit={handleQuiz1Submit}
|
|
zIndex={10001}
|
|
/>
|
|
|
|
{/* 题目2:今天的日记写什么呢? */}
|
|
<SingleChoiceQuizPanel
|
|
visible={showQuiz2}
|
|
title="【单选】今天的日记写什么呢?"
|
|
options={[
|
|
{
|
|
value: 'A',
|
|
label:
|
|
'A.回想一年多来,全国已经组织起了大小罢工上百次。虽然取得过一些胜利,但现在工人们的力量毕竟弱小,我们愈是坚定,便愈将面对敌人疯狂的反扑。若为工人们找到其他方面的朋友,或许方可与荷枪实弹的军阀势力相抗衡。',
|
|
},
|
|
{
|
|
value: 'B',
|
|
label:
|
|
'B.回想一年多来,全国已经组织起了大小罢工上百次,多有捷报。仅依靠工人团结一定能够取得胜利!',
|
|
},
|
|
]}
|
|
correctAnswer="A"
|
|
errorMessage="回答错误,请你再仔细思考一下吧。"
|
|
onSubmit={handleQuiz2Submit}
|
|
zIndex={10001}
|
|
/>
|
|
|
|
{/* 题目3:当下无产阶级工人运动面临困境,你觉得谁可以成为工人们的朋友? */}
|
|
<SingleChoiceQuizPanel
|
|
visible={showQuiz3}
|
|
title="【单选】当下无产阶级工人运动面临困境,你觉得谁可以成为工人们的朋友?"
|
|
options={[
|
|
{
|
|
value: 'A',
|
|
label: 'A.青帮、洪门等颇有势力的会党',
|
|
},
|
|
{
|
|
value: 'B',
|
|
label: 'B.孙中山先生领导的中国国民党',
|
|
},
|
|
]}
|
|
correctAnswer="B"
|
|
errorMessage="回答错误,请你再仔细思考一下吧。"
|
|
onSubmit={handleQuiz3Submit}
|
|
zIndex={10001}
|
|
/>
|
|
|
|
{/* 完成提示弹窗 */}
|
|
<EndingOverlay
|
|
visible={showCompleteGuide}
|
|
text={`恭喜你完成场景二的探索!\n1922年夏到1924年初,李大钊来到上海,在宣传革命思想的同时与孙中山进行了数次会面,为改组国民党和促成第一次国共合作作出了巨大贡献。\n1924年1月,中国国民党第一次全国代表大会在广州召开,孙中山重新解释了三民主义,事实上确立了联俄、联共、扶助农工三大政策。李大钊、毛泽东等10名共产党员当选为中国国民党第一届中央执行委员会委员或候补委员。国民党一大的召开标志着第一次国共合作的正式形成。\n请你走进场景三,继续探索吧!`}
|
|
animationSpeed={200}
|
|
onContinue={handleCompleteGuideClose}
|
|
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_52.mp3')}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default SunzhongshanReflectionScene2
|