feat: add puzzle residence configuration and reflection progress logic

- Introduced `puzzleResidences.ts` to define puzzle residence configurations including properties like display name, position, and puzzle availability.
- Added `reflectionProgress.ts` to manage badge unlocking logic based on place completion, including functions to determine unlocked badges and video sources for scenes.
This commit is contained in:
zhongyidian committed 2026-04-27 11:49:30 +08:00
1 parent e0fa47d21a
commit f5f36f75b5
153 files changed
+3846 -2352

No files matched your search

+22 -116
View File
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
import { usePuzzleStore } from '@/store/puzzleStore'
import { Image } from '@/components'
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
/**
* 六边形进度条组件
@@ -104,119 +105,22 @@ export interface MenuPuzzleCenterProps {
onClose: () => void
/** 选择某个故居后的回调(进入对应的碎片系统) */
onSelectResidence: (residence: string) => void
/** 是否允许从管理中心进入具体拼图 */
allowPuzzleEntry?: boolean
}
interface ResidenceConfig {
/** 拼图存储用的英文 key(如 chenyun) */
residence: string
/** 显示名称 */
displayName: string
/** 在大地图上的水平位置(百分比字符串,如 "20%") */
left: string
/** 在大地图上的垂直位置(百分比字符串,如 "40%") */
top: string
/** 显示用图片路径(相对于 puzzle/home/ 目录) */
image: string
/** 是否已经开放拼图(未开放时只展示,不可点击) */
hasPuzzle: boolean
/** 是否有导图资源 */
hasMap?: boolean
/** 导图资源文件名(位于 puzzle/home/map 下) */
mapImage?: string
}
/**
* 顶层碎片管理中心中展示的故居列表(8 个故居)
* left / top 为大致位置,后续可以根据视觉效果微调
* image 资源都位于 puzzle/home 目录下,命名可按实际资源调整
*/
const RESIDENCE_LIST: ResidenceConfig[] = [
{
residence: 'maozedong',
displayName: '毛泽东旧居',
left: '30%',
top: '35%',
image: '毛泽东',
hasPuzzle: true,
hasMap: true,
mapImage: '毛泽东导图',
},
{
residence: 'sunzhongshan',
displayName: '孙中山故居',
left: '12%',
top: '51%',
image: '孙中山',
hasPuzzle: true,
hasMap: true,
mapImage: '孙中山导图',
},
{
residence: 'luxun',
displayName: '鲁迅故居',
left: '26.3%',
top: '74%',
image: '鲁迅',
hasPuzzle: true,
hasMap: true,
mapImage: '鲁迅导图',
},
{
residence: 'huangyanpei',
displayName: '黄炎培故居',
left: '44%',
top: '67%',
image: '黄炎培',
hasPuzzle: true,
hasMap: true,
mapImage: '黄炎培导图',
},
{
residence: 'nieer',
displayName: '聂耳旧居',
left: '62%',
top: '60%',
image: '聂耳',
hasPuzzle: true,
hasMap: false,
},
{
residence: 'zhangleping',
displayName: '张乐平故居',
left: '73%',
top: '33%',
image: '张乐平',
hasPuzzle: true,
hasMap: false,
},
{
residence: 'rongdesheng',
displayName: '荣德生旧居',
left: '90%',
top: '45%',
image: '荣德生',
hasPuzzle: true,
hasMap: false,
},
{
residence: 'chenyun',
displayName: '陈云故居',
left: '80%',
top: '76%',
image: '陈云',
hasPuzzle: true,
hasMap: true,
mapImage: '陈云导图',
},
]
const MenuPuzzleCenter = ({ visible, onClose, onSelectResidence }: MenuPuzzleCenterProps) => {
const MenuPuzzleCenter = ({
visible,
onClose,
onSelectResidence,
allowPuzzleEntry = true,
}: MenuPuzzleCenterProps) => {
const { getPuzzleCollectProgress } = usePuzzleStore()
const [activeMapImage, setActiveMapImage] = useState<string | null>(null)
const residenceProgress = useMemo(
() =>
RESIDENCE_LIST.map((item) => ({
PUZZLE_RESIDENCES.map((item) => ({
...item,
// 只有已开放拼图的故居才计算收集进度
progress: item.hasPuzzle ? getPuzzleCollectProgress(item.residence) : 0,
@@ -287,16 +191,18 @@ const MenuPuzzleCenter = ({ visible, onClose, onSelectResidence }: MenuPuzzleCen
{/* 图标 */}
<div className="flex justify-center gap-6">
<Image
src={getStaticImage(`puzzle/home/拼图`)}
alt="拼图"
button
onClick={(e) => {
if (!hasPuzzle) return
e.stopPropagation()
onSelectResidence(residence)
}}
/>
{allowPuzzleEntry && (
<Image
src={getStaticImage(`puzzle/home/拼图`)}
alt="拼图"
button
onClick={(e) => {
if (!hasPuzzle) return
e.stopPropagation()
onSelectResidence(residence)
}}
/>
)}
<Image
src={getStaticImage(`puzzle/home/导图`)}
alt="导图"
+63 -4
View File
@@ -4,6 +4,7 @@ import { cn, getBackgroundImage, getStaticImage } from '@/lib/utils'
import { usePuzzleStore } from '@/store/puzzleStore'
import { getPuzzlePieceData } from '@/config/puzzleData'
import { getPuzzlePiecePath, getPuzzleViewBox } from '@/config/puzzlePaths'
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
import { Image } from '@/components'
/**
@@ -16,6 +17,9 @@ const getResidenceDisplayName = (residence: string): string => {
luxun: '鲁迅',
sunzhongshan: '孙中山',
maozedong: '毛泽东',
zhangleping: '张乐平',
nieer: '聂耳',
rongdesheng: '荣德生',
}
return nameMap[residence] || residence
@@ -28,6 +32,10 @@ export interface PuzzleSystemProps {
onClose: () => void
/** 故居名称,如 'chenyun' */
residence: string
/** 是否显示菜单入口的故居导航 */
showResidenceNav?: boolean
/** 切换故居 */
onResidenceChange?: (residence: string) => void
}
/**
@@ -143,7 +151,13 @@ const PuzzlePieceSvg = ({
* - 已解锁显示正常图片,未解锁显示遮罩
* - 点击碎片可跳转到对应页面
*/
const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
const PuzzleSystem = ({
visible,
onClose,
residence,
showResidenceNav = false,
onResidenceChange,
}: PuzzleSystemProps) => {
const navigate = useNavigate()
const { moduleName } = useParams<{ moduleName: string }>()
const { isPuzzleUnlocked, getPuzzleCollectProgress } = usePuzzleStore()
@@ -154,9 +168,15 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
useEffect(() => {
if (!visible) {
setSelectedPiece(null)
setHoveredPiece(null)
}
}, [visible])
useEffect(() => {
setSelectedPiece(null)
setHoveredPiece(null)
}, [residence])
if (!visible) return null
// 背景图片
@@ -177,13 +197,13 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
// 处理前往按钮点击
const handleGoToPage = () => {
if (moduleName && selectedPiece) {
if (activeModuleName && selectedPiece) {
// console.log(residence, selectedPiece, moduleName);
// const route = getPuzzleRoute(residence, selectedPiece, moduleName);
// console.log(route);
const route = selectedPieceData?.route
if (route) {
navigate(`/${moduleName}/${residence}/${route}`)
navigate(`/${activeModuleName}/${residence}/${route}`)
onClose()
}
}
@@ -200,6 +220,12 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
// 获取收集进度
const collectProgress = getPuzzleCollectProgress(residence)
const activeResidenceConfig = PUZZLE_RESIDENCES.find((item) => item.residence === residence)
const activeModuleName = moduleName || activeResidenceConfig?.moduleName
const residenceNavItems = PUZZLE_RESIDENCES.filter((item) => item.hasPuzzle).map((item) => ({
...item,
progress: getPuzzleCollectProgress(item.residence),
}))
return (
<div
@@ -221,6 +247,37 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
backgroundImage: `url(${backgroundImage})`,
}}
>
{showResidenceNav && (
<div className="absolute left-0 top-0 z-10 h-full w-[360px] pt-10 flex flex-col gap-3 pointer-events-auto">
{residenceNavItems.map((item) => {
const isActive = item.residence === residence
const progress = Math.round(item.progress)
return (
<button
key={item.residence}
type="button"
className={cn(
'relative w-[341px] h-[112px] bg-contain bg-center bg-no-repeat text-left transition-all hover:brightness-110',
isActive ? 'translate-x-3 scale-105' : ''
)}
style={{
backgroundImage: `url(${getStaticImage(
isActive ? 'puzzle/选中' : 'puzzle/未选中'
)})`,
}}
onClick={() => onResidenceChange?.(item.residence)}
>
<div className="relative h-full pl-14 pr-5 flex items-center justify-between text-white font-bold">
<span className="text-2xl drop-shadow">{item.displayName}</span>
<span className="text-xl drop-shadow">{progress}%</span>
</div>
</button>
)
})}
</div>
)}
{/* 关闭按钮 */}
<Image
className="absolute right-8 top-8 z-10"
@@ -259,7 +316,9 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
</div>
{/* 完整拼图底图 - 作为背景显示所有碎片拼好的样子 */}
<div className="absolute inset-0 flex justify-end">
<div
className={cn('absolute inset-0 flex justify-end', showResidenceNav ? 'pl-[360px]' : '')}
>
<div className="flex items-center gap-4 px-6 pt-30">
<div className="w-200 h-200 perspective-[1000px]">
{/* SVG拼图容器 - 使用3D变换和缩放 */}