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

630 lines
23 KiB
TypeScript

import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
EndingOverlay,
Image,
Title,
Video,
type VideoRef,
SingleChoiceQuizPanel,
} from '@/components'
import { useNavigate, useParams } from 'react-router-dom'
import { getBackgroundImage, getStaticImage, getPublicPath } 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'
import { setCourseTimeProgress } from '@/lib/utils/api-common'
// starName 固定为 maozedong
const starName = 'maozedong'
// 展品 ID 与资源路径映射,ID 用来做唯一标识
const exhibitSrcMap: Record<1 | 2 | 3 | 4 | 5, string> = {
1: 'maozedong/place4/场景4展品1',
2: 'maozedong/place4/场景4展品2_',
3: 'maozedong/place4/场景4展品4',
4: 'maozedong/place4/场景4展品5',
5: 'maozedong/place4/场景4展品6',
}
const MaozedongPlace4 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
// 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 场景级引导对话显示状态(暂未使用,可根据需要开启)
const [visible, setVisible] = useState(false)
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | 5 | null>(null)
// 持久化接入
const markVisited = useMaozedongStore((s) => s.markExhibitVisited)
const getVisited = useMaozedongStore((s) => s.getVisitedExhibits)
const persistedVisited = useMemo(
() => new Set(getVisited('place4') as (1 | 2 | 3 | 4 | 5)[]),
[getVisited]
)
const [visited, setVisited] = useState<Set<1 | 2 | 3 | 4 | 5>>(() => new Set(persistedVisited))
// 光标1的对话状态
const [guide1Visible, setGuide1Visible] = useState(false)
const [guide1Step, setGuide1Step] = useState(0)
// 光标3的对话状态
const [guide3Visible, setGuide3Visible] = useState(false)
const [guide3Step, setGuide3Step] = useState(0)
// 光标4(展品4)对话
// 光标4关闭后的过场视频与总结对话
const [showVideo4, setShowVideo4] = useState(false)
const [guide4SummaryVisible, setGuide4SummaryVisible] = useState(false)
const [guide4SummaryStep, setGuide4SummaryStep] = useState(0)
const [endingVisible, setEndingVisible] = useState(false)
// 光标2的对话状态
const [guide2Visible, setGuide2Visible] = useState(false)
const [guide2Step, setGuide2Step] = useState(0)
// 光标2的视频与题目状态
const [showVideo2, setShowVideo2] = useState(false)
const video2Ref = useRef<VideoRef>(null)
const [quizStep, setQuizStep] = useState(0) // 0 表示未开始,1-4 为四道题
const [quizVisible, setQuizVisible] = useState(false)
const [successAlertVisible, setSuccessAlertVisible] = useState(false)
const [successAlertContent, setSuccessAlertContent] = useState<string>('')
// Cursor 顺序控制:1 -> 2 -> 3 -> 4 -> 5 -> 6
const { isEnabled } = useCursorController({
initialEnabled: [2],
dependencies: {
1: [2],
3: [1],
4: [3, 5],
5: [1],
},
visited,
})
const handleOpenExhibit = (id: 1 | 2 | 3 | 4 | 5) => {
setActiveExhibitId(id)
if (id === 1) {
setGuide1Step(0)
setGuide1Visible(true)
}
if (id === 2) {
setGuide2Step(0)
setGuide2Visible(true)
setQuizStep(0)
}
if (id === 3) {
setGuide3Step(0)
setGuide3Visible(true)
}
}
if (!moduleName) return null
const progressCount = visited.size
const total = 5
const completed = progressCount >= total
const subtitle = completed ? '已完成' : `${progressCount}/${total}`
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/place4/场景4`),
}}
>
{/* 标题 */}
<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}/place3`)
}
}}
/>
{completed && (
<Image
className="absolute right-0 bottom-0"
src={getStaticImage('components/place/离开场景')}
button
onClick={() => {
if (moduleName && starName) {
saveMaozedongStore()
setCourseTimeProgress(1)
navigate(`/${moduleName}/${starName}/home`)
}
}}
/>
)}
{/* 光标1 */}
<Cursor
className="absolute left-105 bottom-158"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute left-199 bottom-162"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
{/* <Cursor
className="absolute left-215 bottom-163"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/> */}
{/* 光标3 */}
<Cursor
className="absolute left-212 bottom-123"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{/* 光标4 */}
<Cursor
className="absolute right-186 bottom-162"
onClick={() => handleOpenExhibit(4)}
read={visited.has(4)}
disabled={!isEnabled(4)}
/>
{/* 光标5 */}
<Cursor
className="absolute right-153 bottom-123"
onClick={() => handleOpenExhibit(5)}
read={visited.has(5)}
disabled={!isEnabled(5)}
/>
{/* 场景对话 */}
<GuideSteps
visible={visible}
onVisibleChange={(nextVisible) => {
setVisible(nextVisible)
}}
showMask={false}
>
<GuideStepsItem speaker="student">小沪,毛泽东是如何组织和发动工农群众的?</GuideStepsItem>
<GuideStepsItem speaker="guide">
1925年,毛泽东回到湖南,深入农村调查研究,写出了《湖南农民运动考察报告》。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
他认识到农民是中国革命的主力军,只有发动农民,革命才能成功。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
毛泽东在各地建立农民协会,组织农民开展减租减息、反对封建压迫的斗争。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
同时,他也重视工人运动,在安源、长沙等地组织工人罢工,提高工人的政治觉悟。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
通过这些工作,毛泽东为后来的革命斗争奠定了坚实的群众基础。
</GuideStepsItem>
</GuideSteps>
{/* 光标1的对话:点击展品1时出现 */}
<GuideSteps
visible={guide1Visible}
currentStep={guide1Step}
onStepChange={setGuide1Step}
onVisibleChange={(nextVisible) => {
setGuide1Visible(nextVisible)
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_15.mp3')}
>
看来中共四大明确提出了“无产阶级领导权”和“工农联盟”,这和毛主席的实践有关吗?
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_16_1.mp3')}
>
太有关系了!中共三大后毛主席到上海工作了一段时间,看到工人运动轰轰烈烈,但农民还是“沉默的大多数”。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_16_2.mp3')}
>
后来毛主席回湖南,专门跑了湘潭、湘乡、衡山、醴陵、长沙五县,发现农民不仅能革命,而且革命性极强——他们被剥削得最狠,
反抗起来也最坚决!
</GuideStepsItem>
</GuideSteps>
{/* 光标2的对话:点击展品2时出现 */}
<GuideSteps
visible={guide2Visible}
currentStep={guide2Step}
onStepChange={setGuide2Step}
onVisibleChange={(nextVisible) => {
setGuide2Visible(nextVisible)
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_13.mp3')}
>
那中共四大为什么召开呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_14.mp3')}>
因为此前中共三大虽然确定了国共合作的方针,但未明确解决“谁领导革命”的问题。我们必须要明确“谁领导革命”“依靠谁革命”这两个基本问题。
</GuideStepsItem>
</GuideSteps>
{/* 光标3的对话:点击展品3时出现(图1) */}
<GuideSteps
visible={guide3Visible}
currentStep={guide3Step}
onStepChange={setGuide3Step}
onVisibleChange={(nextVisible) => {
setGuide3Visible(nextVisible)
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_17.mp3')}
>
所以毛主席觉得,光靠工人不够,得把农民拉进革命队伍?
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_18_01.mp3')}
>
对!无产阶级要领导革命,必须联合农民——他们是工人阶级的“天然盟友”。中共四大虽然还没有明确提出“农村包围城市”,但已经意识到农民问题的重要性。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_18_02.mp3')}
>
后来毛主席常说:“国民革命的中心问题,就是农民问题,一切都要靠农民问题的解决。”这句话的种子,就是在中共四大前后埋下的。
</GuideStepsItem>
</GuideSteps>
{/* 光标4视频结束后出现的总结对话(图1) */}
<GuideSteps
visible={guide4SummaryVisible}
currentStep={guide4SummaryStep}
onStepChange={setGuide4SummaryStep}
onVisibleChange={(nextVisible) => {
setGuide4SummaryVisible(nextVisible)
if (!nextVisible) {
setEndingVisible(true)
}
}}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_19.mp3')}
>
为有牺牲多壮志,敢教日月换新天。小沪,通过您的讲解,我明白了中共一大到四大是在艰难探索中不断前进的——从建党的理想,到制定民主革命纲领;从国共合作,到发现农民的力量。这些探索对我们今天有什么启示呢?
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_20_1.mp3')}
>
同学问到了根子上!革命就像爬山,一开始可能看不清山顶的样子,但只要沿着正确的方向,一步一步扎扎实实地走,总会离目标越来越近。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_20_2.mp3')}
>
你们现在学历史,不能只记年代和事件,更要理解“为什么”——为什么马克思主义要和中国实际相结合?为什么农民是革命的重要力量?把这些道理想通了,将来不管做什么事,都能脚踏实地。
</GuideStepsItem>
<GuideStepsItem
speaker="guide"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_20_3.mp3')}
>
“实事求是”这四个字,是我们党一路走来的法宝,希望你也能记住。
</GuideStepsItem>
</GuideSteps>
{/* 展品遮罩层 */}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 2) {
triggerPuzzleAnimation(8)
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(9)
}
if (activeExhibitId === 3) {
triggerPuzzleAnimation(10)
}
if (activeExhibitId === 4) {
triggerPuzzleAnimation(12)
}
if (activeExhibitId === 5) {
triggerPuzzleAnimation(11)
}
}}
onClose={() => {
const currentId = activeExhibitId
// 展品2:关闭后播放视频和答题,全部正确后再标记已访问
if (currentId === 2) {
setGuide2Step(0)
setGuide2Visible(false)
setActiveExhibitId(null)
setShowVideo2(true)
return
}
// 展品4:关闭后播放视频,视频结束后显示总结对话,并立即标记已访问
if (currentId === 4) {
setActiveExhibitId(null)
setVisited((prev) => {
const next = new Set(prev)
next.add(4)
markVisited('place4', 4)
saveMaozedongStore()
return next
})
setShowVideo4(true)
return
}
if (currentId !== null) {
setVisited((prev) => {
const next = new Set(prev)
next.add(currentId)
markVisited('place4', currentId)
saveMaozedongStore()
return next
})
}
setActiveExhibitId(null)
}}
/>
<EndingOverlay
visible={endingVisible}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_21.mp3')}
text="我们已经了解了毛主席1924年前后居住在上海时所经历的重要事件。接下来,请你跨越时空,走进风云激荡的1924年,通过参与毛主席在筹备中共四大期间做出的几次关键选择,体悟早期共产党人探索革命道路的初心与智慧,体悟理论与实践结合的深刻意义。"
onContinue={() => {
setEndingVisible(false)
}}
animationSpeed={200}
/>
{/* 光标4:展品关闭后播放的视频 */}
{showVideo4 && (
<Video
src={getPublicPath('video/maozedong/mzd_02.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
skipKey="maozedong_place4_video1"
showReadonlyProgress
onSkip={() => {
setShowVideo4(false)
setGuide4SummaryStep(0)
setGuide4SummaryVisible(true)
}}
onEnded={() => {
setShowVideo4(false)
setGuide4SummaryStep(0)
setGuide4SummaryVisible(true)
}}
/>
)}
{/* 光标2:展品关闭后播放的视频 */}
{showVideo2 && (
<Video
ref={video2Ref}
src={getPublicPath('video/maozedong/mzd_5.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
skipKey="maozedong_place4_video2"
showReadonlyProgress
onEnded={() => {
setShowVideo2(false)
setQuizStep(1)
setQuizVisible(true)
}}
/>
)}
{/* 光标2:四道单选题 + 每题答对后的提示 */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
{/* 题目1:中共四大召开时间 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 1}
zIndex={10000}
title="【单选】中共四大召开的时间是:"
options={[
{ value: 'A', label: 'A.1923年6月' },
{ value: 'B', label: 'B.1924年1月' },
{ value: 'C', label: 'C.1925年1月' },
{ value: 'D', label: 'D.1926年7月' },
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === 'A') {
return '1923年6月,中共三大召开,确定了国共合作方针。请你再想想吧!'
}
if (value === 'B') {
return '1924年1月,国民党一大召开,国共合作正式形成。请你再想想吧!'
}
if (value === 'D') {
return '1926年7月,北伐战争开始。请你再想想吧!'
}
return '请结合时间线再仔细思考一下~'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuizVisible(false)
setSuccessAlertContent('中共四大于1925年1月11日至22日在上海召开。')
setSuccessAlertVisible(true)
}}
/>
{/* 题目2:召开地点 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 2}
zIndex={10000}
title="【单选】中共四大召开的地点是:"
options={[
{ value: 'A', label: 'A.北京' },
{ value: 'B', label: 'B.上海' },
{ value: 'C', label: 'C.广州' },
{ value: 'D', label: 'D.武汉' },
]}
correctAnswer="B"
onWrongSelect={(value) => {
if (value === 'A') {
return '现在中国共产党全国代表大会多在北京召开,但是中共四大不是哦!请你再想想吧!'
}
if (value === 'C') {
return '中共三大在广州召开,请你再想想吧!'
}
if (value === 'D') {
return '中共五大在武汉召开,请你再想想吧!'
}
return '请再仔细对照题干信息选择~'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuizVisible(false)
setSuccessAlertContent('')
setSuccessAlertVisible(true)
}}
/>
{/* 题目3:首次明确提出的重要问题 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 3}
zIndex={10000}
title="【单选】中共四大第一次明确提出的重要问题是:"
options={[
{ value: 'A', label: 'A.党的建设问题' },
{ value: 'B', label: 'B.武装斗争问题' },
{ value: 'C', label: 'C.无产阶级领导权和工农联盟问题' },
{ value: 'D', label: 'D.土地革命问题' },
]}
correctAnswer="C"
onWrongSelect={(value) => {
if (value === 'A') {
return '古田会议更系统阐述党的建设问题哦!请你再想想吧!'
}
if (value === 'B') {
return '八七会议更系统阐述武装斗争问题哦!请你再想想吧!'
}
if (value === 'D') {
return '八七会议后提出土地革命问题哦!请你再想想吧!'
}
return '请再仔细比较不同会议的主要议题~'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuizVisible(false)
setSuccessAlertContent('')
setSuccessAlertVisible(true)
}}
/>
{/* 题目4:党的基本组织形式 */}
<SingleChoiceQuizPanel
visible={quizVisible && quizStep === 4}
zIndex={10000}
title="【单选】中共四大确立的党的基本组织形式是:"
options={[
{ value: 'A', label: 'A.组' },
{ value: 'B', label: 'B.支部' },
// { value: "C", label: "C.区委" },
// { value: "D", label: "D.中央局" },
]}
correctAnswer="B"
onWrongSelect={(value) => {
if (value === 'A') {
return '中共四大以前党的基本组织形式是组或者小组,请你再想想吧!'
}
return '请注意中共四大之后对基层组织形式的规定~'
}}
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuizVisible(false)
setSuccessAlertContent(
'中共四大明确“支部是党的基本组织”,规定“有三名党员即可成立一个支部”。'
)
setSuccessAlertVisible(true)
// 全部答对后标记展品2为已访问
setVisited((prev) => {
const next = new Set(prev)
next.add(2)
return next
})
}}
/>
{/* 每题答对后的统一提示框 */}
<SuccessAlert
visible={successAlertVisible}
content={successAlertContent}
onContinue={() => {
setSuccessAlertVisible(false)
//答题结束,标记展品4为已查看
markVisited('place4', 2)
// 进入下一题
setQuizStep((prev) => {
const next = prev + 1
if (next >= 1 && next <= 4) {
setQuizVisible(true)
}
return next
})
}}
/>
</>
</ScaledPortal>
</div>
)
}
export default MaozedongPlace4