Files
shanghai-residences/src/pages/nieer/reflection/index.tsx
T
2026-04-29 12:55:02 +08:00

259 lines
9.6 KiB
TypeScript

import { Image, StudyGuideModal } from '@/components'
import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
import { useMemo } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { LOBBY_COLUMNS, LOBBY_LAYOUT, LOBBY_TIMELINE } from './lobby.config'
import { useNieerStore } from '@/store/nieerStore'
import { useDialogReducer } from './useDialogReducer'
import DialogLayer from './DialogLayer'
import {
BADGE_CONFIG,
getAllPlacesCompleted,
getBadgeUnlockedMap,
getGuideSceneId,
getPlaceCompletionMap,
getUnlockVideoSrcForScene,
getUnlockedByIdMap,
} from './reflectionProgress'
const starName = 'nieer'
const NieerReflection = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{ moduleName: string }>()
const debugLobby = new URLSearchParams(window.location.search).get('debugLobby') === '1'
const visitedReflectionCursors = useNieerStore((state) => state.visitedReflectionCursors)
const markReflectionCursorVisited = useNieerStore((state) => state.markReflectionCursorVisited)
const setReflectionWork = useNieerStore((state) => state.setReflectionWork)
const hasSeenReflectionGuide = useNieerStore((state) => state.hasSeenReflectionGuide)
const markReflectionGuideSeen = useNieerStore((state) => state.markReflectionGuideSeen)
const showGuideModal = !hasSeenReflectionGuide
const {
state: dialogState,
openScene,
enterUnlock,
handleShare,
finishBadgeVideo,
close,
} = useDialogReducer(markReflectionCursorVisited, setReflectionWork)
const placeCompletion = useMemo(
() => getPlaceCompletionMap(visitedReflectionCursors),
[visitedReflectionCursors]
)
const unlockedById = useMemo(() => getUnlockedByIdMap(placeCompletion), [placeCompletion])
const badgeUnlocked = useMemo(() => getBadgeUnlockedMap(placeCompletion), [placeCompletion])
const guideSceneId = useMemo(() => getGuideSceneId(placeCompletion), [placeCompletion])
const allPlacesCompleted = useMemo(
() => getAllPlacesCompleted(placeCompletion),
[placeCompletion]
)
const handleResultShare = (sceneId: number, payload?: Record<string, unknown>) => {
handleShare({
payload,
unlockVideoSrc: getUnlockVideoSrcForScene(sceneId, visitedReflectionCursors),
})
}
if (!moduleName) return null
return (
<div
className="w-full h-full relative bg-cover bg-center bg-no-repeat overflow-hidden"
style={{ backgroundImage: getBackgroundImage(`${starName}/reflection/lobby/场景首页底图`) }}
>
<Image
className="absolute right-6 top-5 z-30"
src={getStaticImage('components/返回')}
button
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
/>
<div
className={cn(
'absolute h-full grid z-1',
LOBBY_LAYOUT.columnsTop,
LOBBY_LAYOUT.columnsLeft
)}
style={{
width: `${LOBBY_LAYOUT.columnsWidthPx}px`,
gridTemplateColumns: `repeat(${LOBBY_LAYOUT.columnCount}, minmax(0, 1fr))`,
}}
>
<img
className="absolute -left-65 top-0 z-2 w-[1651px] h-auto pointer-events-none"
src={getStaticImage(`${starName}/reflection/lobby/menu/琴弦`)}
alt="琴弦"
/>
{LOBBY_COLUMNS.map((scene) => {
const isUnlocked = unlockedById[scene.id as keyof typeof unlockedById]
const isCompleted = placeCompletion[scene.id as keyof typeof placeCompletion]
const iconSrc = isCompleted ? scene.icon : scene.iconBw
const isClickable = isUnlocked || scene.id === guideSceneId
const showGuideState = !allPlacesCompleted && scene.id === guideSceneId
return (
<button
key={scene.id}
type="button"
className={cn(
'relative w-[220px] h-full flex flex-col items-center',
!isClickable && 'pointer-events-none cursor-not-allowed',
showGuideState && 'border-l-6 border-r-6 border-white'
)}
onClick={() => {
if (!isClickable) return
openScene(scene.id, !!isUnlocked)
}}
>
<Image
className={cn('absolute inset-0 -z-1', !isUnlocked && 'brightness-50')}
src={getStaticImage(`${starName}/reflection/lobby/${scene.cityImage}`)}
alt={scene.city}
/>
<div
className={cn(
'relative z-3',
'w-[233px] h-[233px] flex items-center justify-center -mt-13'
)}
>
<div className="relative w-[112px] h-[111px]">
{showGuideState && (
<span
className={cn(
' absolute -z-3 left-0 inline-flex h-full w-full rounded-full',
'bg-[#fcc419] opacity-100 animate-ping',
'border-2 border-white drop-shadow-2xl'
)}
/>
)}
<img
className="w-full"
src={getStaticImage(`${starName}/reflection/lobby/menu/${iconSrc}`)}
alt={scene.icon}
/>
</div>
</div>
{showGuideState && (
<Image
className="animate-finger-bounce absolute bottom-[calc(50%-200px)] translate-y-1/2"
src={getStaticImage(`${starName}/reflection/lobby/icon/点击前往`)}
alt="点击前往"
/>
)}
</button>
)
})}
</div>
<div
className={cn('absolute z-10 grid', LOBBY_LAYOUT.timelineTop, LOBBY_LAYOUT.columnsLeft)}
style={{
width: `${LOBBY_LAYOUT.columnsWidthPx}px`,
gridTemplateColumns: `repeat(${LOBBY_LAYOUT.columnCount}, minmax(0, 1fr))`,
}}
>
{LOBBY_TIMELINE.map((item) => {
if (item.type === 'single') {
return (
<div
key={`${item.type}-${item.col}-${item.label}`}
className="text-center text-4xl font-semibold text-white [text-shadow:0_0_20px_rgb(247_214_97_/0.8),0_0_20px_rgb(247_214_97/0.8)]"
style={{ gridColumn: `${item.col + 1} / span 1` }}
>
{item.label}
</div>
)
}
return (
<div
key={`${item.type}-${item.colStart}-${item.colEnd}-${item.label}`}
className="flex items-center justify-center gap-3 text-4xl font-semibold text-white [text-shadow:0_0_20px_rgb(247_214_97_/0.8),0_0_20px_rgb(247_214_97/0.8)]"
style={{ gridColumn: `${item.colStart + 1} / ${item.colEnd + 2}` }}
>
{item.withLine && <span className="h-[2px] w-24 bg-[#f5e9c7]/80" />}
<span>{item.label}</span>
{item.withLine && <span className="h-[2px] w-24 bg-[#f5e9c7]/80" />}
</div>
)
})}
</div>
<div
className="absolute left-0 bottom-0 w-full h-[201px] z-20 flex justify-center items-center pt-20 gap-10"
style={{
backgroundImage: getBackgroundImage(`${starName}/reflection/lobby/footer`),
}}
>
<div className="relative w-340 h-full">
{BADGE_CONFIG.map((badge) => {
const isLit = badgeUnlocked[badge.id as keyof typeof badgeUnlocked]
return (
<div key={badge.id} className={cn('absolute w-[193px]', badge.className)}>
<Image
width={193}
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章底板`)}
alt="徽章底板"
/>
{isLit && (
<Image
className="absolute inset-0"
width={193}
src={getStaticImage(`${starName}/reflection/lobby/icon/徽章${badge.id}`)}
alt={`徽章${badge.id}`}
/>
)}
</div>
)
})}
</div>
</div>
<Image
button
src={getStaticImage(`${starName}/reflection/lobby/icon/旅行箱`)}
className="absolute right-0 bottom-0 z-30"
alt="旅行箱"
onClick={() => navigate(`/${moduleName}/${starName}/reflection/box`)}
/>
{debugLobby && (
<div
className="absolute inset-0 z-40 pointer-events-none"
style={{
background:
'linear-gradient(to right, rgba(255,0,0,0.25) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,255,255,0.18) 1px, transparent 1px)',
backgroundSize: '120px 100%, 100% 80px',
}}
/>
)}
<DialogLayer
state={dialogState}
onEnterUnlock={enterUnlock}
onShare={handleResultShare}
onFinishBadgeVideo={finishBadgeVideo}
onClose={close}
/>
<StudyGuideModal
visible={showGuideModal}
onContinue={markReflectionGuideSeen}
audioSrc={getPublicPath('audio/nieer/mp3_nieer_28.mp3')}
contentPaddingLeft="2em"
// firstLineIndent="2em"
>
{`同学你好,欢迎来到“跨越时空·体悟”学习模块!
请登陆“时空回溯・亲历时代”体验仓,下一站:1936年,上海。
请带上你的旅行包,穿越时空解锁6个国歌故事,集齐精神徽章,绘制属于你的国歌精神图谱吧~时空门已开,请出发吧!`}
</StudyGuideModal>
</div>
)
}
export default NieerReflection