feat:修改英文双引号

This commit is contained in:
kevinzhu committed 2026-04-30 09:17:31 +08:00
commit 4a2be3b4d0
1351 files changed
+56547

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
import { useNavigate, useParams } from 'react-router-dom'
import { Image } from '@/components'
import { useMaozedongStore } from '@/store/maozedongStore'
const Maozedong = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
// starName 固定为 maozedong
const starName = 'maozedong'
const lastVisitedPlace = useMaozedongStore((state) => state.lastVisitedPlace)
if (!moduleName) return null
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/毛泽东故居底图`),
}}
>
<Image
className="absolute right-37 top-22"
src={getStaticImage('components/革命先驱故居-返回')}
button
onClick={() => navigate(`/entrance/revolutionary`)}
/>
<div className="absolute right-20 top-75 flex flex-col space-y-10">
<Image
button
src={getStaticImage('star/一-解锁')}
onClick={() => navigate(`/${moduleName}/${starName}/${lastVisitedPlace || 'place1'}`)}
/>
<Image
button
src={getStaticImage('star/二-解锁')}
onClick={() => navigate(`/${moduleName}/${starName}/reflection`)}
/>
<Image
button
src={getStaticImage('star/三-解锁')}
onClick={() => navigate(`/${moduleName}/${starName}/internalization`)}
/>
</div>
</div>
)
}
export default Maozedong
+521
View File
@@ -0,0 +1,521 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
Image,
Video,
type VideoAnchor,
type VideoRef,
type DragFillResult,
SingleChoiceQuizPanel,
LetterToMaozedong,
EndingOverlay,
Narration,
} from '@/components'
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
import { GuideSteps, GuideStepsItem } from '@/components'
import { BasketQuizPanel, DragQuizPanel, SuccessAlert } from '@/components/layout'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
const starName = 'maozedong'
const MaozedongInternalization = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{ moduleName: string }>()
// 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 进页面:先播“引入视频” -> 引入对话(图1) -> 主视频(视频11)
const [showIntroVideo, setShowIntroVideo] = useState(true)
const [introQuizVisible, setIntroQuizVisible] = useState(false)
const [introQuizSuccessVisible, setIntroQuizSuccessVisible] = useState(false)
const [introDialogVisible, setIntroDialogVisible] = useState(false)
const [showMainVideo, setShowMainVideo] = useState(false)
// 题目弹层状态(视频锚点 render 中使用)
const [activeSuccess, setActiveSuccess] = useState<string | null>(null)
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(null)
const [dragQuizValues, setDragQuizValues] = useState<Record<string, DragFillResult[]>>({})
const [dragQuizErrors, setDragQuizErrors] = useState<Record<string, string>>({})
const [dragQuizFeedbackTypes, setDragQuizFeedbackTypes] = useState<
Record<string, 'success' | 'error'>
>({})
// 写信、寄信动画与完成提示
const [showLetter, setShowLetter] = useState(false)
const [showCompleted, setShowCompleted] = useState(false)
const [showTransition, setShowTransition] = useState(false)
const videoRef = useRef<VideoRef>(null)
useEffect(() => {}, [])
const handleVideoCanPlay = () => {
if (showMainVideo && videoRef.current) {
videoRef.current.play().catch(() => {})
}
}
// —— 题目配置:题目2(1:59)、题目3(4:31)、题目4(6:58)、题目5(7:15)——
type DragQuizConfig = {
id: string
time: number
title: string
prompt: string
correct: string[]
wrongValue: string
wrongHint: string
optionHints: Partial<Record<string, string>>
options: { value: string; label: string }[]
blanks: { id: string; label: string }[]
separators?: string[]
}
const DRAG_QUIZZES: DragQuizConfig[] = useMemo(
() => [
{
id: 'quiz-2',
time: 60 * 1 + 59,
title: '',
prompt: '请将你觉得属于新时代坚持“实事求是”思想的做法拖入篮子吧!',
correct: ['A', 'B', 'C'],
wrongValue: 'D',
wrongHint:
'这种做法脱离实际,违背了“实事求是”的思想路线,容易导致发展走弯路。请你再想想吧!',
optionHints: {
A: '恭喜你,选择正确!这是党在精准把握发展阶段特征的基础上作出的科学判断,体现了“实事求是”的思想精髓。',
B: '恭喜你,选择正确!这是从上海实际出发,立足自身优势精准定位发展方向,是“实事求是”在区域发展中的生动体现。',
C: '恭喜你,选择正确!这是立足实际、聚焦问题、务求实效的实践路径,是“实事求是”在改革发展中的具体践行。',
D: '这种做法脱离实际,违背了“实事求是”的思想路线,容易导致发展走弯路。请你再想想吧!',
},
options: [
{
value: 'A',
label: '党的十八大以来,根据世情国情变化作出“社会主要矛盾转化”的新论断',
},
{
value: 'B',
label: '基于上海的区位优势和发展基础,党中央赋予上海建设“五个中心”的使命',
},
{
value: 'C',
label: '上海以问题为导向,推动“五个中心”联动发展,将改革开放“施工图”转化为“实景画”',
},
{ value: 'D', label: '不顾本地资源禀赋,盲目照搬其他地区的产业发展模式' },
],
blanks: [
{ id: 'basket-1', label: '' },
{ id: 'basket-2', label: '' },
{ id: 'basket-3', label: '' },
],
},
{
id: 'quiz-3',
time: 60 * 4 + 32,
title: '',
prompt: '请将你认为符合新时代践行“群众路线”思想的做法拖入篮子吧!',
correct: ['A', 'C', 'D'],
wrongValue: 'B',
wrongHint:
'这种做法违背了以人民为中心的发展思想,不符合新时代群众路线的要求。请你再想想吧!',
optionHints: {
A: '恭喜你,选择正确!这一理念是新时代群众路线在城市治理中的生动表达,体现了以人民为中心的发展思想。',
B: '这种做法违背了以人民为中心的发展思想,不符合新时代群众路线的要求。请你再想想吧!',
C: '恭喜你,选择正确!这是群众路线在民主政治领域的创新实践,保障了人民群众的参与权、表达权,让人民真正成为城市的主人。',
D: '恭喜你,选择正确!这是群众路线在新时代的深化与拓展,充分调动了人民群众的积极性、主动性、创造性。',
},
options: [
{ value: 'A', label: '提出“人民城市人民建,人民城市为人民”的重要理念' },
{ value: 'B', label: '城市在经济发展中只关注企业效益,忽视普通市民的民生需求' },
{
value: 'C',
label: '上海作为全过程人民民主首提地,搭建多元民主平台让群众声音直达决策层面',
},
{
value: 'D',
label: '上海通过推进“全过程人民民主”,汇聚各方智慧共同推进中国式现代化',
},
],
blanks: [
{ id: 'basket-1', label: '' },
{ id: 'basket-2', label: '' },
{ id: 'basket-3', label: '' },
],
},
{
id: 'quiz-4',
time: 60 * 6 + 59,
title: '',
prompt: '请将你认为符合新时代践行“独立自主”思想的做法拖入篮子吧!',
correct: ['B', 'C', 'D'],
wrongValue: 'A',
wrongHint:
'这种做法违背了“独立自主”的原则,会导致发展受制于他人,不符合中国式现代化的本质要求。请你再想想吧!',
optionHints: {
A: '这种做法违背了“独立自主”的原则,会导致发展受制于他人,不符合中国式现代化的本质要求。请你再想想吧!',
B: '恭喜你,选择正确!这是我国践行独立自主思想的典范,彰显了我国在高端制造领域依靠自身力量突破核心技术的决心与能力。',
C: '恭喜你,选择正确!这是上海在改革开放中坚持独立自主、探索制度创新的生动实践,体现了在开放中掌握发展主动权的思路。',
D: '恭喜你,选择正确!这是上海坚持把发展放在自己力量基点上的具体体现,通过打造自主产业集群,在开放合作中牢牢掌握了发展主动权。',
},
options: [
{ value: 'A', label: '部分地区在产业发展中完全依赖国外技术,放弃自主研发投入' },
{
value: 'B',
label: 'C919大飞机研制成功,实现百余项关键技术突破,60%的自主配套率',
},
{
value: 'C',
label: '上海自贸试验区率先试点“负面清单管理”,临港新片区打造全产业链集群',
},
{
value: 'D',
label: '临港新片区布局“东方芯港”“生命蓝湾”等特色产业园区,构建自主可控产业生态',
},
],
blanks: [
{ id: 'basket-1', label: '' },
{ id: 'basket-2', label: '' },
{ id: 'basket-3', label: '' },
],
},
{
id: 'quiz-5',
time: 60 * 7 + 16,
title: '',
prompt: '毛泽东思想活的灵魂包括',
correct: ['实事求是', '群众路线', '独立自主'],
wrongValue: '党的建设',
wrongHint: '党的建设是新民主主义革命胜利的“三大法宝”之一。请你再想想吧!',
optionHints: {
实事求是: '',
群众路线: '',
独立自主: '',
党的建设: '党的建设是新民主主义革命胜利的“三大法宝”之一。请你再想想吧!',
},
options: [
{ value: '实事求是', label: 'A.实事求是' },
{ value: '群众路线', label: 'B.群众路线' },
{ value: '独立自主', label: 'C.独立自主' },
{ value: '党的建设', label: 'D.党的建设' },
],
blanks: [
{ id: 'blank-1', label: '' },
{ id: 'blank-2', label: '' },
{ id: 'blank-3', label: '' },
],
separators: ['、', '、'],
},
],
[]
)
const anchors: VideoAnchor[] = useMemo(() => {
return DRAG_QUIZZES.map((q) => ({
id: q.id,
time: q.time,
once: true,
afterResume: 'remove',
render: ({ resume }) => {
const handleDragChange = (val: DragFillResult[]) => {
// quiz-5:参考黄炎培 DragQuizPanel 的用法,不做“每次拖拽都提示”,仅在填满后校验并提示
if (q.id === 'quiz-5') {
const valid = val.filter((v) => q.blanks.some((b) => b.id === v.blankId))
if (valid.some((v) => v.optionValue === q.wrongValue)) {
setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: q.wrongHint }))
// 错误项拖拽后回到原来的选项框位(从结果中移除)
setDragQuizValues((prev) => ({
...prev,
[q.id]: valid.filter((v) => v.optionValue !== q.wrongValue),
}))
return
}
setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: '' }))
setDragQuizValues((prev) => ({ ...prev, [q.id]: valid }))
if (valid.length < q.blanks.length) return
const filledSet = new Set(valid.map((v) => v.optionValue))
const answerSet = new Set(q.correct)
const isCorrect =
filledSet.size >= answerSet.size && [...answerSet].every((a) => filledSet.has(a))
if (isCorrect) {
setActiveSuccess(q.id)
triggerPuzzleAnimation(16)
setResumeCallback(() => resume)
}
return
}
const prev = dragQuizValues[q.id] || []
const valid = val.filter((v) => q.blanks.some((b) => b.id === v.blankId))
const prevOptionSet = new Set(prev.map((v) => v.optionValue))
const newlyAdded = valid.find((v) => !prevOptionSet.has(v.optionValue))
const newlyAddedValue = newlyAdded?.optionValue
const isNewlyWrong = newlyAddedValue ? newlyAddedValue === q.wrongValue : false
const isNewlyCorrect = newlyAddedValue ? newlyAddedValue !== q.wrongValue : false
if (valid.some((v) => v.optionValue === q.wrongValue)) {
const wrongMsg = newlyAdded?.optionValue
? q.optionHints[newlyAdded.optionValue] || q.wrongHint
: q.wrongHint
setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: wrongMsg }))
setDragQuizFeedbackTypes((prevType) => ({ ...prevType, [q.id]: 'error' }))
setDragQuizValues((prev) => ({
...prev,
[q.id]: valid.filter((v) => v.optionValue !== q.wrongValue),
}))
return
}
const defaultTip = isNewlyCorrect ? '恭喜你,选择正确!' : ''
const tip = newlyAddedValue ? q.optionHints[newlyAddedValue] || defaultTip : ''
setDragQuizErrors((prevErr) => ({ ...prevErr, [q.id]: tip }))
if (newlyAddedValue) {
setDragQuizFeedbackTypes((prevType) => ({
...prevType,
[q.id]: isNewlyWrong ? 'error' : 'success',
}))
}
setDragQuizValues((prev) => ({ ...prev, [q.id]: valid }))
const filledSet = new Set(valid.map((v) => v.optionValue))
const answerSet = new Set(q.correct)
const isCorrect =
filledSet.size >= answerSet.size && [...answerSet].every((a) => filledSet.has(a))
if (isCorrect) {
setActiveSuccess(q.id)
if (q.id === 'quiz-2') triggerPuzzleAnimation(15)
setResumeCallback(() => resume)
}
}
return (
<>
{q.id === 'quiz-5' ? (
<DragQuizPanel
visible={activeSuccess !== q.id}
zIndex={10000}
title=""
questionPrefix={`【拖拽】${q.prompt}`}
questionSuffix="。"
options={q.options}
blanks={q.blanks}
blankSeparators={q.separators}
value={dragQuizValues[q.id] || []}
error={dragQuizErrors[q.id]}
onChange={handleDragChange}
/>
) : (
<BasketQuizPanel
visible={activeSuccess !== q.id}
zIndex={10000}
title={q.title}
questionPrefix={`【拖拽】${q.prompt}`}
options={q.options}
blanks={q.blanks}
blankSeparators={q.separators}
value={dragQuizValues[q.id] || []}
error={dragQuizErrors[q.id]}
feedbackType={dragQuizFeedbackTypes[q.id] || 'error'}
onChange={handleDragChange}
/>
)}
<SuccessAlert
visible={activeSuccess === q.id}
title="恭喜你,选择正确!"
content=""
onContinue={() => {
setActiveSuccess(null)
setDragQuizValues((prev) => {
const next = { ...prev }
delete next[q.id]
return next
})
setDragQuizErrors((prev) => {
const next = { ...prev }
delete next[q.id]
return next
})
setDragQuizFeedbackTypes((prev) => {
const next = { ...prev }
delete next[q.id]
return next
})
const cb = resumeCallback
setResumeCallback(null)
if (q.id === 'quiz-5') {
cb?.()
return
}
cb?.()
}}
/>
</>
)
},
}))
}, [
DRAG_QUIZZES,
activeSuccess,
dragQuizErrors,
dragQuizFeedbackTypes,
dragQuizValues,
resumeCallback,
triggerPuzzleAnimation,
])
if (!moduleName) return null
return (
<div
className="relative h-full w-full bg-black"
style={{ backgroundImage: getBackgroundImage('components/internalization/bg') }}
>
<Image
className="absolute right-8 top-8 z-10"
src={getStaticImage('components/离开')}
button
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
/>
{/* 过渡语 */}
<Narration
visible={showTransition}
onClose={() => {
setShowTransition(false)
setIntroDialogVisible(true)
}}
text="毛泽东思想是一个完整的科学思想体系,它的活的灵魂的三个基本方面——实事求是、群众路线、独立自主,是贯穿于毛泽东思想各个组成部分的立场、观点、方法,这三个方面在新时代被赋予了更加丰富的内涵和实践形式,成为习近平新时代中国特色社会主义思想的方法论基础和行动指南。"
animationSpeed={200}
/>
{/* 引入视频:进入页面自动播放(暂用其他视频代替) */}
{showIntroVideo && (
<Video
src={getPublicPath('video/maozedong/mzd_10.mp4')}
className="h-full w-full z-20"
playsInline
showSkipButton
autoPlay
onEnded={() => {
setShowIntroVideo(false)
setIntroQuizVisible(true)
}}
/>
)}
<SingleChoiceQuizPanel
visible={introQuizVisible}
zIndex={10001}
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
setIntroQuizVisible(false)
setIntroQuizSuccessVisible(true)
}}
/>
<SuccessAlert
visible={introQuizSuccessVisible}
title="恭喜你,选择正确!"
content="1945年,中共七大正式将毛泽东思想确立为党的指导思想并写入党章。"
onContinue={() => {
setIntroQuizSuccessVisible(false)
setShowTransition(true)
}}
/>
{/* 页面进入后直接播放引入视频,不再展示额外引导 */}
{/* 引入视频结束后的对话(图1) */}
<GuideSteps
visible={introDialogVisible}
onVisibleChange={(v) => {
setIntroDialogVisible(v)
if (!v) {
setShowMainVideo(true)
handleVideoCanPlay()
}
}}
showMask={false}
zIndex={60}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_24.mp3')}
>
关于“实事求是、群众路线、独立自主”这十二个字我还是有些不理解,总感觉这十二个字离我们很遥远。
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_25.mp3')}>
“实事求是、群众路线、独立自主”这十二个字,可以说是中国共产党人认识世界、改造世界的根本方法与价值遵循。今天我们有幸邀请到了华东师范大学马克思主义学院马立政副教授。关于你的问题,我想马教授会给你指点迷津!
</GuideStepsItem>
</GuideSteps>
{/* 主视频(视频11):对话结束自动播放,并插入题目锚点 */}
{showMainVideo && (
<Video
ref={videoRef}
src={getPublicPath('video/maozedong/mzd_neihua.mp4')}
className="h-full w-full"
anchors={anchors}
playsInline
controls
autoPlay
onCanPlay={handleVideoCanPlay}
onEnded={() => {
setShowMainVideo(false)
// 等视频组件卸载后再弹出信件
setTimeout(() => {
setShowLetter(true)
}, 300)
}}
/>
)}
{/* 写给毛主席的信:视频播完后出现 */}
<LetterToMaozedong
visible={showLetter}
onClose={() => {
setShowLetter(false)
}}
onAnimationEnd={() => {
setShowCompleted(true)
}}
showVideo={true}
/>
<EndingOverlay
visible={showCompleted}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_26.mp3')}
text="恭喜你圆满完成毛泽东旧居陈列馆的学习之旅,请带着研学感悟,前往孙中山故居纪念馆继续探访吧~"
animationSpeed={200}
onContinue={() => {
setShowCompleted(false)
navigate('/menu')
}}
/>
</div>
)
}
export default MaozedongInternalization
+316
View File
@@ -0,0 +1,316 @@
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
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
+320
View File
@@ -0,0 +1,320 @@
import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
Image,
Title,
MultipleChoiceQuizPanel,
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 { useMaozedongStore, saveMaozedongStore } from '@/store/maozedongStore'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
// starName 固定为 maozedong
const starName = 'maozedong'
// 展品 ID 与资源路径映射,ID 用来做唯一标识
const exhibitSrcMap: Record<1 | 2, string> = {
1: 'maozedong/place2/场景2展品1',
2: 'maozedong/place2/场景2展品2',
}
const MaozedongPlace2 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
// 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null)
// 持久化接入
const markVisited = useMaozedongStore((s) => s.markExhibitVisited)
const getVisited = useMaozedongStore((s) => s.getVisitedExhibits)
const setLastVisitedPlace = useMaozedongStore((s) => s.setLastVisitedPlace)
const persistedVisited = useMemo(() => new Set(getVisited('place2') as (1 | 2)[]), [getVisited])
const [visited, setVisited] = useState<Set<1 | 2>>(() => new Set(persistedVisited))
// 展品1的对话引导状态
const [guideVisible1, setGuideVisible1] = useState(false)
const [guideStep1, setGuideStep1] = useState(0)
// 展品2的对话引导状态
const [guideVisible2, setGuideVisible2] = useState(false)
const [guideStep2, setGuideStep2] = useState(0)
// 展品1对话结束后播放的视频状态(播完后再出题目)
const [showVideo, setShowVideo] = useState(false)
const videoRef = useRef<VideoRef>(null)
// 展品1对话结束后的多选题
const [quizVisible, setQuizVisible] = useState(false)
const [quizPassed, setQuizPassed] = useState(false)
const [successAlertVisible, setSuccessAlertVisible] = useState(false)
// Cursor 顺序控制:1 -> 2
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: {
2: [1],
},
visited,
})
const handleOpenExhibit = (id: 1 | 2) => {
setActiveExhibitId(id)
if (id === 1) {
setGuideVisible1(true)
setGuideStep1(0)
// 重新进入时重置题目完成状态
setQuizPassed(false)
} else if (id === 2) {
// 展品2:先看展品,关闭展品后再显示对话
setGuideVisible2(false)
setGuideStep2(0)
}
}
// 展品1对话步骤变化
const handleGuideStepChange1 = (step: number) => {
setGuideStep1(step)
}
// 展品1对话完成
const handleGuideVisibleChange1 = (visible: boolean) => {
setGuideVisible1(visible)
if (!visible && !quizPassed) {
setShowVideo(true)
}
}
// 展品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}/place2/场景2`),
}}
>
{/* 标题 */}
<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}/place1`)
}
}}
/>
{/* 光标1 */}
<Cursor
className="absolute left-217 bottom-145"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute right-92 bottom-105"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
{completed && (
<Image
className="absolute bottom-0 right-0"
src={getStaticImage('components/place/继续前进')}
button
onClick={() => {
setLastVisitedPlace('place3')
saveMaozedongStore()
navigate(`/${moduleName}/${starName}/place3`)
}}
/>
)}
{/* 展品遮罩层 */}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 1) {
triggerPuzzleAnimation(3)
}
if (activeExhibitId === 2) {
triggerPuzzleAnimation(4)
}
}}
onClose={() => {
const currentId = activeExhibitId
if (currentId !== null) {
setVisited((prev) => {
const next = new Set(prev)
next.add(currentId)
markVisited('place2', currentId)
saveMaozedongStore()
return next
})
}
setActiveExhibitId(null)
// 展品2:关闭后显示对话
if (currentId === 2) {
setGuideStep2(0)
setGuideVisible2(true)
}
}}
/>
{/* 展品1对话结束后播放的视频:播放完毕后出现题目 */}
{showVideo && (
<ScaledPortal zIndex={20000}>
<Video
ref={videoRef}
src={getPublicPath('video/maozedong/mzd_002.mp4')}
className="absolute inset-0 w-full h-full"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo(false)
setQuizVisible(true)
}}
/>
</ScaledPortal>
)}
{/* 展品1的对话引导(文字+语音播报占位) */}
<GuideSteps
visible={guideVisible1}
onVisibleChange={handleGuideVisibleChange1}
currentStep={guideStep1}
onStepChange={handleGuideStepChange1}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_05.mp3')}
>
中共一大定了“推翻资产阶级政权”的目标,但中共二大提
出了“反帝反封建的民主革命纲领”,为什么会有这样的转变呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_06.mp3')}>
中共一大时我们中国共产党刚成立,像刚学走路的孩子,
目标很宏大——“建立无产阶级专政”“消灭阶级”。但毛主席回到湖南后,
做了实际调查发现必须先解决“眼前的敌人”。
</GuideStepsItem>
<GuideStepsItem
speaker="student"
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_07.mp3')}
>
“眼前的敌人”?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_08.mp3')}>
对!想想看1922年长沙的码头工人罢工,安源的路矿工人罢工,他们的敌人是谁?是外国资本家,是军阀。中共二大时毛主席虽然没有出席,但完全赞同“最低纲领”——打倒军阀、推翻帝国主义压迫,实现民族独立和人民民主。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
这就像盖房子,得先把地基打好;等打倒了军阀和帝国主义,再谈社会主义也不迟。
</GuideStepsItem>
</GuideSteps>
{/* 展品2的对话引导 */}
<GuideSteps
visible={guideVisible2}
onVisibleChange={handleGuideVisibleChange2}
currentStep={guideStep2}
onStepChange={handleGuideStepChange2}
showMask={false}
highlightSelectors={[]}
zIndex={10001}
>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_09.mp3')}>
后来喊出的“打倒列强,除军阀”的口号,就是从这儿来的。
</GuideStepsItem>
</GuideSteps>
{/* 展品1对话结束后的多选题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quizVisible || successAlertVisible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quizVisible}
title="【多选】1922年7月16日至23日,中国共产党第二次全国代表大会在上海南成都路辅德里625号召开。大会制定了党的最低纲领和最高纲领,党的最低纲领,即党在民主革命阶段的纲领是:"
options={[
{ value: 'A', label: 'A.打倒军阀,建立国内和平' },
{
value: 'B',
label: 'B.推翻国际帝国主义的压迫,达到中华民族的完全独立',
},
{ value: 'C', label: 'C.统一中国为真正的民主共和国' },
]}
correctAnswer={['A', 'B', 'C']}
minSelections={1}
maxSelections={3}
errorMessage={(selectedValues) => {
if (selectedValues.length < 3) {
return '你还没有全面了解中共二大提出的党的最低纲领哦。请你再想想吧!'
}
return '还不太对,再仔细对照对话内容想一想吧~'
}}
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuizVisible(false)
setQuizPassed(true)
setSuccessAlertVisible(true)
}
}}
zIndex={10000}
/>
<SuccessAlert
visible={successAlertVisible}
onContinue={() => {
setSuccessAlertVisible(false)
}}
/>
</>
</ScaledPortal>
</div>
)
}
export default MaozedongPlace2
+310
View File
@@ -0,0 +1,310 @@
import {
ExhibitOverlay,
GuideSteps,
GuideStepsItem,
Image,
Title,
Video,
type VideoRef,
MultipleChoiceQuizPanel,
Narration,
} from '@/components'
import { useNavigate, useParams } from 'react-router-dom'
import { getBackgroundImage, getStaticImage, getPublicPath } from '@/lib/utils'
import { useEffect, 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'
// starName 固定为 maozedong
const starName = 'maozedong'
// 展品 ID 与资源路径映射,ID 用来做唯一标识
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
1: 'maozedong/place3/场景3展品1',
2: 'maozedong/place3/场景3展品2',
3: 'maozedong/place3/场景3展品3',
}
const MaozedongPlace3 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{
moduleName: string
}>()
// 获取碎片动画触发方法
const { triggerPuzzleAnimation } = usePuzzleAnimation()
// 当前激活的展品 ID
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(null)
// 持久化接入
const markVisited = useMaozedongStore((s) => s.markExhibitVisited)
const getVisited = useMaozedongStore((s) => s.getVisitedExhibits)
const setLastVisitedPlace = useMaozedongStore((s) => s.setLastVisitedPlace)
const persistedVisited = useMemo(
() => new Set(getVisited('place3') as (1 | 2 | 3)[]),
[getVisited]
)
const [visited, setVisited] = useState<Set<1 | 2 | 3>>(() => new Set(persistedVisited))
// 入场旁白
const [narrationVisible, setNarrationVisible] = useState(false)
// 光标3对应的对话状态
const [guide3Visible, setGuide3Visible] = useState(false)
const [guide3Step, setGuide3Step] = useState(0)
// 光标3的视频和题目状态
const [showVideo3, setShowVideo3] = useState(false)
const video3Ref = useRef<VideoRef>(null)
const [quiz3Visible, setQuiz3Visible] = useState(false)
const [successAlert3Visible, setSuccessAlert3Visible] = useState(false)
// Cursor
const { isEnabled } = useCursorController({
initialEnabled: [3],
dependencies: {
2: [3],
1: [3],
},
visited,
})
const handleOpenExhibit = (id: 1 | 2 | 3) => {
setActiveExhibitId(id)
setLastVisitedPlace('place3')
saveMaozedongStore()
if (id === 3) {
setGuide3Step(0)
setGuide3Visible(true)
}
}
const progressCount = visited.size
const total = 3
const completed = progressCount >= total
const subtitle = completed ? '已完成' : `${progressCount}/${total}`
useEffect(() => {
if (visited.has(1) && visited.has(2)) {
setNarrationVisible(true)
}
}, [visited])
if (!moduleName) return null
return (
<div
className="w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${starName}/place3/场景3`),
}}
>
<Narration
visible={narrationVisible}
onClose={() => setNarrationVisible(false)}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_12.mp3')}
text={`本党(国民党)为革命党,凡有利于革命的可采用,有害于革命的即应摒弃。——(毛泽东在国民党“一大”上的发言)`}
/>
{/* 标题 */}
<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-162 bottom-140"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute left-200 bottom-140"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
<Cursor
className="absolute right-155 bottom-90"
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')
saveMaozedongStore()
navigate(`/${moduleName}/${starName}/place4`)
}}
/>
)}
{/* 光标3的对话(点击展品3时出现) */}
<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_10.mp3')}
>
中共三大决定和国民党合作,但党内有争论。毛主席当时是什么态度呢?
</GuideStepsItem>
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/maozedong/mp3_mzd_11.mp3')}>
争论确实激烈!有人觉得和国民党合作是“降低身份”,但毛主席认为这是必要的。当时孙中山先生领导的国民党有革命传统,但缺真正的革命力量;我们有马克思主义,但缺群众基础。国共合作就好比“星火借烈焰,共燃燎原势”。
</GuideStepsItem>
</GuideSteps>
{/* 展品遮罩层 */}
<ExhibitOverlay
visible={activeExhibitId !== null}
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ''}
onOpen={() => {
if (activeExhibitId === 3) {
triggerPuzzleAnimation(5)
}
if (activeExhibitId === 1) {
triggerPuzzleAnimation(6)
}
if (activeExhibitId === 2) {
triggerPuzzleAnimation(7)
}
}}
onClose={() => {
const currentId = activeExhibitId
if (currentId === 3) {
// 关闭展品3窗口:结束对话,播放视频
setGuide3Step(0)
setGuide3Visible(false)
setActiveExhibitId(null)
setShowVideo3(true)
return
}
if (currentId !== null) {
setVisited((prev) => {
const next = new Set(prev)
next.add(currentId)
markVisited('place3', currentId)
saveMaozedongStore()
return next
})
}
setActiveExhibitId(null)
}}
/>
{/* 光标3的视频:播放结束后出现多选题 */}
{showVideo3 && (
<Video
ref={video3Ref}
src={getPublicPath('video/maozedong/mzd_01.mp4')}
className="fixed inset-0 z-20000"
autoPlay
playsInline
showSkipButton
onEnded={() => {
setShowVideo3(false)
setQuiz3Visible(true)
}}
/>
)}
{/* 光标3的视频后的多选题 + 成功提示(使用 ScaledPortal) */}
<ScaledPortal enabled={quiz3Visible || successAlert3Visible} zIndex={10050}>
<>
<MultipleChoiceQuizPanel
visible={quiz3Visible}
zIndex={10000}
title="【多选】1923年6月12日至20日,中国共产党第三次全国代表大会在广州召开。以下符合中共三大核心内容及历史意义的有:"
options={[
{
value: 'A',
label: 'A.大会中心议题是讨论全体共产党员以个人身份加入国民党,实现国共“党内合作”',
},
{
value: 'B',
label: 'B.大会强调共产党党员加入国民党后,需保持政治、思想和组织上的独立性',
},
{
value: 'C',
label: 'C.大会标志着中国共产党以国共合作为基础的统一战线政策正式形成',
},
{
value: 'D',
label: 'D.大会提出了“打倒军阀、推翻帝国主义”的革命目标',
},
]}
correctAnswer={['A', 'B', 'C']}
minSelections={1}
maxSelections={4}
errorMessage={(selectedValues) => {
if (selectedValues.includes('D')) {
return '“打倒军阀、推翻帝国主义”的革命目标是中共二大提出的,中共三大是肯定该目标的正确性,并明确通过国共合作推进这一目标的实现。请你再想想吧!'
}
if (selectedValues.length < 3) {
return '恭喜你,答案已经十分接近!请再结合上下文仔细完善吧。'
}
return '请再仔细分析一下题目,做出更全面的选择吧!'
}}
onSubmit={(_values, isCorrect) => {
if (isCorrect) {
setQuiz3Visible(false)
setSuccessAlert3Visible(true)
// 答题成功后标记展品3已访问
setVisited((prev) => {
const next = new Set(prev)
next.add(3)
return next
})
markVisited('place3', 3)
}
}}
/>
<SuccessAlert
content="你理解了中共三大和国民党合作的核心内容及历史意义"
visible={successAlert3Visible}
onContinue={() => {
setSuccessAlert3Visible(false)
}}
/>
</>
</ScaledPortal>
</div>
)
}
export default MaozedongPlace3
+610
View File
@@ -0,0 +1,610 @@
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'
// 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) {
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.mp3')}>
太有关系了!中共三大后毛主席到上海工作了一段时间,看到工人运动轰轰烈烈,
但农民还是“沉默的大多数”。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
后来毛主席回湖南,专门跑了湘潭、湘乡、衡山、
醴陵、长沙五县,发现农民不仅能革命,而且革命性极强——他们被剥削得最狠, 反抗起来也最坚决!
</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.mp3')}>
对!无产阶级要领导革命,必须联合农民——他们是工人阶级的“天然盟友”。
四大虽然还没有明确提出“农村包围城市”,但已经意识到农民问题的重要性。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
后来毛主席常说:“国民革命的中心问题,就是农民问题,一切都要靠农民问题的解决。”
这句话的种子,就是在四大前后埋下的。
</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.mp3')}>
同学问到了根子上!革命就像爬山,一开始可能看不清山顶的样子,
但只要沿着正确的方向,一步一步扎扎实实地走,总会离目标越来越近。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
你们现在学历史,不能只记年代和事件,更要理解“为什么”——
为什么马克思主义要和中国实际相结合?为什么农民是革命的重要力量?
把这些道理想通了,将来不管做什么事,都能脚踏实地。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
“实事求是”这四个字,是我们党一路走来的法宝,希望你也能记住。
</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
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
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
+512
View File
@@ -0,0 +1,512 @@
import {
SingleChoiceQuizPanel,
Video,
type VideoAnchor,
type VideoRef,
EndingOverlay,
StudyGuideModal,
} from '@/components'
import { SuccessAlert } from '@/components/layout'
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
import { cn, getStaticImage } from '@/lib/utils'
import { getPublicPath } from '@/lib/utils'
import { useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
// 光标组件 - 带旋转动画
// Safari 兼容:使用 onTouchEnd 和 -webkit-tap-highlight-color
const Cursor = ({ className, onClick }: { className?: string; onClick: () => void }) => (
<img
className={cn('w-35 cursor-pointer animate-[spin_2s_linear_infinite] select-none', className)}
onClick={onClick}
onTouchEnd={(e) => {
e.preventDefault() // 防止 Safari 触发两次
onClick()
}}
style={{
WebkitTapHighlightColor: 'transparent',
WebkitTouchCallout: 'none',
WebkitUserSelect: 'none',
touchAction: 'manipulation',
}}
src={getStaticImage('components/光标')}
alt=""
draggable={false}
/>
)
const firstQuizOptions = [
{
value: 'A',
label: 'A. 基层情况对决策至关重要,关乎无产阶级领导权、工农联盟议题能否贴合实际',
},
{
value: 'B',
label: 'B. 深入基层,收集各地党组织、工农运动实情,再推进筹备等备',
},
]
const secondQuizOptions = [
{
value: 'A',
label: 'A. 以强硬指令,要求各地限时上报意见',
},
{
value: 'B',
label: 'B. 讲清意见对四大讨论无产阶级领导权、工农联盟的重要性,以革命大义动员党员',
},
]
const endingQuizOptions = [
{
value: 'A',
label: 'A. 仅安心调养',
},
{
value: 'B',
label: 'B. 在调养修整后调研农民运动',
},
]
type QuizCursorProps = {
resume: () => void
unmute: () => void
onCorrect: () => void
}
const FirstQuizCursor = ({ resume, unmute, onCorrect }: QuizCursorProps) => {
const [visible, setVisible] = useState(false)
const [showSuccessAlert, setShowSuccessAlert] = useState(false)
const hasResumedRef = useRef(false)
return (
<>
<Cursor
className="absolute left-320 bottom-70"
onClick={() => {
unmute()
setVisible(true)
}}
/>
<SingleChoiceQuizPanel
visible={visible}
title="【单选】基于对史实的了解,你觉得毛主席会做出何种选择?"
options={firstQuizOptions}
correctAnswer="B"
errorMessage="很抱歉,你选错了!高层快速定框架,筹备脱离基层实际,因缺乏实战支撑,在推动工农运动、明确领导权时,难以契合各地实情,革命力量动员只会受阻。请你重新做出选择吧!"
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setVisible(false)
setShowSuccessAlert(true)
onCorrect()
}}
/>
<SuccessAlert
visible={showSuccessAlert}
content="基于对史实的理解,毛主席的选择应当是深入基层、收集真实情况,再推进各项筹备与行动。"
onContinue={() => {
if (hasResumedRef.current) return
hasResumedRef.current = true
setShowSuccessAlert(false)
resume()
}}
onClose={() => {
if (hasResumedRef.current) return
hasResumedRef.current = true
setShowSuccessAlert(false)
resume()
}}
/>
</>
)
}
const SecondQuizCursor = ({ resume, unmute, onCorrect }: QuizCursorProps) => {
const [visible, setVisible] = useState(false)
const [showSuccessAlert, setShowSuccessAlert] = useState(false)
const hasResumedRef = useRef(false)
return (
<>
<Cursor
className="absolute left-110 bottom-65"
onClick={() => {
unmute()
setVisible(true)
}}
/>
<SingleChoiceQuizPanel
visible={visible}
title="【单选】基于对史实的了解,你觉得毛主席会做出何种选择?"
options={secondQuizOptions}
correctAnswer="B"
errorMessage="很抱歉,你选错了!下达强硬指令,基层反而误陷内容空洞、应付,四大筹备缺乏真实基层素材,决策难以解决实际问题,工农联盟等议题讨论会浮于表面。请你重新做出选择吧!"
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setVisible(false)
setShowSuccessAlert(true)
onCorrect()
}}
/>
<SuccessAlert
visible={showSuccessAlert}
title="恭喜你,选择正确!"
content="当时毛泽东采用动员方式,以“钟英”为代号发出《关于召开四大致各地党组织的信》和《关于召开四大的通知》,在通知中阐明意义,各地党组织积极响应,为四大议题积累丰富实践依据,推动筹备深入。"
onContinue={() => {
if (hasResumedRef.current) return
hasResumedRef.current = true
setShowSuccessAlert(false)
resume()
}}
onClose={() => {
if (hasResumedRef.current) return
hasResumedRef.current = true
setShowSuccessAlert(false)
resume()
}}
/>
</>
)
}
const EndingQuizCursor = ({ resume, unmute }: Omit<QuizCursorProps, 'onCorrect'>) => {
const [questionVisible, setQuestionVisible] = useState(false)
const [showSuccessAlert, setShowSuccessAlert] = useState(false)
const hasResumedRef = useRef(false)
return (
<>
<Cursor
className="absolute left-110 bottom-140"
onClick={() => {
unmute()
setQuestionVisible(true)
}}
/>
<SingleChoiceQuizPanel
visible={questionVisible}
title="【单选】基于对史实的了解,你觉得毛主席会做出何种选择?"
options={endingQuizOptions}
correctAnswer="B"
errorMessage="很抱歉,你选错了!当时如果错过调研契机,农民运动在四大后推进缺乏针对性实践参考,工农联盟思想深化缓慢,革命力量整合会受到影响。请你重新做出选择吧!"
onSubmit={(_value, isCorrect) => {
if (!isCorrect) return
setQuestionVisible(false)
setShowSuccessAlert(true)
}}
/>
<SuccessAlert
visible={showSuccessAlert}
title="恭喜你,选择正确!"
content="毛泽东回湘后,深入农村,了解农民疾苦与诉求,为后续《湖南农民运动考察报告》奠基,让四大工农联盟思想在实践中持续深化,开始了领导工农运动的新征程。"
onContinue={() => {
if (hasResumedRef.current) return
hasResumedRef.current = true
setShowSuccessAlert(false)
resume()
}}
onClose={() => {
if (hasResumedRef.current) return
hasResumedRef.current = true
setShowSuccessAlert(false)
resume()
}}
/>
</>
)
}
const Home = () => {
const { triggerPuzzleAnimation } = usePuzzleAnimation()
const bgmRef = useRef<HTMLAudioElement>(null)
const videoRef = useRef<VideoRef>(null)
const navigate = useNavigate()
const [showGuideModal, setShowGuideModal] = useState(true)
const [endingVisible, setEndingVisible] = useState(false)
const moduleName = 'maozedong'
const playBGM = () => {
bgmRef.current?.play().catch(() => {})
}
const anchors: VideoAnchor[] = useMemo(
() => [
{
id: 'anchor-0',
time: 3,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-223 top-108"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-1',
time: 8,
afterResume: 'hide',
onTrigger: () => console.log('埋点:锚点 1 触发'),
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-150 top-80"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-2',
time: 22,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute right-42 bottom-95"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-3',
time: 32,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-50 bottom-100"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-4',
time: 40,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute right-158 bottom-105"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-4-1',
time: 52,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-223 top-135"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-5',
time: 60 + 8,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute right-58 bottom-115"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-6',
time: 60 + 25,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-80 bottom-110"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-6-1',
time: 60 + 34,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<FirstQuizCursor
resume={resume}
unmute={unmute}
onCorrect={() => triggerPuzzleAnimation(13)}
/>
),
},
{
id: 'anchor-7',
time: 60 + 38,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-220 bottom-100"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-8',
time: 60 * 2,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-243 bottom-100"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-9',
time: 60 * 2 + 11.5,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-375 bottom-100"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-10',
time: 60 * 2 + 21,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<SecondQuizCursor
resume={resume}
unmute={unmute}
onCorrect={() => triggerPuzzleAnimation(14)}
/>
),
},
{
id: 'anchor-11',
time: 60 * 2 + 24,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-220 bottom-100"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-12',
time: 60 * 2 + 43,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-320 bottom-120"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-13',
time: 60 * 2 + 57,
afterResume: 'hide',
render: ({ resume, unmute }) => (
<Cursor
className="absolute left-248 bottom-130"
onClick={() => {
unmute()
resume()
}}
/>
),
},
{
id: 'anchor-14',
time: 60 * 3 + 9,
afterResume: 'hide',
render: ({ resume, unmute }) => <EndingQuizCursor resume={resume} unmute={unmute} />,
},
],
[triggerPuzzleAnimation]
)
return (
<div>
<audio ref={bgmRef} src={getPublicPath('audio/BGM.mp3')} loop autoPlay />
<Video
ref={videoRef}
src={getPublicPath('video/maozedong/mzd_7_8_9_tiwu.mp4')}
className="w-full h-full aspect-video"
anchors={anchors}
autoPlay={false}
muted
playsInline
onEnded={() => {
setEndingVisible(true)
}}
/>
<StudyGuideModal
visible={showGuideModal}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_22.mp3')}
onContinue={() => {
setShowGuideModal(false)
playBGM()
videoRef.current?.play()
}}
firstLineIndent="2em"
>
{`同学你好,欢迎来到“跨越时空·体悟”学习模块!
请登陆“时空回溯・亲历时代”体验仓,下一站:1924年,上海。
1924年的上海,革命浪潮暗涌,中共四大筹备工作关乎革命前途。无产阶级领导权、工农联盟等核心议题亟待明确,毛泽东同志在筹备关键节点面临诸多重要抉择。请你化身历史见证者,站在毛主席的视角做出选择,解锁四大筹备的关键密码,感受革命先辈的坚定与智慧!完成三次选择后,可进入下一阶段的学习。时空门已开,请做好准备迎接考验吧!`}
</StudyGuideModal>
<EndingOverlay
visible={endingVisible}
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_23.mp3')}
animationSpeed={200}
text="毛泽东在中共四大筹备期间的三次重要抉择,均锚定真实实践、扎根基层,为四大首次明确无产阶级领导权、工农联盟筑牢基础。虽因健康未参会,但其筹备工作与后续调研,如纽带般连接理论与实践,让党的革命理念在历史浪潮中,朝着正确方向坚定前行,成为早期共产党人探索革命道路的生动注脚,影响深远,照耀后世奋进征程。"
onContinue={() => {
setEndingVisible(false)
navigate(`/${moduleName}/maozedong/home`)
}}
/>
</div>
)
}
export default Home