chore: 更新素材和组件优化
- 更新展品图片(鲁迅/毛泽东/孙中山/黄炎培/聂耳/张乐平故居) - 新增点击查看图标、报纸素材 - 优化视频组件、拼图组件、展品组件 - 更新陈云音频资源 - 新增输入验证文档和工具函数 - 组件布局和动画优化 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
73d02eb62d
commit
73f6df92ce
130 files changed
+2345
-759
No files matched your search
+22
-5
@@ -4,9 +4,22 @@ import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { Image, Video } from '@/components'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const INTRO_VIEWED_KEY = 'home-intro-viewed'
|
||||
|
||||
const Home = () => {
|
||||
const navigate = useNavigate()
|
||||
const [showIntroVideo, setShowIntroVideo] = useState(false)
|
||||
const [introViewed, setIntroViewed] = useState(
|
||||
() => localStorage.getItem(INTRO_VIEWED_KEY) === 'true'
|
||||
)
|
||||
|
||||
const handleIntroClick = () => {
|
||||
setShowIntroVideo(true)
|
||||
if (!introViewed) {
|
||||
setIntroViewed(true)
|
||||
localStorage.setItem(INTRO_VIEWED_KEY, 'true')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -23,11 +36,15 @@ const Home = () => {
|
||||
exit={{ opacity: 0, scale: 0.92, x: 24 }}
|
||||
transition={{ duration: 0.22, ease: 'easeOut' }}
|
||||
>
|
||||
<Image
|
||||
src={getStaticImage('home/课程简介')}
|
||||
button
|
||||
onClick={() => setShowIntroVideo(true)}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Image src={getStaticImage('home/课程简介')} button onClick={handleIntroClick} />
|
||||
{!introViewed && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
+18
-2
@@ -1,8 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||||
import { Image, PuzzleSystem } from '@/components'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { DEFAULT_PUZZLE_RESIDENCE } from '@/config/puzzleResidences'
|
||||
import { DEFAULT_PUZZLE_RESIDENCE, PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||||
import { puzzleDataConfig } from '@/config/puzzleData'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
import { isMenuModuleComplete } from '@/config/progression'
|
||||
|
||||
@@ -12,6 +13,18 @@ const Menu = () => {
|
||||
const puzzles = usePuzzleStore((state) => state.puzzles)
|
||||
const isLoaded = usePuzzleStore((state) => state.isLoaded)
|
||||
|
||||
const courseProgress = useMemo(() => {
|
||||
const totalPieces = PUZZLE_RESIDENCES.reduce((total, item) => {
|
||||
return total + Object.keys(puzzleDataConfig[item.residence] ?? {}).length
|
||||
}, 0)
|
||||
const unlockedPieces = PUZZLE_RESIDENCES.reduce((total, item) => {
|
||||
return total + (puzzles[item.residence]?.size ?? 0)
|
||||
}, 0)
|
||||
|
||||
if (!isLoaded || totalPieces === 0) return 0
|
||||
return Math.round((unlockedPieces / totalPieces) * 100)
|
||||
}, [isLoaded, puzzles])
|
||||
|
||||
/**
|
||||
* 类型:1:革命先驱、文化民人、艺术名家、经济巨擘
|
||||
*/
|
||||
@@ -61,6 +74,9 @@ const Menu = () => {
|
||||
button
|
||||
onClick={() => setActiveResidence(DEFAULT_PUZZLE_RESIDENCE)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center pb-8 pointer-events-none text-white text-[36px] font-bold leading-none drop-shadow-[0_2px_2px_rgba(0,0,0,0.8)]">
|
||||
{courseProgress}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进入具体故居的拼图系统(与故居页面一致的 PuzzleSystem) */}
|
||||
|
||||
@@ -219,7 +219,6 @@ const ChenyunInternalization = () => {
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content=""
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
@@ -334,7 +333,6 @@ const ChenyunInternalization = () => {
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你,选择正确!"
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
const cb = resumeCallback
|
||||
@@ -395,6 +393,9 @@ const ChenyunInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="chenyun_internalization_video"
|
||||
showReadonlyProgress
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -144,6 +144,8 @@ const ChenyunPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="chenyun_place3_video"
|
||||
showReadonlyProgress
|
||||
controls={false}
|
||||
onEnded={() => {
|
||||
setVideoVisible(false)
|
||||
|
||||
@@ -185,6 +185,8 @@ const ChenyunPlace5 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="chenyun_place5_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setVideoVisible3(false)
|
||||
setQuiz3Visible(true)
|
||||
|
||||
@@ -286,6 +286,8 @@ const ChenyunPlace7 = () => {
|
||||
preload="auto"
|
||||
muted={!video1Playing}
|
||||
showSkipButton
|
||||
skipKey="chenyun_place7_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideo1Ended}
|
||||
/>
|
||||
</div>
|
||||
@@ -350,6 +352,8 @@ const ChenyunPlace7 = () => {
|
||||
preload="auto"
|
||||
muted={!video2Playing}
|
||||
showSkipButton
|
||||
skipKey="chenyun_place7_video2"
|
||||
showReadonlyProgress
|
||||
skipText="跳过"
|
||||
onEnded={handleVideo2Ended}
|
||||
/>
|
||||
|
||||
@@ -122,6 +122,8 @@ const ChenyunPlace9 = () => {
|
||||
preload="auto"
|
||||
muted={!videoPlaying}
|
||||
showSkipButton
|
||||
skipKey="chenyun_place9_video"
|
||||
showReadonlyProgress
|
||||
skipText="跳过"
|
||||
onEnded={() => {
|
||||
setVideoPlaying(false)
|
||||
|
||||
@@ -60,6 +60,8 @@ export const VideoHandler = ({
|
||||
src={videoConfig.src}
|
||||
autoPlay
|
||||
showSkipButton
|
||||
skipKey={`chenyun_reflection_${playingVideo}`}
|
||||
showReadonlyProgress
|
||||
onEnded={onVideoEnd}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
|
||||
@@ -152,7 +152,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
time: 369, // 6:09
|
||||
title: '',
|
||||
prompt:
|
||||
'【拖拽】当年黄炎培先生有推广国货的爱国情怀,如今我们的“国货”已经自强自立,请将其中的国产企业/品牌放置到空格处',
|
||||
'【拖拽】当年黄炎培先生有推广国货的爱国情怀,如今我们的“国货”已经自强自立,请将以下属于中国的企业、品牌、装备放置到空格处:',
|
||||
type: 'drag',
|
||||
options: [
|
||||
{ value: '特斯拉', label: '特斯拉' },
|
||||
@@ -395,7 +395,6 @@ const HuangyanpeiInternalization = () => {
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
@@ -465,6 +464,9 @@ const HuangyanpeiInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="huangyanpei_internalization_video"
|
||||
showReadonlyProgress
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -332,6 +332,8 @@ const HuangyanpeiPlace4 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="huangyanpei_place4_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
setShowQuiz(true)
|
||||
|
||||
@@ -152,12 +152,13 @@ const HuangyanpeiPlace5 = () => {
|
||||
ref={videoRef}
|
||||
src={getPublicPath('video/huangyanpei/huangyanpei_07.mp4')}
|
||||
className="w-full h-full"
|
||||
controls
|
||||
autoPlay
|
||||
playsInline
|
||||
preload="auto"
|
||||
showSkipButton
|
||||
skipText="跳过"
|
||||
skipKey="huangyanpei_place5_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
// 对话结束后显示题目
|
||||
setShowQuiz(true)
|
||||
|
||||
@@ -262,7 +262,9 @@ const BambooPuzzleGame = ({ visible = true, onClose, onComplete }: BambooPuzzleG
|
||||
>
|
||||
毛竹拼单杠
|
||||
</h1>
|
||||
<p className="text-4xl text-white mt-10">请将下方的竹子拖到正确位置,拼出单杠!</p>
|
||||
<p className="text-4xl text-white mt-10">
|
||||
请将下方的竹子拖到正确位置,拼出单杠!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 拼图区域 - 空白容器 */}
|
||||
|
||||
@@ -135,7 +135,7 @@ const HuangyanpeiReflectionScene2 = () => {
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="川沙小学堂"
|
||||
title="浦东中学"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
|
||||
@@ -70,7 +70,7 @@ const HuangyanpeiReflectionScene3 = () => {
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="川沙小学堂"
|
||||
title="中华职业学校"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
@@ -240,6 +240,8 @@ const HuangyanpeiReflectionScene3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="huangyanpei_reflection_scene3_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
saveHuangyanpeiStore()
|
||||
|
||||
@@ -210,6 +210,8 @@ const ViseAssemblyDialog = ({ visible = true, onClose, onComplete }: ViseAssembl
|
||||
playsInline
|
||||
preload="auto"
|
||||
showSkipButton
|
||||
skipKey="huangyanpei_reflection_vise_video"
|
||||
showReadonlyProgress
|
||||
skipText="跳过"
|
||||
onEnded={handleVideoEnd}
|
||||
/>
|
||||
|
||||
@@ -73,7 +73,7 @@ const HuangyanpeiReflectionScene4 = () => {
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="川沙小学堂"
|
||||
title="中华职业学校"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
@@ -220,6 +220,8 @@ const HuangyanpeiReflectionScene4 = () => {
|
||||
className="h-full w-full"
|
||||
autoPlay
|
||||
showSkipButton
|
||||
skipKey="huangyanpei_reflection_scene4_video"
|
||||
showReadonlyProgress
|
||||
playsInline
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -331,7 +331,7 @@ const TowelSalesDialog = ({ visible = true, onClose, onComplete }: TowelSalesDia
|
||||
|
||||
{/* 成功提示 */}
|
||||
<SuccessAlert
|
||||
title="恭喜你, 推销语设计成功!"
|
||||
title="恭喜你,推销语设计成功!"
|
||||
content="既突出了国货优势,又传递了爱国情怀,当年的商科学生也是这么做的。"
|
||||
visible={successVisible}
|
||||
onContinue={handleSuccessContinue}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
|
||||
const SPOTLIGHT_PROMPT_STORAGE_PREFIX = 'learning-map:spotlight-clicked:'
|
||||
|
||||
interface HexagonProgressBarProps {
|
||||
progress: number
|
||||
}
|
||||
@@ -96,21 +95,32 @@ const LearningMap = () => {
|
||||
}, [completedResidence, unlockedMap])
|
||||
|
||||
useEffect(() => {
|
||||
if (!completedResidence || !isLoaded || !completeMap[completedResidence]) {
|
||||
if (!isLoaded) {
|
||||
setSpotlightPromptResidence(null)
|
||||
return
|
||||
}
|
||||
|
||||
let latestCompleted: string | null = null
|
||||
for (const { residence } of PUZZLE_RESIDENCES) {
|
||||
if (isResidenceComplete(residence, puzzles)) {
|
||||
latestCompleted = residence
|
||||
}
|
||||
}
|
||||
|
||||
if (!latestCompleted) {
|
||||
setSpotlightPromptResidence(null)
|
||||
return
|
||||
}
|
||||
|
||||
const isKnownResidence = PUZZLE_RESIDENCES.some((item) => item.residence === completedResidence)
|
||||
const isPromptClicked =
|
||||
window.sessionStorage.getItem(`${SPOTLIGHT_PROMPT_STORAGE_PREFIX}${completedResidence}`) ===
|
||||
window.sessionStorage.getItem(`${SPOTLIGHT_PROMPT_STORAGE_PREFIX}${latestCompleted}`) ===
|
||||
'true'
|
||||
|
||||
setSpotlightPromptResidence(isKnownResidence && !isPromptClicked ? completedResidence : null)
|
||||
}, [completeMap, completedResidence, isLoaded])
|
||||
setSpotlightPromptResidence(isPromptClicked ? null : latestCompleted)
|
||||
}, [isLoaded, puzzles])
|
||||
|
||||
const handleSpotlightClick = (residence: string, isUnlocked: boolean) => {
|
||||
if (!isUnlocked) return
|
||||
const handleSpotlightClick = (residence: string, isComplete: boolean) => {
|
||||
if (!isComplete) return
|
||||
|
||||
if (residence === spotlightPromptResidence) {
|
||||
window.sessionStorage.setItem(`${SPOTLIGHT_PROMPT_STORAGE_PREFIX}${residence}`, 'true')
|
||||
@@ -147,6 +157,7 @@ const LearningMap = () => {
|
||||
<div className="absolute inset-0">
|
||||
{PUZZLE_RESIDENCES.map(({ residence, image, moduleName, left, top }) => {
|
||||
const isUnlocked = unlockedMap[residence]
|
||||
const isComplete = completeMap[residence]
|
||||
const isNewlyUnlocked = residence === newlyUnlockedResidence
|
||||
const showSpotlightPrompt = residence === spotlightPromptResidence
|
||||
|
||||
@@ -194,20 +205,60 @@ const LearningMap = () => {
|
||||
|
||||
<div className="relative flex justify-center gap-6">
|
||||
{showSpotlightPrompt && (
|
||||
<motion.img
|
||||
className="absolute -right-10 -top-14 z-20 w-20 pointer-events-none"
|
||||
src={getStaticImage('learning-map/箭头指引')}
|
||||
alt="点击追光"
|
||||
animate={{ y: [0, -8, 0], scale: [1, 1.08, 1] }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'easeInOut' }}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-1/2 top-0 z-30 pointer-events-none"
|
||||
style={{
|
||||
transform: 'translate(-40%, 30%)',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
>
|
||||
<div className="relative flex items-center justify-center">
|
||||
<motion.span
|
||||
className="absolute w-14 h-14 rounded-full bg-amber-400"
|
||||
style={{ willChange: 'transform, opacity' }}
|
||||
animate={{ scale: [0.55, 1.6], opacity: [0.5, 0] }}
|
||||
transition={{
|
||||
duration: 1.4,
|
||||
repeat: Infinity,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
/>
|
||||
<motion.svg
|
||||
viewBox="0 0 32 32"
|
||||
className="relative w-10 h-10"
|
||||
style={{
|
||||
filter: 'drop-shadow(0 2px 3px rgba(0,0,0,0.45))',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
animate={{ scale: [1, 0.88, 1] }}
|
||||
transition={{
|
||||
duration: 1.2,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M9 5 L9 23 L14 19 L17 28 L20 27 L17 18 L24 18 Z"
|
||||
fill="white"
|
||||
stroke="#0f172a"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</motion.svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Image
|
||||
className={isUnlocked ? undefined : 'cursor-not-allowed'}
|
||||
className={isComplete ? undefined : 'cursor-not-allowed'}
|
||||
style={
|
||||
isComplete
|
||||
? undefined
|
||||
: { opacity: 0.4, filter: 'grayscale(1) brightness(0.8)' }
|
||||
}
|
||||
src={getStaticImage('learning-map/追光入口')}
|
||||
alt="追光"
|
||||
button={isUnlocked}
|
||||
onClick={() => handleSpotlightClick(residence, !!isUnlocked)}
|
||||
button={isComplete}
|
||||
onClick={() => handleSpotlightClick(residence, !!isComplete)}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Navigate, useNavigate, useParams } from 'react-router-dom'
|
||||
import { Image } from '@/components'
|
||||
import { isResidenceUnlocked } from '@/config/progression'
|
||||
import { isResidenceComplete } from '@/config/progression'
|
||||
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||||
import { getStaticImage } from '@/lib/utils'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
@@ -23,7 +23,7 @@ const SpotlightMap = () => {
|
||||
return <div className="relative w-full h-full bg-black" />
|
||||
}
|
||||
|
||||
if (!isResidenceUnlocked(residenceConfig.residence, puzzles)) {
|
||||
if (!isResidenceComplete(residenceConfig.residence, puzzles)) {
|
||||
return <Navigate to="/learning-map" replace />
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
id: 'quiz-3',
|
||||
time: 60 * 6 + 39, // 6:39
|
||||
title: '',
|
||||
prompt: '【单选】以下那句诗出自鲁迅的作品:',
|
||||
prompt: '【单选】以下哪句诗出自鲁迅的作品:',
|
||||
type: 'single',
|
||||
options: [
|
||||
{
|
||||
@@ -460,6 +460,9 @@ const LuxunInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="luxun_internalization_video"
|
||||
showReadonlyProgress
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -146,7 +146,7 @@ const LuxunPlace3 = () => {
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title={`历史新编中的现实呐喊\n为亡友编纂的精神丰碑`}
|
||||
title={`历史新编中的现实呐喊 为亡友编纂的精神丰碑`}
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
|
||||
@@ -363,6 +363,8 @@ const LuxunPlace5 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="luxun_place4_video"
|
||||
showReadonlyProgress
|
||||
anchors={videoAnchors}
|
||||
onEnded={() => {
|
||||
setEndingGuideVisible(true)
|
||||
|
||||
@@ -16,64 +16,64 @@ const dragItems = [
|
||||
name: '闰土',
|
||||
imagePath: 'luxun/reflection/scene2/拖拽-润土',
|
||||
targetBlankId: 'city-1-label-0', // 绍兴-人物
|
||||
errorHint: '❌闰土的故事和鲁迅的少年绍兴时光有关哦!',
|
||||
successMessage: '✅答对啦! 闰土是鲁迅绍兴时期笔下的经典人物。',
|
||||
errorHint: '❌闰土的故事和鲁迅的少年绍兴时光有关哦!',
|
||||
successMessage: '✅答对啦!闰土是鲁迅绍兴时期笔下的经典人物。',
|
||||
},
|
||||
{
|
||||
id: 'baicaoyuan',
|
||||
name: '《从百草园到三味书屋》',
|
||||
imagePath: 'luxun/reflection/scene2/拖拽-百草园到三味书屋',
|
||||
targetBlankId: 'city-1-label-1', // 绍兴-作品
|
||||
errorHint: '❌这个作品写的是鲁迅在绍兴的故事哦!',
|
||||
successMessage: '✅真棒! 这篇散文记录了鲁迅在绍兴的求学时光。',
|
||||
errorHint: '❌这个作品写的是鲁迅在绍兴的故事哦!',
|
||||
successMessage: '✅真棒!这篇散文记录了鲁迅在绍兴的求学时光。',
|
||||
},
|
||||
{
|
||||
id: 'aqzhengzhuan',
|
||||
name: '《阿Q正传》',
|
||||
imagePath: 'luxun/reflection/scene2/拖拽-阿Q正传',
|
||||
targetBlankId: 'city-2-label-0', // 北京-作品
|
||||
errorHint: '❌这部经典小说和鲁迅在北京的创作有关哦!',
|
||||
successMessage: '✅答对啦! 《阿Q正传》是鲁迅小说的代表作,诞生于北京。',
|
||||
errorHint: '❌这部经典小说和鲁迅在北京的创作有关哦!',
|
||||
successMessage: '✅答对啦!《阿Q正传》是鲁迅小说的代表作,诞生于北京。',
|
||||
},
|
||||
{
|
||||
id: 'jingshenshengli',
|
||||
name: '“精神胜利法”',
|
||||
imagePath: 'luxun/reflection/scene2/拖拽-精神胜利法',
|
||||
targetBlankId: 'city-2-label-1', // 北京-核心主题
|
||||
errorHint: '❌这个主题是《阿Q正传》的核心,和北京创作背景相关哦!',
|
||||
successMessage: '✅没错! 《阿Q正传》画出了国人的灵魂,以便疗救。',
|
||||
errorHint: '❌这个主题是《阿Q正传》的核心,和北京创作背景相关哦!',
|
||||
successMessage: '✅没错!《阿Q正传》画出了国人的灵魂,以便疗救。',
|
||||
},
|
||||
{
|
||||
id: 'guanzhudiceng',
|
||||
name: '“关注底层”',
|
||||
imagePath: 'luxun/reflection/scene2/关注底层',
|
||||
targetBlankId: 'city-3-label-0', // 厦门-创作方向
|
||||
errorHint: '❌这个创作方向是鲁迅在厦门的核心追求哦!',
|
||||
successMessage: '✅答对啦!鲁迅在厦门任教时,依旧关注底层民众的命运。',
|
||||
errorHint: '❌这个创作方向是鲁迅在厦门的核心追求哦!',
|
||||
successMessage: '✅答对啦!鲁迅在厦门任教时,依旧关注底层民众的命运。',
|
||||
},
|
||||
{
|
||||
id: 'zhaohuaxishi',
|
||||
name: '《朝花夕拾》',
|
||||
imagePath: 'luxun/reflection/scene2/拖拽-朝花夕拾',
|
||||
targetBlankId: 'city-4-label-0', // 广州-作品集
|
||||
errorHint: '❌这个作品集和鲁迅在广州的旅居时光有关哦!',
|
||||
successMessage: '✅真棒!《朝花夕拾》部分篇章在广州白云楼定稿。',
|
||||
errorHint: '❌这个作品集和鲁迅在广州的旅居时光有关哦!',
|
||||
successMessage: '✅真棒!《朝花夕拾》部分篇章在广州白云楼定稿。',
|
||||
},
|
||||
{
|
||||
id: 'gushixinbian',
|
||||
name: '《故事新编》',
|
||||
imagePath: 'luxun/reflection/scene2/拖拽-故事新编',
|
||||
targetBlankId: 'city-5-label-0', // 上海-作品
|
||||
errorHint: '❌再试试~这个作品和上海的创作故事更相关哦!',
|
||||
successMessage: '✅答对啦!《故事新编》是鲁迅在上海故居完成的经典作品集。',
|
||||
errorHint: '❌再试试~这个作品和上海的创作故事更相关哦!',
|
||||
successMessage: '✅答对啦!《故事新编》是鲁迅在上海故居完成的经典作品集。',
|
||||
},
|
||||
{
|
||||
id: 'biweidaoqiang',
|
||||
name: '“笔为刀枪”',
|
||||
imagePath: 'luxun/reflection/scene2/笔为刀枪',
|
||||
targetBlankId: 'city-5-label-1', // 上海-创作中心
|
||||
errorHint: '❌这个初心是鲁迅在上海创作的核心哦!',
|
||||
successMessage: '✅没错!鲁迅在上海以笔为刃,批判黑暗的社会现实。',
|
||||
errorHint: '❌这个初心是鲁迅在上海创作的核心哦!',
|
||||
successMessage: '✅没错!鲁迅在上海以笔为刃,批判黑暗的社会现实。',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -167,6 +167,8 @@ const LuxunReflectionScene2 = () => {
|
||||
const [completionGuideVisible, setCompletionGuideVisible] = useState(false)
|
||||
// 通关提示是否已经显示过(防止重复显示)
|
||||
const [completionGuideShown, setCompletionGuideShown] = useState(false)
|
||||
// 完成弹窗是否显示
|
||||
const [showCompletionModal, setShowCompletionModal] = useState(false)
|
||||
|
||||
// 转换为 DragFill 的 options
|
||||
const options = useMemo(
|
||||
@@ -280,14 +282,20 @@ const LuxunReflectionScene2 = () => {
|
||||
setSuccessMessage('')
|
||||
}
|
||||
|
||||
// 当所有任务完成且成功提示关闭后,显示通关提示(只显示一次)
|
||||
// 当所有任务完成且成功提示关闭后,显示完成弹窗(只显示一次)
|
||||
useEffect(() => {
|
||||
if (allCompleted && !showSuccessAlert && !completionGuideShown) {
|
||||
setCompletionGuideVisible(true)
|
||||
setShowCompletionModal(true)
|
||||
setCompletionGuideShown(true)
|
||||
}
|
||||
}, [allCompleted, showSuccessAlert, completionGuideShown])
|
||||
|
||||
// 关闭完成弹窗后显示通关提示
|
||||
const handleCompletionModalClose = () => {
|
||||
setShowCompletionModal(false)
|
||||
setCompletionGuideVisible(true)
|
||||
}
|
||||
|
||||
// 获取每个标签区域已放置的元素
|
||||
const getLabelPlacedItem = (cityId: number, labelIndex: number) => {
|
||||
const blankId = `city-${cityId}-label-${labelIndex}`
|
||||
@@ -401,7 +409,7 @@ const LuxunReflectionScene2 = () => {
|
||||
>
|
||||
{/* 任务完成后显示点一点图标 */}
|
||||
{allCompleted && (
|
||||
<div className="absolute -right-20 top-1/2 flex items-center gap-2 z-10 pointer-events-none">
|
||||
<div className="absolute -right-20 top-[calc(50%+26px)] flex items-center gap-2 z-10 pointer-events-none">
|
||||
<img
|
||||
className="animate-finger-bounce scale-x-[-1]"
|
||||
src={getStaticImage(`${starName}/reflection/scene1/鲁迅体悟点一点图标`)}
|
||||
@@ -415,32 +423,20 @@ const LuxunReflectionScene2 = () => {
|
||||
{/* 右侧区域 */}
|
||||
<div className="flex-1 relative">
|
||||
{/* 任务进度 */}
|
||||
<div className="absolute top-4 left-[27%] w-[430px] h-51 flex justify-center items-center">
|
||||
<div className="absolute top-6 left-[24.5%] w-[430px] h-51 flex justify-center items-center">
|
||||
<span className="text-white text-[54px] font-bold [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
|
||||
{allCompleted ? '任务进度已完成' : `任务进度:(${progress}/${totalTasks})`}
|
||||
</span>
|
||||
</div>
|
||||
{/* 鲁迅画廊 */}
|
||||
<div className="absolute top-[calc(2.4rem+204px)] left-0 w-[995px] h-[467px] pt-16 pl-70 pr-10">
|
||||
{/* 提示图标 */}
|
||||
<div className="absolute left-14 top-35 space-y-2 z-10 pointer-events-none">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={getStaticImage(`${starName}/reflection/scene2/鲁迅体悟拖一拖`)}
|
||||
alt="拖一拖"
|
||||
/>
|
||||
<img
|
||||
src={getStaticImage(`${starName}/reflection/scene2/鲁迅体悟拖一拖图标`)}
|
||||
alt="拖一拖图标"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-45 leading-relaxed tracking-wider text-black text-3xl font-bold">
|
||||
请将右侧图标拖到左侧对应位置。
|
||||
</div>
|
||||
{/* 拖拽区域 */}
|
||||
<div className="absolute top-[calc(2.4rem+214px)] h-206 left-0 w-full px-16">
|
||||
{/* 提示文字 */}
|
||||
<div className="my-16 text-yellow-600 text-5xl font-bold text-center">
|
||||
请将下面图标拖到左侧对应的位置。
|
||||
</div>
|
||||
|
||||
{/* 使用 DragFill 组件 */}
|
||||
<div className="mt-10">
|
||||
<div className="w-full max-w-[1000px] mx-auto">
|
||||
<DragFill
|
||||
options={options}
|
||||
blanks={blanks}
|
||||
@@ -451,7 +447,7 @@ const LuxunReflectionScene2 = () => {
|
||||
layout="horizontal-options"
|
||||
hideLabels={true}
|
||||
className="w-full"
|
||||
optionClassName="grid grid-cols-4 gap-10"
|
||||
optionClassName="grid grid-cols-4 gap-x-14 gap-y-14"
|
||||
blankClassName="hidden" // 隐藏默认的空白区域,我们在地图上自定义显示
|
||||
renderOption={(option, isUsed) => {
|
||||
const item = dragItems.find((item) => item.id === option.value)
|
||||
@@ -459,16 +455,19 @@ const LuxunReflectionScene2 = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative transition-all duration-200 cursor-move ${
|
||||
className={`relative transition-all duration-200 cursor-move flex flex-col items-center gap-4 ${
|
||||
isUsed ? 'opacity-30' : 'hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={getStaticImage(item.imagePath)}
|
||||
alt={item.name}
|
||||
className="w-full h-auto"
|
||||
className="w-[150px] h-[150px] object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
<div className="text-black text-3xl font-bold text-center leading-tight px-2 min-w-[160px] max-w-[200px]">
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
@@ -494,43 +493,6 @@ const LuxunReflectionScene2 = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 任务展示窗口 */}
|
||||
<div className="absolute bottom-3.5 right-8 w-[995px] h-[335px] px-10 py-4 overflow-y-auto luxun-scrollbar">
|
||||
{allCompleted ? (
|
||||
// 全部完成后显示详细信息
|
||||
<div className="space-y-8">
|
||||
{cityConfig.map((city) => (
|
||||
<div key={city.id} className="space-y-4">
|
||||
<div className="text-red-600 text-5xl font-bold flex items-end gap-6">
|
||||
<div>{city.location}</div>
|
||||
<div className="text-gray-600 text-4xl font-normal">{city.time}</div>
|
||||
</div>
|
||||
<div className="text-black text-4xl font-normal leading-relaxed">
|
||||
{city.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// 未完成时显示提示内容
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="text-black text-5xl font-bold mb-10">任务展示窗口</div>
|
||||
{/* <div className="text-red-600 text-4xl font-bold">
|
||||
请将以下内容拖拽至正确位置
|
||||
</div> */}
|
||||
<div className="grid grid-cols-4 gap-4 text-3xl font-bold">
|
||||
{dragItems.map((item) => {
|
||||
const isPlaced = dragFillValue.some((result) => result.optionValue === item.id)
|
||||
return (
|
||||
<div key={item.id} className={`${isPlaced ? 'text-green-600' : 'text-black'}`}>
|
||||
{item.name}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示弹窗 */}
|
||||
@@ -548,6 +510,84 @@ const LuxunReflectionScene2 = () => {
|
||||
onClose={handleSuccessAlertClose}
|
||||
/>
|
||||
|
||||
{/* 完成弹窗 */}
|
||||
{showCompletionModal && (
|
||||
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60">
|
||||
<div
|
||||
className="relative w-[1200px] p-12 bg-cover bg-center bg-no-repeat rounded-2xl shadow-2xl"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to bottom, rgba(255, 248, 220, 0.95), rgba(255, 248, 220, 0.98)), url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48cGF0dGVybiBpZD0icGFwZXIiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2ZmZjhkYyIvPjwvcGF0dGVybj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9InVybCgjcGFwZXIpIi8+PC9zdmc+)`,
|
||||
border: '4px solid #8B4513',
|
||||
boxShadow: '0 0 30px rgba(139, 69, 19, 0.4), inset 0 0 50px rgba(218, 165, 32, 0.2)',
|
||||
}}
|
||||
>
|
||||
{/* 装饰性边框 */}
|
||||
<div className="absolute inset-4 border-2 border-yellow-700/40 rounded-xl pointer-events-none" />
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={handleCompletionModalClose}
|
||||
className="absolute top-6 right-6 w-12 h-12 flex items-center justify-center text-red-700 hover:text-red-900 text-5xl font-bold bg-yellow-100/80 rounded-full hover:bg-yellow-200/80 transition-colors z-10"
|
||||
style={{ lineHeight: '1' }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
{/* 标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<div
|
||||
className="text-6xl font-bold mb-2"
|
||||
style={{
|
||||
background: 'linear-gradient(to bottom, #8B0000, #DC143C)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: '2px 2px 4px rgba(139, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
恭喜完成任务!
|
||||
</div>
|
||||
<div className="text-3xl text-yellow-800 font-semibold mt-6">
|
||||
鲁迅的文学足迹已然清晰
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 两列布局 */}
|
||||
<div className="grid grid-cols-2 gap-8 px-4">
|
||||
{cityConfig.map((city, index) => (
|
||||
<div
|
||||
key={city.id}
|
||||
className="bg-gradient-to-r from-yellow-50/80 to-amber-50/80 p-6 rounded-xl border-2 border-yellow-600/30 shadow-md hover:shadow-lg transition-shadow"
|
||||
style={{
|
||||
animation: `fadeIn 0.5s ease-out ${index * 0.1}s both`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-end gap-4 mb-3">
|
||||
<div className="text-red-700 text-4xl font-bold">{city.location}</div>
|
||||
<div className="text-amber-700 text-2xl font-medium pb-0.5">{city.time}</div>
|
||||
</div>
|
||||
<div className="text-gray-800 text-2xl leading-relaxed font-normal pl-2 border-l-4 border-red-600/40">
|
||||
{city.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 学习引导对话 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Image, GuideSteps, GuideStepsItem, StudyGuideModal, EndingOverlay } from '@/components'
|
||||
import { SingleChoiceQuizPanel } from '@/components/layout/quiz'
|
||||
@@ -16,6 +16,8 @@ type HintPosition = 'luxun1' | 'guanzhu' | 'piping' | 'jiaguo' | 'luxun2' | 'non
|
||||
const LuxunReflectionScene3 = () => {
|
||||
//碎片动画触发方法
|
||||
const { triggerPuzzleAnimation } = usePuzzleAnimation()
|
||||
const triggerPuzzleAnimationRef = useRef(triggerPuzzleAnimation)
|
||||
triggerPuzzleAnimationRef.current = triggerPuzzleAnimation
|
||||
const navigate = useNavigate()
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string
|
||||
@@ -32,6 +34,7 @@ const LuxunReflectionScene3 = () => {
|
||||
const [completedQuiz3, setCompletedQuiz3] = useState(false) // 题目3是否完成
|
||||
const [showSummary, setShowSummary] = useState(false) // 是否显示总结
|
||||
const [completionGuideVisible, setCompletionGuideVisible] = useState(false) // 通关提示对话是否显示
|
||||
const [completionGuideQueued, setCompletionGuideQueued] = useState(false) // 通关提示是否已排队显示
|
||||
|
||||
const [showGuideModal, setShowGuideModal] = useState(true)
|
||||
// 成功提示相关状态
|
||||
@@ -47,14 +50,14 @@ const LuxunReflectionScene3 = () => {
|
||||
|
||||
// 当显示总结后,延迟1秒显示通关提示对话
|
||||
useEffect(() => {
|
||||
if (showSummary) {
|
||||
if (completionGuideQueued) {
|
||||
const timer = setTimeout(() => {
|
||||
setCompletionGuideVisible(true)
|
||||
triggerPuzzleAnimation(15)
|
||||
triggerPuzzleAnimationRef.current(15)
|
||||
}, 500)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [showSummary, triggerPuzzleAnimation])
|
||||
}, [completionGuideQueued])
|
||||
|
||||
// 点击"鲁迅1"的处理
|
||||
const handleLuxun1Click = () => {
|
||||
@@ -121,8 +124,9 @@ const LuxunReflectionScene3 = () => {
|
||||
|
||||
// 点击"鲁迅2"的处理
|
||||
const handleLuxun2Click = () => {
|
||||
if (allCompleted) {
|
||||
if (allCompleted && !showSummary) {
|
||||
setShowSummary(true)
|
||||
setCompletionGuideQueued(true)
|
||||
setHintPosition('none') // 隐藏提示图标
|
||||
}
|
||||
}
|
||||
@@ -370,7 +374,7 @@ const LuxunReflectionScene3 = () => {
|
||||
{ value: 'B', label: 'B. 生活中的小美好' },
|
||||
]}
|
||||
correctAnswer="A"
|
||||
errorMessage="不是哦,先生的小说核心是批判而非歌颂哦!请结合阿Q的碎片回忆一下吧!"
|
||||
errorMessage="不是哦,先生的小说核心是批判而非歌颂哦!请结合阿Q的碎片回忆一下吧!"
|
||||
onSubmit={handleQuiz2Submit}
|
||||
/>
|
||||
|
||||
@@ -383,7 +387,7 @@ const LuxunReflectionScene3 = () => {
|
||||
{ value: 'B', label: 'B. 只为个人成名' },
|
||||
]}
|
||||
correctAnswer="A"
|
||||
errorMessage="不是哦,先生的创作从不是为了自己哦!请回忆先生“弃医从文”的初心。"
|
||||
errorMessage="不是哦,先生的创作从不是为了自己哦!请回忆先生“弃医从文”的初心。"
|
||||
onSubmit={handleQuiz3Submit}
|
||||
/>
|
||||
|
||||
|
||||
@@ -403,6 +403,8 @@ const MaozedongInternalization = () => {
|
||||
className="h-full w-full z-20"
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="maozedong_internalization_intro"
|
||||
showReadonlyProgress
|
||||
autoPlay
|
||||
onEnded={() => {
|
||||
setShowIntroVideo(false)
|
||||
@@ -482,6 +484,9 @@ const MaozedongInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="maozedong_internalization_main"
|
||||
showReadonlyProgress
|
||||
autoPlay
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
|
||||
@@ -217,6 +217,8 @@ const MaozedongPlace1 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="maozedong_place1_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo2(false)
|
||||
setQuizVisible(true)
|
||||
|
||||
@@ -213,6 +213,8 @@ const MaozedongPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="maozedong_place2_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
setQuizVisible(true)
|
||||
|
||||
@@ -245,6 +245,8 @@ const MaozedongPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="maozedong_place3_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo3(false)
|
||||
setQuiz3Visible(true)
|
||||
|
||||
@@ -305,13 +305,13 @@ const MaozedongPlace4 = () => {
|
||||
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>
|
||||
|
||||
@@ -434,6 +434,8 @@ const MaozedongPlace4 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="maozedong_place4_video1"
|
||||
showReadonlyProgress
|
||||
onSkip={() => {
|
||||
setShowVideo4(false)
|
||||
setGuide4SummaryStep(0)
|
||||
@@ -456,6 +458,8 @@ const MaozedongPlace4 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="maozedong_place4_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo2(false)
|
||||
setQuizStep(1)
|
||||
|
||||
@@ -44,7 +44,7 @@ const firstQuizOptions = [
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: 'B. 深入基层,收集各地党组织、工农运动实情,再推进筹备等备',
|
||||
label: 'B. 深入基层,收集各地党组织、工农运动实情,再推进筹备',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@ const secondQuizOptions = [
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: 'B. 讲清意见对四大讨论无产阶级领导权、工农联盟的重要性,以革命大义动员党员',
|
||||
label: 'B. 讲清意见对四大讨论无产阶级领导权、工农联盟的重要性,以革命大义动员',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -487,7 +487,7 @@ const Home = () => {
|
||||
visible={endingVisible}
|
||||
audioSrc={getPublicPath('audio/maozedong/mp3_mzd_23.mp3')}
|
||||
animationSpeed={200}
|
||||
text="毛泽东在中共四大筹备期间的三次重要抉择,均锚定真实实践、扎根基层,为四大首次明确无产阶级领导权、工农联盟筑牢基础。虽因身体健康原因未参会,但其筹备工作与后续调研,如纽带般连接理论与实践,让党的革命理念在历史浪潮中,朝着正确方向坚定前行,成为早期共产党人探索革命道路的生动注脚,影响深远,照耀后世奋进征程。"
|
||||
text="毛泽东在中共四大筹备期间的三次重要抉择,均锚定真实实践、扎根基层,为四大首次明确无产阶级领导权、工农联盟筑牢基础。虽因身体健康问题未参会,但其筹备工作与后续调研,如纽带般连接理论与实践,让党的革命理念在历史浪潮中,朝着正确方向坚定前行,成为早期共产党人探索革命道路的生动注脚,影响深远,照耀后世奋进征程。"
|
||||
onContinue={() => {
|
||||
setEndingVisible(false)
|
||||
saveMaozedongStore()
|
||||
|
||||
@@ -199,7 +199,7 @@ const NieerInternalization = () => {
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title={item.id === 'quiz-4' ? '恭喜你, 判断正确!' : '恭喜你, 选择正确!'}
|
||||
title={item.id === 'quiz-4' ? '恭喜你,判断正确!' : '恭喜你,选择正确!'}
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
@@ -326,6 +326,9 @@ const NieerInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="nieer_internalization_video"
|
||||
showReadonlyProgress
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -216,13 +216,13 @@ const NieerPlace1 = () => {
|
||||
这里提到聂耳发表了《中国歌舞短论》,批评脱离民众的音乐,这是为什么呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/nieer/mp3_nieer_06_1.mp3')}>
|
||||
你观察得很细致!这时的聂耳在明月歌舞剧社做练习生,社长李锦晖看中了他的潜力,虽然他当时小提琴水平不算专业,还是录取了他,还安排首席小提琴手专门辅导他。聂耳特别刻苦,每天至少练7个小时小提琴,很快就成长为首席小提琴手。
|
||||
你观察得很细致!这时的聂耳在明月歌舞剧社做练习生,社长李锦晖看中了他的潜力,虽然他当时小提琴水平不算专业,但还是录取了他,还安排首席小提琴手专门辅导他。聂耳特别刻苦,每天至少练7个小时小提琴,很快就成长为首席小提琴手。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/nieer/mp3_nieer_06_2.mp3')}>
|
||||
但好景不长,1931年九·一八事变后,全国抗日救亡浪潮高涨,可还存在很多脱离现实的靡靡之音,这让满怀爱国热忱的聂耳特别愤懑。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/nieer/mp3_nieer_06_3.mp3')}>
|
||||
1932年,他以“黑天使”为笔名在《电影艺术》杂志刊登《中国歌舞短论》,猛烈评击“软豆腐”式的音乐,提出我们需要“真刀真枪的硬功夫”,主张音乐要贴近民众、为抗日救亡服务。后来他离开了歌舞剧社。
|
||||
1932年,他以“黑天使”为笔名在《电影艺术》杂志刊登《中国歌舞短论》,猛烈抨击“软豆腐”式的音乐,提出我们需要“真刀真枪的硬功夫”,主张音乐要贴近民众、为抗日救亡服务。后来他离开了歌舞剧社。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
@@ -315,6 +315,8 @@ const NieerPlace1 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="nieer_place1_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
if (video2Step === 1) {
|
||||
setVideo2Step(2)
|
||||
@@ -350,7 +352,7 @@ const NieerPlace1 = () => {
|
||||
},
|
||||
]}
|
||||
correctAnswer="B"
|
||||
errorMessage="“软豆腐”式的音乐是聂耳猛烈评击的音乐类型。请你再想想吧!"
|
||||
errorMessage="“软豆腐”式的音乐是聂耳猛烈抨击的音乐类型。请你再想想吧!"
|
||||
onSubmit={(_value, isCorrect) => {
|
||||
if (isCorrect) {
|
||||
setQuiz1Visible(false)
|
||||
|
||||
@@ -194,6 +194,8 @@ const NieerPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="nieer_place2_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
markExhibitVisited(place, 2)
|
||||
setShowVideo2(false)
|
||||
|
||||
@@ -245,6 +245,8 @@ const NieerPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="nieer_place3_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
const id = videoForExhibitId
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -403,7 +403,7 @@ const NieerPlace5 = () => {
|
||||
|
||||
<SuccessAlert
|
||||
visible={success3_1Visible}
|
||||
title="恭喜你, 匹配正确!"
|
||||
title="恭喜你,匹配正确!"
|
||||
onClose={() => {
|
||||
setSuccess3_1Visible(false)
|
||||
setQuiz3_2Visible(true)
|
||||
|
||||
@@ -200,7 +200,10 @@ const ContentDialog = ({ starName, title, scene, onClose, onEnterScene }: Conten
|
||||
src={getPublicPath(currentPage.src)}
|
||||
autoPlay
|
||||
playsInline
|
||||
controls
|
||||
showSkipButton
|
||||
skipText="跳过"
|
||||
skipKey={`nieer_reflection_scene${scene.id}_video`}
|
||||
showReadonlyProgress
|
||||
className="w-full h-full"
|
||||
onLoad={() => setContentLoaded(true)}
|
||||
onEnded={handleVideoEnded}
|
||||
|
||||
@@ -453,6 +453,9 @@ const RongdeshengInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="rongdesheng_internalization_video"
|
||||
showReadonlyProgress
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -211,6 +211,8 @@ const RongdeshengPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place2_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
onSkip={handleVideoEnded}
|
||||
/>
|
||||
@@ -222,6 +224,8 @@ const RongdeshengPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place2_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
onSkip={handleVideoEnded}
|
||||
/>
|
||||
@@ -233,6 +237,8 @@ const RongdeshengPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place2_video3"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
onSkip={handleVideoEnded}
|
||||
/>
|
||||
@@ -244,6 +250,8 @@ const RongdeshengPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place2_video4"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
onSkip={handleVideoEnded}
|
||||
/>
|
||||
|
||||
@@ -186,6 +186,8 @@ const RongdeshengPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place3_video"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
onSkip={handleVideoEnded}
|
||||
/>
|
||||
|
||||
@@ -206,6 +206,8 @@ const RongdeshengPlace4 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place4_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
onSkip={handleVideoEnded}
|
||||
/>
|
||||
@@ -218,6 +220,8 @@ const RongdeshengPlace4 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="rongdesheng_place4_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={handleEndingVideoEnded}
|
||||
onSkip={handleEndingVideoEnded}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Image, StudyGuideModal, Video } from '@/components'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { useRongdeshengStore } from '@/store/rongdeshengStore'
|
||||
import { saveRongdeshengStore, useRongdeshengStore } from '@/store/rongdeshengStore'
|
||||
import { FinalScene } from './components/FinalScene'
|
||||
import { HintPanel } from './components/HintPanel'
|
||||
import { ModuleNavigation } from './components/ModuleNavigation'
|
||||
@@ -214,6 +214,7 @@ const RongdeshengReflection = () => {
|
||||
},
|
||||
}))
|
||||
markReflectionVideoViewed(currentModuleId, videoId)
|
||||
saveRongdeshengStore()
|
||||
|
||||
setVideoPlayingState({ isPlaying: false, currentVideoId: null, currentVideoSrc: null })
|
||||
}
|
||||
@@ -225,6 +226,7 @@ const RongdeshengReflection = () => {
|
||||
[currentModuleId]: { ...prev[currentModuleId], completed: true },
|
||||
}))
|
||||
markReflectionCompleted(currentModuleId)
|
||||
saveRongdeshengStore()
|
||||
if (currentModuleId === 'scene3') {
|
||||
triggerPuzzleAnimation(14)
|
||||
}
|
||||
@@ -322,6 +324,12 @@ const RongdeshengReflection = () => {
|
||||
src={videoPlayingState.currentVideoSrc}
|
||||
autoPlay
|
||||
showSkipButton
|
||||
skipKey={
|
||||
videoPlayingState.currentVideoId
|
||||
? `rongdesheng_reflection_${videoPlayingState.currentVideoId}`
|
||||
: undefined
|
||||
}
|
||||
showReadonlyProgress
|
||||
onEnded={handleVideoEnded}
|
||||
className="fixed inset-0 z-50 w-full h-full bg-black"
|
||||
/>
|
||||
@@ -378,8 +386,11 @@ const RongdeshengReflection = () => {
|
||||
setCourseTimeProgress(20)
|
||||
setFinalVideoStage('ended')
|
||||
markReflectionFinalCompleted()
|
||||
saveRongdeshengStore()
|
||||
}}
|
||||
onExit={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`)
|
||||
}}
|
||||
onExit={() => navigate(`/${moduleName}/${starName}/home`)}
|
||||
/>
|
||||
|
||||
{/* 学习引导弹窗:首次进入时显示 */}
|
||||
@@ -388,6 +399,7 @@ const RongdeshengReflection = () => {
|
||||
onContinue={() => {
|
||||
setShowGuide(false)
|
||||
markReflectionGuideShown()
|
||||
saveRongdeshengStore()
|
||||
}}
|
||||
audioSrc={getPublicPath('audio/rongdesheng/mp3_rongdesheng_31.mp3')}
|
||||
firstLineIndent="2em"
|
||||
|
||||
@@ -85,8 +85,7 @@ export const MODULES: Module[] = [
|
||||
resultContent:
|
||||
'护厂抗争的胜利,是民族工业坚守爱国初心的体现,也是国人联合“反抗压迫、团结抗争”的印证!',
|
||||
resultImage: 'rongdesheng/reflection/场景2结算',
|
||||
wrongHint:
|
||||
'荣氏兄弟坚守“实业救国”初心,拒绝洋行干涉企业经营,不愿民族产业沦为外资附庸!试试其他方案吧~',
|
||||
wrongHint: '荣氏兄弟坚守“实业救国”初心,绝不会将民族产业拱手让人!试试其他方案吧~',
|
||||
audio: { wrongHint: '' },
|
||||
},
|
||||
{
|
||||
@@ -94,7 +93,7 @@ export const MODULES: Module[] = [
|
||||
title: '民生坚守',
|
||||
backgroundImage: 'rongdesheng/reflection/场景3选中',
|
||||
question:
|
||||
'时局动荡,棉花原料涨价,工厂盈利下滑,职工生活面临困难。荣德生面对既要维持生产,又要保障职工生活,该怎么做?',
|
||||
'时局动荡,棉花原料涨价,工厂盈利下滑,职工生活面临困难。既要维持生产,又要保障职工生活,该怎么做?',
|
||||
options: [
|
||||
{ id: 'opt3-1', text: 'A.削减职工工资,降低运营成本', isCorrect: false },
|
||||
{
|
||||
@@ -176,7 +175,7 @@ export const MODULES: Module[] = [
|
||||
title: '革新突围',
|
||||
backgroundImage: 'rongdesheng/reflection/场景5选中',
|
||||
question:
|
||||
'抗战胜利后,洋纱、洋布卷土重来,申新设备陈旧、技术落后,面临产品竞争力不足的困境,荣德生如何通过技术革新让民族纱厂站稳脚跟?',
|
||||
'抗战胜利后,洋纱、洋布卷土重来,申新设备陈旧、技术落后,面临产品竞争力不足的困境,如何通过技术革新让民族纱厂站稳脚跟?',
|
||||
options: [
|
||||
{
|
||||
id: 'opt5-1',
|
||||
|
||||
@@ -315,7 +315,6 @@ const SunzhongshanInternalization = () => {
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content=""
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
@@ -466,6 +465,9 @@ const SunzhongshanInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="sunzhongshan_internalization_video"
|
||||
showReadonlyProgress
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
|
||||
@@ -441,6 +441,8 @@ const SunzhongshanPlace1 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="sunzhongshan_place1_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo4(false)
|
||||
setDragQuiz4Visible(true)
|
||||
@@ -513,6 +515,8 @@ const SunzhongshanPlace1 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="sunzhongshan_place1_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo4Second(false)
|
||||
setQuiz3Visible(true)
|
||||
@@ -569,7 +573,7 @@ const SunzhongshanPlace1 = () => {
|
||||
中华民国的建立有个重要背景——“南北议和”。辛亥革命中宣告独立的省份大多集中在中国南方,负隅顽抗的清政府将最后希望寄托在主导北洋军阀势力的袁世凯身上。在列强支持下,袁世凯率军南下。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide" audioSrc={audio('17_2')}>
|
||||
他一边以武力威胁革命势力,一边又迎合革命党人急于完成统一的愿望,诱使其开展和议。当时,革命党人力量还不够强。为避免国家陷入前途未卜的内战,孙中山先生发表声明:只要清帝退位,袁世凯赞成共和,就推举他做大总统。
|
||||
他一边以武力威胁革命势力,一边又迎合革命党人急于完成统一的愿望,诱使其开展和议。当时,革命党人力量还不够强,为避免国家陷入前途未卜的内战,孙中山先生发表声明:只要清帝退位,袁世凯赞成共和,就推举他做大总统。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ const SunzhongshanPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="sunzhongshan_place2_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo1(false)
|
||||
setSingleQuiz1Visible(true)
|
||||
@@ -403,6 +405,8 @@ const SunzhongshanPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="sunzhongshan_place2_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo2(false)
|
||||
setSingleQuiz2Visible(true)
|
||||
|
||||
@@ -293,6 +293,8 @@ const SunzhongshanPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="sunzhongshan_place3_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo2(false)
|
||||
setDragQuiz2Visible(true)
|
||||
|
||||
@@ -167,6 +167,8 @@ const SunzhongshanPlace4 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="sunzhongshan_place4_video"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo1(false)
|
||||
// 视频播放结束后,显示学习引导
|
||||
|
||||
@@ -86,10 +86,13 @@ const SunzhongshanReflectionScene1 = () => {
|
||||
const [showQuiz3, setShowQuiz3] = useState(false)
|
||||
const [showCompleteGuide, setShowCompleteGuide] = useState(false)
|
||||
const [pendingQuiz, setPendingQuiz] = useState<'quiz2' | 'quiz3' | 'complete' | null>(null)
|
||||
const [hasClickedClueHint, setHasClickedClueHint] = useState(false)
|
||||
const [hasClickedClueBook, setHasClickedClueBook] = useState(false)
|
||||
|
||||
const { collectClue, isClueCollected } = useSunzhongshanStore()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const clueBookButtonRef = useRef<HTMLDivElement>(null)
|
||||
const backpackButtonRef = useRef<HTMLDivElement>(null)
|
||||
// 为每个线索创建 ref
|
||||
const clueRefs = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(new Map())
|
||||
|
||||
@@ -147,13 +150,10 @@ const SunzhongshanReflectionScene1 = () => {
|
||||
// 收集线索1
|
||||
collectClue(clue1Id, clue1Name)
|
||||
|
||||
// 获取线索1的 ref
|
||||
const clue1Ref = getClueRef(clue1Id)
|
||||
|
||||
// 执行飞行动画
|
||||
if (clue1Ref.current && clueBookButtonRef.current) {
|
||||
// 执行飞行动画 - 从背包按钮开始
|
||||
if (backpackButtonRef.current && clueBookButtonRef.current) {
|
||||
flyToAnimation({
|
||||
fromRef: clue1Ref as React.RefObject<HTMLElement>,
|
||||
fromRef: backpackButtonRef as React.RefObject<HTMLElement>,
|
||||
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
||||
targetPosition: 'center',
|
||||
duration: 1200,
|
||||
@@ -168,6 +168,7 @@ const SunzhongshanReflectionScene1 = () => {
|
||||
|
||||
// 处理线索提示按钮点击 - 打开线索提示框
|
||||
const handleClueHintClick = () => {
|
||||
setHasClickedClueHint(true)
|
||||
setShowClueHint(true)
|
||||
}
|
||||
|
||||
@@ -341,11 +342,9 @@ const SunzhongshanReflectionScene1 = () => {
|
||||
style={{
|
||||
top: clue.position.top,
|
||||
left: clue.position.left,
|
||||
width: clue.position.width,
|
||||
height: clue.position.height,
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative inline-block">
|
||||
<Image
|
||||
src={getClueImage(clue.id)}
|
||||
className="w-20 h-20 border-2 border-yellow-400 rounded shadow-lg"
|
||||
@@ -377,22 +376,47 @@ const SunzhongshanReflectionScene1 = () => {
|
||||
|
||||
{/* 右下角-功能处 */}
|
||||
<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 className="relative">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/线索提示`)}
|
||||
button
|
||||
onClick={handleClueHintClick}
|
||||
/>
|
||||
{!hasClickedClueHint && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={backpackButtonRef} className="relative">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/背包`)}
|
||||
button
|
||||
onClick={handleBackpackClick}
|
||||
/>
|
||||
{hasClickedClueHint && !isClueCollected(`${sceneId}/clue1`) && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={clueBookButtonRef} className="relative" data-clue-book-button="true">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/线索簿`)}
|
||||
button
|
||||
onClick={() => setShowClueBook(true)}
|
||||
onClick={() => {
|
||||
setHasClickedClueBook(true)
|
||||
setShowClueBook(true)
|
||||
}}
|
||||
/>
|
||||
{hasClickedClueHint && isClueCollected(`${sceneId}/clue1`) && !hasClickedClueBook && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -452,7 +476,7 @@ const SunzhongshanReflectionScene1 = () => {
|
||||
{/* 题目1:今天的日期是? */}
|
||||
<SingleChoiceQuizPanel
|
||||
visible={showQuiz1}
|
||||
title="【单选】今天的日期是?"
|
||||
title="【单选】今天的日期是:"
|
||||
options={[
|
||||
{ value: 'A', label: 'A.1919年5月5日' },
|
||||
{ value: 'B', label: 'B.1921年5月5日' },
|
||||
|
||||
@@ -96,10 +96,13 @@ const SunzhongshanReflectionScene2 = () => {
|
||||
const [showQuiz3, setShowQuiz3] = useState(false)
|
||||
const [showCompleteGuide, setShowCompleteGuide] = useState(false)
|
||||
const [pendingQuiz, setPendingQuiz] = useState<'quiz2' | 'quiz3' | 'complete' | null>(null)
|
||||
const [hasClickedClueHint, setHasClickedClueHint] = useState(false)
|
||||
const [hasClickedClueBook, setHasClickedClueBook] = useState(false)
|
||||
|
||||
const { collectClue, isClueCollected } = useSunzhongshanStore()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const clueBookButtonRef = useRef<HTMLDivElement>(null)
|
||||
const backpackButtonRef = useRef<HTMLDivElement>(null)
|
||||
// 为每个线索创建 ref
|
||||
const clueRefs = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(new Map())
|
||||
|
||||
@@ -157,13 +160,10 @@ const SunzhongshanReflectionScene2 = () => {
|
||||
// 收集线索1
|
||||
collectClue(clue1Id, clue1Name)
|
||||
|
||||
// 获取线索1的 ref
|
||||
const clue1Ref = getClueRef(clue1Id)
|
||||
|
||||
// 执行飞行动画
|
||||
if (clue1Ref.current && clueBookButtonRef.current) {
|
||||
// 执行飞行动画 - 从背包按钮开始
|
||||
if (backpackButtonRef.current && clueBookButtonRef.current) {
|
||||
flyToAnimation({
|
||||
fromRef: clue1Ref as React.RefObject<HTMLElement>,
|
||||
fromRef: backpackButtonRef as React.RefObject<HTMLElement>,
|
||||
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
||||
targetPosition: 'center',
|
||||
duration: 1200,
|
||||
@@ -178,6 +178,7 @@ const SunzhongshanReflectionScene2 = () => {
|
||||
|
||||
// 处理线索提示按钮点击 - 打开线索提示框
|
||||
const handleClueHintClick = () => {
|
||||
setHasClickedClueHint(true)
|
||||
setShowClueHint(true)
|
||||
}
|
||||
|
||||
@@ -355,11 +356,9 @@ const SunzhongshanReflectionScene2 = () => {
|
||||
style={{
|
||||
top: clue.position.top,
|
||||
left: clue.position.left,
|
||||
width: clue.position.width,
|
||||
height: clue.position.height,
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative inline-block">
|
||||
<Image
|
||||
src={getClueImage(clue.id)}
|
||||
className="w-20 h-20 border-2 border-yellow-400 rounded shadow-lg"
|
||||
@@ -391,22 +390,47 @@ const SunzhongshanReflectionScene2 = () => {
|
||||
|
||||
{/* 右下角-功能处 */}
|
||||
<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 className="relative">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/线索提示`)}
|
||||
button
|
||||
onClick={handleClueHintClick}
|
||||
/>
|
||||
{!hasClickedClueHint && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={backpackButtonRef} className="relative">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/背包`)}
|
||||
button
|
||||
onClick={handleBackpackClick}
|
||||
/>
|
||||
{hasClickedClueHint && !isClueCollected(`${sceneId}/clue1`) && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={clueBookButtonRef} className="relative" data-clue-book-button="true">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/线索簿`)}
|
||||
button
|
||||
onClick={() => setShowClueBook(true)}
|
||||
onClick={() => {
|
||||
setHasClickedClueBook(true)
|
||||
setShowClueBook(true)
|
||||
}}
|
||||
/>
|
||||
{hasClickedClueHint && isClueCollected(`${sceneId}/clue1`) && !hasClickedClueBook && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -94,10 +94,13 @@ const SunzhongshanReflectionScene3 = () => {
|
||||
const [isMainScene, setIsMainScene] = useState(false)
|
||||
// 控制 Cursor 显示:获取线索1后显示,点击后进入主场景
|
||||
const [showCursor, setShowCursor] = useState(false)
|
||||
const [hasClickedClueHint, setHasClickedClueHint] = useState(false)
|
||||
const [hasClickedClueBook, setHasClickedClueBook] = useState(false)
|
||||
|
||||
const { collectClue, isClueCollected } = useSunzhongshanStore()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const clueBookButtonRef = useRef<HTMLDivElement>(null)
|
||||
const backpackButtonRef = useRef<HTMLDivElement>(null)
|
||||
// 为每个线索创建 ref
|
||||
const clueRefs = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(new Map())
|
||||
|
||||
@@ -169,13 +172,10 @@ const SunzhongshanReflectionScene3 = () => {
|
||||
// 收集线索4
|
||||
collectClue(clue4Id, clue4Name)
|
||||
|
||||
// 获取线索4的 ref
|
||||
const clue4Ref = getClueRef(clue4Id)
|
||||
|
||||
// 执行飞行动画
|
||||
if (clue4Ref.current && clueBookButtonRef.current) {
|
||||
// 执行飞行动画 - 从报纸按钮开始
|
||||
if (backpackButtonRef.current && clueBookButtonRef.current) {
|
||||
flyToAnimation({
|
||||
fromRef: clue4Ref as React.RefObject<HTMLElement>,
|
||||
fromRef: backpackButtonRef as React.RefObject<HTMLElement>,
|
||||
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
||||
targetPosition: 'center',
|
||||
duration: 1200,
|
||||
@@ -190,6 +190,7 @@ const SunzhongshanReflectionScene3 = () => {
|
||||
|
||||
// 处理线索提示按钮点击 - 打开线索提示框
|
||||
const handleClueHintClick = () => {
|
||||
setHasClickedClueHint(true)
|
||||
setShowClueHint(true)
|
||||
}
|
||||
|
||||
@@ -397,11 +398,9 @@ const SunzhongshanReflectionScene3 = () => {
|
||||
style={{
|
||||
top: clue.position.top,
|
||||
left: clue.position.left,
|
||||
width: clue.position.width,
|
||||
height: clue.position.height,
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative inline-block">
|
||||
<Image
|
||||
src={getClueImage(clue.id)}
|
||||
className="w-20 h-20 border-2 border-yellow-400 rounded shadow-lg"
|
||||
@@ -433,22 +432,47 @@ const SunzhongshanReflectionScene3 = () => {
|
||||
|
||||
{/* 右下角-功能处 */}
|
||||
<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 className="relative">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/线索提示`)}
|
||||
button
|
||||
onClick={handleClueHintClick}
|
||||
/>
|
||||
{!isMainScene && !hasClickedClueHint && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={backpackButtonRef} className="relative">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/scene3/报纸`)}
|
||||
button
|
||||
onClick={handleBackpackClick}
|
||||
/>
|
||||
{isMainScene && !isClueCollected(`${sceneId}/clue4`) && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={clueBookButtonRef} className="relative" data-clue-book-button="true">
|
||||
<Image
|
||||
src={getStaticImage(`${starName}/reflection/线索簿`)}
|
||||
button
|
||||
onClick={() => setShowClueBook(true)}
|
||||
onClick={() => {
|
||||
setHasClickedClueBook(true)
|
||||
setShowClueBook(true)
|
||||
}}
|
||||
/>
|
||||
{isMainScene && isClueCollected(`${sceneId}/clue4`) && !hasClickedClueBook && (
|
||||
<Image
|
||||
className="absolute right-full top-1/2 z-20 mr-2 h-28 w-34 -translate-y-1/2 animate-finger-shake pointer-events-none"
|
||||
src={getStaticImage('components/点击查看')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { toJpeg } from 'html-to-image'
|
||||
|
||||
export const STAR_NAME = 'zhangleping'
|
||||
|
||||
// ─── 类型定义 ───────────────────────────────────────────────
|
||||
@@ -210,33 +208,110 @@ export function getTopBarSrc(block: SidebarBlockKey): string {
|
||||
return `${STAR_NAME}/reflection/draw/素材库条`
|
||||
}
|
||||
|
||||
// ─── DOM 截图生成 ─────────────────────────────────────────────
|
||||
// ─── Canvas 合成生成 ───────────────────────────────────────────
|
||||
|
||||
export async function buildCompositeDataUrl(compositionEl: HTMLElement): Promise<string> {
|
||||
// 临时移除定位偏移,避免截图出现留白/偏移
|
||||
const { position, left, right, top, bottom } = compositionEl.style
|
||||
Object.assign(compositionEl.style, {
|
||||
position: 'fixed',
|
||||
left: '0',
|
||||
right: 'auto',
|
||||
top: '0',
|
||||
bottom: 'auto',
|
||||
interface CompositeImageLayer {
|
||||
src: string
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
zIndex: number
|
||||
}
|
||||
|
||||
const EXPORT_WIDTH = 960
|
||||
const EXPORT_HEIGHT = 540
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new window.Image()
|
||||
image.decoding = 'async'
|
||||
image.onload = () => resolve(image)
|
||||
image.onerror = reject
|
||||
image.src = src
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
return await toJpeg(compositionEl, {
|
||||
cacheBust: false,
|
||||
skipFonts: true,
|
||||
width: compositionEl.offsetWidth,
|
||||
height: compositionEl.offsetHeight,
|
||||
canvasWidth: 960,
|
||||
canvasHeight: 540,
|
||||
pixelRatio: 1,
|
||||
quality: 0.82,
|
||||
backgroundColor: '#fff',
|
||||
})
|
||||
} finally {
|
||||
// 恢复原始样式
|
||||
Object.assign(compositionEl.style, { position, left, right, top, bottom })
|
||||
function getBackgroundUrl(element: HTMLElement): string | null {
|
||||
const backgroundImage = window.getComputedStyle(element).backgroundImage
|
||||
const match = backgroundImage.match(/^url\(["']?(.+?)["']?\)$/)
|
||||
return match?.[1] ?? null
|
||||
}
|
||||
|
||||
function getLayerZIndex(element: HTMLElement, root: HTMLElement): number {
|
||||
let current: HTMLElement | null = element
|
||||
|
||||
while (current && current !== root) {
|
||||
const zIndex = Number(window.getComputedStyle(current).zIndex)
|
||||
if (Number.isFinite(zIndex)) return zIndex
|
||||
current = current.parentElement
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function getImageLayer(
|
||||
element: HTMLImageElement,
|
||||
root: HTMLElement,
|
||||
rootRect: DOMRect
|
||||
): CompositeImageLayer {
|
||||
const rect = element.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
src: element.currentSrc || element.src,
|
||||
x: rect.left - rootRect.left,
|
||||
y: rect.top - rootRect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
zIndex: getLayerZIndex(element, root),
|
||||
}
|
||||
}
|
||||
|
||||
async function drawLayer(
|
||||
context: CanvasRenderingContext2D,
|
||||
layer: CompositeImageLayer,
|
||||
scaleX: number,
|
||||
scaleY: number
|
||||
) {
|
||||
const image = await loadImage(layer.src)
|
||||
context.drawImage(
|
||||
image,
|
||||
layer.x * scaleX,
|
||||
layer.y * scaleY,
|
||||
layer.width * scaleX,
|
||||
layer.height * scaleY
|
||||
)
|
||||
}
|
||||
|
||||
export async function buildCompositeDataUrl(compositionEl: HTMLElement): Promise<string> {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = EXPORT_WIDTH
|
||||
canvas.height = EXPORT_HEIGHT
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) throw new Error('Canvas is not available')
|
||||
|
||||
context.fillStyle = '#fff'
|
||||
context.fillRect(0, 0, EXPORT_WIDTH, EXPORT_HEIGHT)
|
||||
|
||||
const rootRect = compositionEl.getBoundingClientRect()
|
||||
const scaleX = EXPORT_WIDTH / rootRect.width
|
||||
const scaleY = EXPORT_HEIGHT / rootRect.height
|
||||
const backgroundUrl = getBackgroundUrl(compositionEl)
|
||||
|
||||
if (backgroundUrl) {
|
||||
const background = await loadImage(backgroundUrl)
|
||||
context.drawImage(background, 0, 0, EXPORT_WIDTH, EXPORT_HEIGHT)
|
||||
}
|
||||
|
||||
const layers = Array.from(compositionEl.querySelectorAll<HTMLImageElement>('img'))
|
||||
.filter((image) => image.complete && image.naturalWidth > 0)
|
||||
.map((image) => getImageLayer(image, compositionEl, rootRect))
|
||||
.sort((a, b) => a.zIndex - b.zIndex)
|
||||
|
||||
for (const layer of layers) {
|
||||
await drawLayer(context, layer, scaleX, scaleY)
|
||||
}
|
||||
|
||||
return canvas.toDataURL('image/jpeg', 0.82)
|
||||
}
|
||||
@@ -209,7 +209,7 @@ const ZhanglepingDraw1 = () => {
|
||||
{/* 底部输入框 */}
|
||||
{showCaptionInput && (
|
||||
<div
|
||||
className="absolute left-20 bottom-2.5 bg-cover bg-center bg-no-repeat w-[1724px] h-[100px] z-40"
|
||||
className="absolute left-20 bottom-0.5 bg-cover bg-center bg-no-repeat w-[1724px] h-[100px] z-40"
|
||||
style={{ backgroundImage: getBackgroundImage(`${STAR_NAME}/reflection/draw/写字底板`) }}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
@@ -239,6 +239,7 @@ const ZhanglepingDraw1 = () => {
|
||||
<Image
|
||||
src={getStaticImage(`${STAR_NAME}/reflection/draw/返回`)}
|
||||
button
|
||||
className="block w-[371px] h-[105px]"
|
||||
onClick={() => navigate(`/${moduleName}/${STAR_NAME}/draw`)}
|
||||
/>
|
||||
</div>
|
||||
@@ -246,7 +247,10 @@ const ZhanglepingDraw1 = () => {
|
||||
<Image
|
||||
src={getStaticImage(`${STAR_NAME}/reflection/draw/提交作品`)}
|
||||
button
|
||||
className={isSubmitting ? 'opacity-70 pointer-events-none' : undefined}
|
||||
className={cn(
|
||||
'block w-[371px] h-[105px]',
|
||||
isSubmitting && 'opacity-70 pointer-events-none'
|
||||
)}
|
||||
onClick={handleSubmitArtwork}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -431,6 +431,9 @@ const ZhanglepingInternalization = () => {
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
controlsOnReplay
|
||||
skipKey="zhangleping_internalization_video"
|
||||
showReadonlyProgress
|
||||
onSkip={() => {
|
||||
setShowVideo(false)
|
||||
setPostGuideVisible(true)
|
||||
|
||||
@@ -286,6 +286,8 @@ const ZhanglepingPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="zhangleping_place2_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo1(false)
|
||||
setQuizVisible1(true)
|
||||
@@ -363,6 +365,8 @@ const ZhanglepingPlace2 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="zhangleping_place2_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo2(false)
|
||||
markExhibitVisited(place, 2)
|
||||
|
||||
@@ -277,6 +277,8 @@ const ZhanglepingPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="zhangleping_place3_video1"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo2(false)
|
||||
setQuiz2Visible(true)
|
||||
@@ -340,6 +342,8 @@ const ZhanglepingPlace3 = () => {
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
skipKey="zhangleping_place3_video2"
|
||||
showReadonlyProgress
|
||||
onEnded={() => {
|
||||
setShowVideo3(false)
|
||||
}}
|
||||
|
||||
@@ -296,7 +296,7 @@ const ZhanglepingReflectionScene1 = () => {
|
||||
{fragmentCompleted && !showFragmentOverlay && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage('components/place/离开场景')}
|
||||
src={getStaticImage('components/place/继续前进')}
|
||||
button
|
||||
onClick={handleLeave}
|
||||
/>
|
||||
|
||||
@@ -473,7 +473,7 @@ const ZhanglepingReflectionScene2 = () => {
|
||||
{fragmentCompleted && !showRepairOverlay && !showFragmentOverlay && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage('components/place/离开场景')}
|
||||
src={getStaticImage('components/place/继续前进')}
|
||||
button
|
||||
onClick={handleLeave}
|
||||
/>
|
||||
|
||||
@@ -211,7 +211,7 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
{/* 场景提示:碎片已解锁 */}
|
||||
{!showItemExhibit && !showFragmentOverlay && fragmentCompleted && (
|
||||
<SceneHint when triggerKey={successHintKey} persistent offset={{ x: -20 }}>
|
||||
苦难善良·记忆碎片已解锁
|
||||
流浪岁月·记忆碎片已解锁
|
||||
</SceneHint>
|
||||
)}
|
||||
{/* 场景提示:拖拽阶段常驻/错误提示 */}
|
||||
@@ -336,7 +336,7 @@ const ZhanglepingReflectionScene3 = () => {
|
||||
{fragmentCompleted && !showItemExhibit && !showFragmentOverlay && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage('components/place/离开场景')}
|
||||
src={getStaticImage('components/place/继续前进')}
|
||||
button
|
||||
onClick={handleLeave}
|
||||
/>
|
||||
|
||||
@@ -344,7 +344,7 @@ const ZhanglepingReflectionScene4 = () => {
|
||||
{fragmentCompleted && !showItemExhibit && !showFragmentOverlay && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage('components/place/离开场景')}
|
||||
src={getStaticImage('components/place/继续前进')}
|
||||
button
|
||||
onClick={handleLeave}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user