Files
shanghai-residences/src/components/puzzle/PuzzleSystem.tsx
T
2026-01-11 18:18:20 +08:00

253 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { getPuzzlePieceLayout } from "@/config/puzzleLayout";
import { getPuzzlePieceData } from "@/config/puzzleData";
import { Image } from "@/components";
export interface PuzzleSystemProps {
/** 是否可见 */
visible: boolean;
/** 关闭回调 */
onClose: () => void;
/** 故居名称,如 'chenyun' */
residence: string;
}
/**
* 拼图系统组件
* - 全屏弹窗,显示拼图碎片
* - 4x4网格布局(16块碎片)
* - 已解锁显示正常图片,未解锁显示遮罩
* - 点击碎片可跳转到对应页面
*/
const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
const navigate = useNavigate();
const { moduleName } = useParams<{ moduleName: string }>();
const { isPuzzleUnlocked, getPuzzleCollectProgress } = usePuzzleStore();
const [selectedPiece, setSelectedPiece] = useState<number | null>(null);
// 当弹窗关闭时重置选中状态
useEffect(() => {
if (!visible) {
setSelectedPiece(null);
}
}, [visible]);
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 (moduleName && 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}`);
onClose();
}
}
};
// 获取当前选中碎片的数据
const selectedPieceData = selectedPiece
? getPuzzlePieceData(residence, selectedPiece)
: null;
// 生成16块碎片(1-16)
const pieces = Array.from({ length: 16 }, (_, i) => i + 1);
// 获取收集进度
const collectProgress = getPuzzleCollectProgress(residence);
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center"
style={{
background: "rgba(0, 0, 0, 0.8)",
}}
onClick={(e) => {
// 点击背景关闭
if (e.target === e.currentTarget) {
onClose();
}
}}
>
{/* 背景图片 */}
<div
className="relative w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${backgroundImage})`,
}}
>
{/* 关闭按钮 */}
<Image
className="absolute right-8 top-8 z-10"
src={getStaticImage("components/关闭")}
alt="关闭"
button
onClick={onClose}
/>
{/* 收集进度 */}
<div
className="absolute left-1/2 -translate-x-1/2 top-8 z-10 w-[708px] h-[107px] px-12 flex justify-between items-center"
style={{ backgroundImage: getBackgroundImage("puzzle/收集进度") }}
>
<div className="relative">
<span
className={cn(
"text-white text-2xl font-bold",
"[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]"
)}
>
陈云故居碎片
</span>
<img
className="absolute -top-10 left-0 -z-10"
src={getStaticImage("puzzle/碎片装饰")}
alt="碎片装饰"
/>
</div>
<div className="text-[#490000] text-4xl font-bold">
{/* 收集进度 */}
{collectProgress.toFixed(2) == "1.00"
? "已集齐"
: `收集进度${collectProgress.toFixed(2)}%`}
</div>
</div>
{/* 完整拼图底图 - 作为背景显示所有碎片拼好的样子 */}
<div className="absolute inset-0 flex justify-end">
<div className="flex items-center gap-4 px-6 pt-40">
<div className="w-200 h-200 perspective-[1000px]">
{/* 拼图容器 - 使用绝对定位,根据配置放置每个碎片 */}
<div className="relative h-full transform-3d rotate-x-20 -rotate-y-0 -rotate-z-0 scale-85">
{pieces.map((pieceId) => {
const isUnlocked = isPuzzleUnlocked(residence, pieceId);
const layout = getPuzzlePieceLayout(residence, pieceId);
// 碎片图片路径
const pieceImage = getStaticImage(
`puzzle/${residence}/${pieceId}`
);
return (
<div
key={pieceId}
className={`absolute transition-all ${
isUnlocked
? "cursor-pointer hover:scale-105 hover:z-99 z-10"
: "cursor-not-allowed z-0"
}`}
style={{
left: layout.position.x,
top: layout.position.y,
}}
onClick={() => handlePieceClick(pieceId)}
>
<div className="relative">
{/* 碎片图片 - 始终显示,未解锁的用CSS变黑 */}
<img
src={pieceImage}
alt={`碎片 ${pieceId}`}
style={{
filter: isUnlocked ? "" : `brightness(0.3) `,
}}
/>
{/* 选中效果遮罩 - 只对已解锁的碎片显示 */}
{/* {isSelected && isUnlocked && (
<div
className="absolute inset-0 pointer-events-none z-10"
style={{
backgroundImage: `url(${selectedMaskImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
)} */}
</div>
</div>
);
})}
</div>
</div>
<div className="w-2xl h-200">
{selectedPieceData && (
<div
className="w-[657px] h-[776px] bg-no-repeat bg-cover p-10 text-center"
style={{
backgroundImage: getBackgroundImage("puzzle/详情框"),
}}
>
{/* 标题 */}
{selectedPieceData && (
<div
className={cn(
"text-white text-5xl font-bold whitespace-nowrap mt-6 h-20 space-y-4",
"[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]"
)}
>
{/* 空格换行 */}
{selectedPieceData.title
.split(" ")
.map((word: string, index: number) => (
<div key={index}>
{word}
{index <
selectedPieceData.title.split(" ").length - 1 &&
" "}
</div>
))}
</div>
)}
{/* 对应页面图片 */}
<div className="max-h-[500px] min-h-[400px] flex items-center justify-center">
<img
src={getStaticImage(
`puzzle/${selectedPieceData.detailImage}`
)}
alt={selectedPieceData.title}
className="max-w-full max-h-full object-contain"
/>
</div>
{/* 前往按钮 */}
{selectedPieceData && (
<Image
src={getStaticImage("puzzle/点击前往浏览")}
alt="前往按钮"
button
onClick={handleGoToPage}
/>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default PuzzleSystem;