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:
1 parent
e0fa47d21a
commit
f5f36f75b5
153 files changed
+3846
-2352
No files matched your search
@@ -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="导图"
|
||||
|
||||
@@ -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变换和缩放 */}
|
||||
|
||||
Reference in new issue
Block a user