253 lines
9.1 KiB
TypeScript
253 lines
9.1 KiB
TypeScript
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;
|