Files
shanghai-residences/src/pages/maozedong/place/place1.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

320 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,
StudyGuideModal,
Title,
SelectFillQuizPanel,
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 type { SelectFillResult } from '@/components/layout/quiz'
import { saveMaozedongStore, useMaozedongStore } from '@/store/maozedongStore'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
// starName 固定为 maozedong
const starName = 'maozedong'
// 展品 ID 与资源路径映射,ID 用来做唯一标识
const exhibitSrcMap: Record<1 | 2, string> = {
1: 'maozedong/place1/场景1展品1',
2: 'maozedong/place1/场景1展品2',
}
const MaozedongPlace1 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
// 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 接入持久化:记录本场景 place1 的已访问展品与断点
const markVisited = useMaozedongStore((s) => s.markExhibitVisited)
const getVisited = useMaozedongStore((s) => s.getVisitedExhibits)
const setLastVisitedPlace = useMaozedongStore((s) => s.setLastVisitedPlace)
const persistedVisited = useMemo(() => new Set(getVisited('place1') as (1 | 2)[]), [getVisited])
// 学习引导弹窗
const [showGuideModal, setShowGuideModal] = useState(true)
// 对话结束后的学习引导弹窗
const [showAfterDialogGuide, setShowAfterDialogGuide] = useState(false)
// 展品2的对话引导状态
const [guideVisible2, setGuideVisible2] = useState(false)
const [guideStep2, setGuideStep2] = useState(0)
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null)
// 已访问的展品(初始化自 store;随后每次关闭展品时写回)
const [visited, setVisited] = useState<Set<1 | 2>>(() => new Set(persistedVisited))
// 展品2关闭后播放的视频 + 选择填空题
const [showVideo2, setShowVideo2] = useState(false)
const video2Ref = useRef<VideoRef>(null)
const [quizVisible, setQuizVisible] = useState(false)
const [successAlertVisible, setSuccessAlertVisible] = useState(false)
// Cursor 顺序控制:1 -> 2
const { isEnabled } = useCursorController({
initialEnabled: [2],
dependencies: {
1: [2],
},
visited,
})
const handleOpenExhibit = (id: 1 | 2) => {
setActiveExhibitId(id)
// 根据展品ID显示对应的对话
if (id === 1) {
setGuideVisible2(true)
setGuideStep2(0)
}
}
// 监听展品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}/place1/场景1`),
}}
>
{/* 标题 */}
<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}/home`)
}
}}
/>
{/* 光标1 */}
<Cursor
className="absolute left-106 bottom-150"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute left-170 bottom-135"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
{completed && (
<Image
className="absolute bottom-0 right-140"
src={getStaticImage(`${starName}/place1/场景1继续前进`)}
button
onClick={() => {
setLastVisitedPlace('place2')
saveMaozedongStore()
navigate(`/${moduleName}/${starName}/place2`)
}}
/>
)}
{/* 展品2的对话引导 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={handleGuideVisibleChange2}
currentStep={guideStep2}
onStepChange={handleGuideStepChange2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_03.mp3')}
>
那参加中共一大时,毛主席对未来有什么想法吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_04.mp3')}>
当时全国只有50多个党员,毛主席所在的湖南也才几个。但他觉得,重要的是把马克思主义和中国的实际情况结合起来。就像种田,种子再好,也得看土壤——中国的
“土壤”就是农民占大多数,这是和苏联不一样的地方。
</GuideStepsItem>
</GuideSteps>
{/* 展品遮罩层 */}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 2) {
triggerPuzzleAnimation(1)
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(2)
}
}}
onClose={() => {
if (activeExhibitId === 2) {
setActiveExhibitId(null)
setShowVideo2(true)
return
}
if (activeExhibitId !== null) {
setVisited((prev) => {
const next = new Set(prev)
next.add(activeExhibitId)
// 写回持久化
markVisited('place1', activeExhibitId)
return next
})
}
setActiveExhibitId(null)
}}
/>
{/* 展品2关闭后播放的视频 */}
{showVideo2 && (
<Video
ref={video2Ref}
src={getPublicPath('video/maozedong/mzd_00.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
skipKey="maozedong_place1_video"
showReadonlyProgress
onEnded={() => {
setShowVideo2(false)
setQuizVisible(true)
}}
onSkip={() => {
setShowVideo2(false)
setQuizVisible(true)
}}
/>
)}
{/* 展品2对话结束后的选择填空题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
<SelectFillQuizPanel
visible={quizVisible}
titlePrefix="【填空】毛泽东主席认为,到了"
titleSuffix="年夏天,在理论上,而且在某种程度的行动上,他已成为一个马克思主义者了。"
blanks={[{ id: 'd1' }, { id: 'd2' }, { id: 'd3' }, { id: 'd4' }]}
options={[
{ value: '0', label: '0' },
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' },
{ value: '5', label: '5' },
{ value: '6', label: '6' },
{ value: '7', label: '7' },
{ value: '8', label: '8' },
{ value: '9', label: '9' },
]}
correctAnswers={{
d1: '1',
d2: '9',
d3: '2',
d4: '0',
}}
errorMessage="1920年5月5日到7月初,在上海居住的2个月期间,毛主席的思想完成了向马克思主义者的转变。请你再想想吧!"
onSubmit={(_results: SelectFillResult[], isCorrect: boolean) => {
if (isCorrect) {
setQuizVisible(false)
setSuccessAlertVisible(true)
}
}}
onError={(_error: string) => {
// 错误时仅提示,再让学生重做
}}
submitText="确定"
zIndex={10000}
/>
<SuccessAlert
visible={successAlertVisible}
onContinue={() => {
setSuccessAlertVisible(false)
setVisited((prev) => {
const next = new Set(prev)
next.add(2)
markVisited('place1', 2)
return next
})
}}
/>
</>
</ScaledPortal>
{/* 学习引导词 */}
<StudyGuideModal
visible={showGuideModal}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_02.mp3')}
onContinue={() => {
setShowGuideModal(false)
setShowAfterDialogGuide(true)
}}
ailgn="left"
>
1.场景中出现动态光点的展品表示未浏览,可通过点击光点来了解展品背后的故事,通过滚轮可以放大展品,浏览展品后蓝色动态光点会变为已浏览状态。
{'\n'}
2.在学习过程中会掉落拼图碎片,拼图碎片会自动收入拼图盒中,拼图盒可以点开查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习。
{'\n'}
3.所有动态光点的展品全部学习后才能进入下一个场景学习。
</StudyGuideModal>
{/* 对话结束后的学习引导 */}
<StudyGuideModal
visible={showAfterDialogGuide}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_01.mp3')}
onContinue={() => setShowAfterDialogGuide(false)}
firstLineIndent="2em"
>
{`同学你好,欢迎来到“走进故居·对话”学习模块!
此刻你正站在上海毛泽东旧居陈列馆的门口,接下来请跟着时光的脚步,一同回望毛泽东主席人生中4段风云激荡的岁月:摸索方向·确立信仰、播撒红色火种、吹响合作号角、唤起工农群众。准备好推开这扇门,走进那段滚烫的峥嵘岁月了吗?`}
</StudyGuideModal>
</div>
)
}
export default MaozedongPlace1