- 更新 package.json 和 package-lock.json 中的依赖项 - 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件 - 新增场景和组件的图片资源 - 清理未使用的音频和图片文件 - 优化多个组件和页面的实现逻辑
285 lines
10 KiB
TypeScript
285 lines
10 KiB
TypeScript
import { Image } from '@/components'
|
|
import { cn, getBackgroundImage, getStaticImage } from '@/lib/utils'
|
|
import { useMemo } from 'react'
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
import { LOBBY_COLUMNS, LOBBY_LAYOUT, LOBBY_TIMELINE, PLACE_REQUIRED_COUNTS } from './lobby.config'
|
|
import { useNieerStore } from '@/store/nieerStore'
|
|
import { useDialogReducer } from './useDialogReducer'
|
|
import DialogLayer from './DialogLayer'
|
|
|
|
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 {
|
|
state: dialogState,
|
|
openScene,
|
|
enterUnlock,
|
|
handleShare,
|
|
close,
|
|
} = useDialogReducer(markReflectionCursorVisited, setReflectionWork)
|
|
|
|
const placeCompletion = useMemo(() => {
|
|
const getVisitedCount = (placeName: string): number =>
|
|
visitedReflectionCursors[placeName]?.size ?? 0
|
|
return {
|
|
1: getVisitedCount('place1') >= (PLACE_REQUIRED_COUNTS[1] ?? 0),
|
|
2: getVisitedCount('place2') >= (PLACE_REQUIRED_COUNTS[2] ?? 0),
|
|
3: getVisitedCount('place3') >= (PLACE_REQUIRED_COUNTS[3] ?? 0),
|
|
4: getVisitedCount('place4') >= (PLACE_REQUIRED_COUNTS[4] ?? 0),
|
|
5: getVisitedCount('place5') >= (PLACE_REQUIRED_COUNTS[5] ?? 0),
|
|
6: getVisitedCount('place6') >= (PLACE_REQUIRED_COUNTS[6] ?? 0),
|
|
} as const
|
|
}, [visitedReflectionCursors])
|
|
|
|
const unlockedById = useMemo(
|
|
() =>
|
|
({
|
|
1: true,
|
|
2: placeCompletion[1],
|
|
3: placeCompletion[2],
|
|
4: placeCompletion[3],
|
|
5: placeCompletion[4],
|
|
6: placeCompletion[5],
|
|
}) as const,
|
|
[placeCompletion]
|
|
)
|
|
|
|
const badgeUnlocked = useMemo(
|
|
() => ({
|
|
1: placeCompletion[1] && placeCompletion[2],
|
|
2: placeCompletion[3] && placeCompletion[4],
|
|
3: placeCompletion[5] && placeCompletion[6],
|
|
}),
|
|
[placeCompletion]
|
|
)
|
|
|
|
const guideSceneId = useMemo(() => {
|
|
const nextScene = LOBBY_COLUMNS.find(
|
|
(scene) => !placeCompletion[scene.id as keyof typeof placeCompletion]
|
|
)
|
|
return nextScene?.id ?? 6
|
|
}, [placeCompletion])
|
|
const allPlacesCompleted = useMemo(
|
|
() => LOBBY_COLUMNS.every((scene) => placeCompletion[scene.id as keyof typeof placeCompletion]),
|
|
[placeCompletion]
|
|
)
|
|
|
|
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>
|
|
|
|
{/* 横跨年份层:只负责 1937 跨列展示 */}
|
|
<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>
|
|
|
|
{/* 底部 footer + 徽章区 */}
|
|
<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">
|
|
{[
|
|
{
|
|
id: 1,
|
|
className: 'top-0 left-36',
|
|
},
|
|
{
|
|
id: 2,
|
|
className: 'top-0 left-146',
|
|
},
|
|
{
|
|
id: 3,
|
|
className: 'top-0 right-36',
|
|
},
|
|
].map((i) => {
|
|
const isLit = badgeUnlocked[i.id as keyof typeof badgeUnlocked]
|
|
return (
|
|
<div key={i.id} className={cn('absolute w-[193px]', i.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/徽章${i.id}`)}
|
|
alt={`徽章${i.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`)}
|
|
/>
|
|
|
|
{/* debug 参考线 */}
|
|
{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={handleShare}
|
|
onClose={close}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default NieerReflection
|