更新依赖项及历史人物相关内容资源

- 更新 package.json 和 package-lock.json 中的依赖项
- 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件
- 新增场景和组件的图片资源
- 清理未使用的音频和图片文件
- 优化多个组件和页面的实现逻辑
This commit is contained in:
zhongyidian committed 2026-04-20 13:27:18 +08:00
1 parent b1d55b9464
commit e0fa47d21a
629 files changed
+24390 -2991

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
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