import { useState, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' 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' /** * 将英文故居名称转换为中文显示名称 */ const getResidenceDisplayName = (residence: string): string => { const nameMap: Record = { chenyun: '陈云', huangyanpei: '黄炎培', luxun: '鲁迅', sunzhongshan: '孙中山', maozedong: '毛泽东', zhangleping: '张乐平', nieer: '聂耳', rongdesheng: '荣德生', } return nameMap[residence] || residence } export interface PuzzleSystemProps { /** 是否可见 */ visible: boolean /** 关闭回调 */ onClose: () => void /** 故居名称,如 'chenyun' */ residence: string /** 是否显示菜单入口的故居导航 */ showResidenceNav?: boolean /** 切换故居 */ onResidenceChange?: (residence: string) => void } /** * SVG拼图碎片组件 */ const PuzzlePieceSvg = ({ residence, pieceId, isUnlocked, isSelected, isHovered, onClick, onHover, onLeave, }: { residence: string pieceId: number isUnlocked: boolean isSelected: boolean isHovered: boolean onClick: () => void onHover: (id: number) => void onLeave: () => void }) => { const pathData = getPuzzlePiecePath(residence, pieceId) if (!pathData) return null // 为每个碎片生成唯一的pattern ID const patternId = `pattern-${residence}-${pieceId}` // 碎片图片路径 const pieceImage = getStaticImage(`puzzle/${residence}/${pieceId}`) return ( <> (isUnlocked ? onHover(pieceId) : undefined)} onMouseLeave={onLeave} style={{ cursor: isUnlocked ? 'pointer' : 'not-allowed', }} className={cn(isUnlocked ? 'puzzle-piece' : '')} > {/* Safari/iOS 对 SVG image 的 CSS filter 支持有限,使用滤镜定义保证生效 */} {/* 将亮度压低到约 20% */} {/* 实际的路径:只有点击到路径填充区域才会触发 onClick */} ) } /** * 拼图系统组件 * - 全屏弹窗,显示拼图碎片 * - 4x4网格布局(16块碎片) * - 已解锁显示正常图片,未解锁显示遮罩 * - 点击碎片可跳转到对应页面 */ const PuzzleSystem = ({ visible, onClose, residence, showResidenceNav = false, onResidenceChange, }: PuzzleSystemProps) => { const navigate = useNavigate() const { moduleName } = useParams<{ moduleName: string }>() const { isPuzzleUnlocked, getPuzzleCollectProgress } = usePuzzleStore() const [selectedPiece, setSelectedPiece] = useState(null) const [hoveredPiece, setHoveredPiece] = useState(null) // 当弹窗关闭时重置选中状态 useEffect(() => { if (!visible) { setSelectedPiece(null) setHoveredPiece(null) } }, [visible]) useEffect(() => { setSelectedPiece(null) setHoveredPiece(null) }, [residence]) if (!visible) return null // 背景图片 const backgroundImage = getStaticImage('puzzle/底图') // 处理碎片点击 const handlePieceClick = (pieceId: number) => { const isUnlocked = isPuzzleUnlocked(residence, pieceId) if (isUnlocked) { // 显示选中效果,更新详情框 setSelectedPiece(pieceId) } else { // 未解锁的碎片,可以显示提示或不做任何操作 // 暂时不做处理,或者可以添加提示 } } // 处理前往按钮点击 const handleGoToPage = () => { if (activeModuleName && selectedPiece) { // console.log(residence, selectedPiece, moduleName); // const route = getPuzzleRoute(residence, selectedPiece, moduleName); // console.log(route); const route = selectedPieceData?.route if (route) { navigate(`/${activeModuleName}/${residence}/${route}`) onClose() } } } // 获取当前选中碎片的数据 const selectedPieceData = selectedPiece ? getPuzzlePieceData(residence, selectedPiece) : null // 生成16块碎片(1-16) const pieces = Array.from({ length: 16 }, (_, i) => i + 1) // 将悬停的碎片放到最后渲染,保证 hover 层级在最上 const renderPieces = hoveredPiece != null ? [...pieces.filter((p) => p !== hoveredPiece), hoveredPiece] : pieces // 获取收集进度 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 (
{ // 点击背景关闭 if (e.target === e.currentTarget) { onClose() } }} > {/* 背景图片 */}
{showResidenceNav && (
{residenceNavItems.map((item) => { const isActive = item.residence === residence const progress = Math.round(item.progress) return ( ) })}
)} {/* 关闭按钮 */} 关闭 {/* 收集进度 */}
{getResidenceDisplayName(residence)}故居碎片 碎片装饰
{/* 收集进度 */} {collectProgress.toFixed(2) == '1.00' ? '已集齐' : `收集进度${collectProgress.toFixed(2)}%`}
{/* 完整拼图底图 - 作为背景显示所有碎片拼好的样子 */}
{/* SVG拼图容器 - 使用3D变换和缩放 */}
{renderPieces.map((pieceId) => { const isUnlocked = isPuzzleUnlocked(residence, pieceId) return ( handlePieceClick(pieceId)} onHover={(id) => setHoveredPiece(id)} onLeave={() => setHoveredPiece(null)} /> ) })}
{selectedPieceData && (
{/* 标题 */} {selectedPieceData && (
{/* 空格换行 */} {selectedPieceData.title.split(' ').map((word: string, index: number) => (
{word} {index < selectedPieceData.title.split(' ').length - 1 && ' '}
))}
)} {/* 对应页面图片 */}
{selectedPieceData.title}
{/* 前往按钮 */} {selectedPieceData && ( 前往按钮 )}
)}
) } export default PuzzleSystem