Files
shanghai-residences/src/pages/zhangleping/place/place3.tsx
T
zhongyidianandClaude Opus 4.8 73f6df92ce chore: 更新素材和组件优化
- 更新展品图片(鲁迅/毛泽东/孙中山/黄炎培/聂耳/张乐平故居)
- 新增点击查看图标、报纸素材
- 优化视频组件、拼图组件、展品组件
- 更新陈云音频资源
- 新增输入验证文档和工具函数
- 组件布局和动画优化

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:50:40 +08:00

439 lines
16 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,
SingleChoiceQuizPanel,
Video,
type VideoRef,
} from '@/components'
import { useNavigate, useParams } from 'react-router-dom'
import { getBackgroundImage, getStaticImage, getPublicPath } from '@/lib/utils'
import { 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 { saveZhanglepingStore, useZhanglepingStore } from '@/store/zhanglepingStore'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
// starName 固定为 zhangleping
const starName = 'zhangleping'
// 展品 ID 与资源路径映射
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
1: 'zhangleping/place3/张乐平场景3展品3',
2: 'zhangleping/place3/张乐平场景3展品2',
3: 'zhangleping/place3/张乐平场景3展品1',
}
// 阶段三:三毛之父(3 个光标)
const ZhanglepingPlace3 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
const { triggerPuzzleAnimation } = usePuzzleAnimation()
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(null)
const place = 'place3'
const { markExhibitVisited, getVisitedExhibits, setLastVisitedPlace } = useZhanglepingStore()
const visitedExhibits = getVisitedExhibits(place) as (1 | 2 | 3)[]
const visited = new Set<1 | 2 | 3>(visitedExhibits)
const [guideVisible1, setGuideVisible1] = useState(false)
const [guideStep1, setGuideStep1] = useState(0)
// 展品2对话已移除(保留占位,避免未来扩展时大改)
const [_guideVisible2, _setGuideVisible2] = useState(false)
const [_guideStep2, _setGuideStep2] = useState(0)
const [guideVisible3, setGuideVisible3] = useState(false)
const [guideStep3, setGuideStep3] = useState(0)
// 展品2:关闭后播放的视频 + 单选题
const [showVideo2, setShowVideo2] = useState(false)
const video2Ref = useRef<VideoRef>(null)
const [quiz2Visible, setQuiz2Visible] = useState(false)
const [successAlert2Visible, setSuccessAlert2Visible] = useState(false)
const [successAlert2Content, setSuccessAlert2Content] = useState('')
// 展品3对话结束后的讲解视频 + 两道单选题
const [showVideo3, setShowVideo3] = useState(false)
const video3Ref = useRef<VideoRef>(null)
const [quiz3Visible, setQuiz3Visible] = useState(false)
const [quiz3Step, setQuiz3Step] = useState<1 | 2>(1)
const [quiz3Finished, setQuiz3Finished] = useState(false)
const [successAlert3Visible, setSuccessAlert3Visible] = useState(false)
const [successAlert3Content, setSuccessAlert3Content] = useState('')
const { isEnabled } = useCursorController({
initialEnabled: [3],
dependencies: {
2: [3],
1: [2],
},
visited,
})
if (!moduleName) return null
const total = 3
const progressCount = visited.size
const completed = progressCount >= total
const subtitle = completed ? '已完成' : `${progressCount}/${total}`
const handleOpenExhibit = (id: 1 | 2 | 3) => {
setActiveExhibitId(id)
if (id === 1) {
setGuideVisible1(true)
setGuideStep1(0)
} else if (id === 2) {
// 展品2:去掉对话,打开弹窗同时直接播放讲解视频(其余逻辑保持不变)
setShowVideo3(true)
} else {
setGuideVisible3(true)
setGuideStep3(0)
}
}
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/place3/张乐平场景3`),
}}
>
{/* 标题 */}
<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}/place2`)
}
}}
/>
{/* 光标1 */}
<Cursor
className="absolute left-202 bottom-109"
size={0.8}
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute right-178 bottom-62"
size={0.8}
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
<Cursor
className="absolute right-58 bottom-68"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{/* 继续前进 */}
{completed && (
<Image
className="absolute bottom-0 right-0"
src={getStaticImage('components/place/继续前进')}
button
onClick={() => {
setLastVisitedPlace('place4')
saveZhanglepingStore()
navigate(`/${moduleName}/${starName}/place4`)
}}
/>
)}
{/* 展品1:三毛的诞生 */}
<GuideSteps
visible={guideVisible1}
onVisibleChange={setGuideVisible1}
currentStep={guideStep1}
onStepChange={setGuideStep1}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_12.mp3')}
>
那我明白了!这个戴上红领巾的三毛体现的应该是已经摆脱苦难的中国人民了。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_13_1.mp3')}
>
确实如此。这幅名叫《向日葵》的画选自于1950—1965年的《三毛日记》。像这样的“新三毛”形象再次发生重要转变是自1949年新中国成立之后,三毛戴上红领巾,成为新中国的少先队员。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_13_2.mp3')}
>
这一变化不仅标志着角色自身的成长,更是整个中国社会由战乱走向安定、由贫弱迈向新生的时代缩影。三毛那三根标志性的头发,仿佛穿越时光的见证者,记录下一个民族从山河破碎到国泰民安的巨大变迁。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_13_3.mp3')}
>
他的每一次蜕变,不仅是一个漫画人物的成长,更是一个国家与一代人的共同记忆。
</GuideStepsItem>
</GuideSteps>
{/* 展品2:三毛形象的特点(原对话已移除) */}
{/* 现改为:打开展品窗口的同时直接播放讲解视频 */}
{/* 展品3:三毛的社会影响 */}
<GuideSteps
visible={guideVisible3}
onVisibleChange={(visible) => {
setGuideVisible3(visible)
// 对话结束后依次进入两道题目(仅在未完成时触发)
if (!visible) {
setQuiz3Step(1)
setQuiz3Visible(true)
}
}}
currentStep={guideStep3}
onStepChange={setGuideStep3}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_10.mp3')}
>
小沪,我一直知道张乐平先生被称为“三毛之父”,但是今天才发现张先生笔下成长时期的三毛看起来比早期的三毛多了一些磨难,张先生塑造的这样一个小流浪儿也在跟着时代一起成长吗?
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_11.mp3')}
>
你的感受很准确。三毛初次亮相于1935年的上海,他的创作者张乐平先生也开创了中国无文字儿童连环漫画之先河。
</GuideStepsItem>
</GuideSteps>
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 3) {
triggerPuzzleAnimation(6)
}
if (activeExhibitId === 2) {
triggerPuzzleAnimation(7)
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(8)
}
}}
onClose={() => {
if (activeExhibitId !== null) {
// 展品2:关闭后先播放一段视频,视频结束再进入题目
if (activeExhibitId === 2) {
setActiveExhibitId(null)
setShowVideo2(true)
return
}
// 展品3:需完成两道题目后才标记为已访问
if (activeExhibitId === 3) {
if (quiz3Finished) {
markExhibitVisited(place, 3)
}
} else {
markExhibitVisited(place, activeExhibitId)
}
}
setActiveExhibitId(null)
}}
/>
{/* 展品2关闭后的讲解视频 */}
{showVideo2 && (
<Video
ref={video2Ref}
src={getPublicPath('video/zhangleping/zhangleping_5.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
skipKey="zhangleping_place3_video1"
showReadonlyProgress
onEnded={() => {
setShowVideo2(false)
setQuiz2Visible(true)
}}
onSkip={() => {
setShowVideo2(false)
setQuiz2Visible(true)
}}
/>
)}
{/* 展品2关闭后的视频 + 单选题(如图1) */}
<ScaledPortal enabled={quiz2Visible || successAlert2Visible} zIndex={10040}>
<>
<SingleChoiceQuizPanel
visible={quiz2Visible}
zIndex={10000}
title="【单选】张乐平先生根据自己抗战前线的经历见闻创作的作品是:"
options={[
{ value: 'A', label: 'A.三毛流浪记' },
{ value: 'B', label: 'B.三毛日记' },
{ value: 'C', label: 'C.三毛从军记' },
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === 'A') {
return '《三毛流浪记》以旧上海为背景,聚焦三毛在城市底层的流浪生活,主要反映旧社会儿童的苦难遭遇与人性冷暖。请你再想想吧!'
}
if (value === 'B') {
return '《三毛日记》以日记体形式记录三毛的日常琐事与童真视角,内容偏向儿童生活的趣味表达。请你再想想吧!'
}
return '请你再想想吧!'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuiz2Visible(false)
setSuccessAlert2Content(
'《三毛从军记》是张乐平根据自己奔赴抗战前线的亲身经历与见闻创作的作品,通过三毛从军的荒诞经历,讽刺了当时的社会乱象,也传递出抗战背景下的民生百态。'
)
setSuccessAlert2Visible(true)
}}
/>
<SuccessAlert
visible={successAlert2Visible}
content={successAlert2Content}
onContinue={() => {
setSuccessAlert2Visible(false)
markExhibitVisited(place, 2)
}}
/>
</>
</ScaledPortal>
{showVideo3 && (
<ScaledPortal enabled={true} zIndex={10050}>
<Video
ref={video3Ref}
src={getPublicPath('video/zhangleping/zhangleping_01.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
skipKey="zhangleping_place3_video2"
showReadonlyProgress
onEnded={() => {
setShowVideo3(false)
}}
onSkip={() => {
setShowVideo3(false)
}}
/>
</ScaledPortal>
)}
{/* 展品3对话结束后的两道单选题 + 每题答对后的提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quiz3Visible || successAlert3Visible} zIndex={10050}>
<>
{/* 题目1:三毛形象首次亮相年份与地点 */}
<SingleChoiceQuizPanel
visible={quiz3Visible && quiz3Step === 1}
zIndex={10000}
title="【单选】1935年,张乐平先生创作的三毛形象初次亮相于:"
options={[
{ value: 'A', label: 'A.上海' },
{ value: 'B', label: 'B.延安' },
{ value: 'C', label: 'C.浙江' },
]}
correctAnswer="A"
onWrongSelect={(value) => {
if (value === 'B') {
return '延安是当时的革命根据地,但张乐平先生的主要创作活动和延安没有关联。请你再想想吧!'
}
if (value === 'C') {
return '浙江是张乐平先生的故乡,不是三毛初次亮相的地方。请你再想想吧!'
}
return '请再仔细阅读题干中的时间和地点信息~'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuiz3Visible(false)
setSuccessAlert3Content('张乐平先生创作的三毛初次亮相于1935年上海的《晨报》副刊。')
setSuccessAlert3Visible(true)
}}
/>
{/* 题目2:三毛与中国无文字儿童连环漫画 */}
<SingleChoiceQuizPanel
visible={quiz3Visible && quiz3Step === 2}
zIndex={10000}
title="【判断】张乐平先生创作的三毛开创了中国无文字儿童连环漫画之先河。"
options={[
{ value: 'A', label: 'A.正确' },
{ value: 'B', label: 'B.错误' },
]}
correctAnswer="A"
onWrongSelect={(value) => {
if (value === 'B') {
return '再想一想三毛在中国儿童漫画史上的地位吧~'
}
return '请再仔细对照题干信息选择~'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuiz3Visible(false)
setSuccessAlert3Content('')
setSuccessAlert3Visible(true)
}}
/>
{/* 每题答对后的统一提示框 */}
<SuccessAlert
visible={successAlert3Visible}
title={quiz3Step === 2 ? '恭喜你,判断正确!' : '恭喜你,选择正确!'}
content={successAlert3Content}
onContinue={() => {
setSuccessAlert3Visible(false)
// 进入下一题或标记完成
setQuiz3Step((prev) => {
const next = (prev + 1) as 1 | 2 | 3
if (next === 2) {
setQuiz3Visible(true)
return 2
}
setQuiz3Finished(true)
markExhibitVisited(place, 3)
return prev
})
}}
/>
</>
</ScaledPortal>
</div>
)
}
export default ZhanglepingPlace3