feat: 添加全局键盘快捷键和更新故居数据,提升用户体验
- 在 main.tsx 中添加全局键盘快捷键功能,允许用户通过 Ctrl+Shift+R 重置所有缓存。 - 更新多个故居的拼图数据,增加黄炎培和鲁迅的相关信息,丰富学习内容。 - 调整 ResidenceLayout 组件以支持新的故居,确保数据一致性。 - 清理不再使用的拼图布局配置文件,提升代码整洁性。
This commit is contained in:
1 parent
8ba36a4a1a
commit
a365964f54
155 files changed
+1387
-348
No files matched your search
@@ -192,6 +192,7 @@ const GuideSteps = ({
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
onClick={handleTextClick}
|
||||
>
|
||||
{/* 引导板容器和背景 - 作为整体切换,确保 padding 和背景同步 */}
|
||||
{/* 只在 isVisible 为 true 时才渲染,避免阻塞 exit 动画 */}
|
||||
|
||||
@@ -15,7 +15,7 @@ import PuzzlePiecePortal from "@/components/puzzle/PuzzlePiecePortal";
|
||||
* 支持拼图盒功能的故居列表
|
||||
* 后续如需为其他故居添加拼图盒,只需在此数组中添加对应的故居名称即可
|
||||
*/
|
||||
const RESIDENCES_WITH_PUZZLE = ["chenyun"] as const;
|
||||
const RESIDENCES_WITH_PUZZLE = ["chenyun", "huangyanpei","sunzhongshan","luxun"] as const;
|
||||
|
||||
/**
|
||||
* 故居布局组件
|
||||
|
||||
@@ -2,10 +2,24 @@ 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 { getPuzzlePiecePath, getPuzzleViewBox } from "@/config/puzzlePaths";
|
||||
import { Image } from "@/components";
|
||||
|
||||
/**
|
||||
* 将英文故居名称转换为中文显示名称
|
||||
*/
|
||||
const getResidenceDisplayName = (residence: string): string => {
|
||||
const nameMap: Record<string, string> = {
|
||||
chenyun: "陈云",
|
||||
huangyanpei: "黄炎培",
|
||||
luxun: "鲁迅",
|
||||
sunzhongshan: "孙中山",
|
||||
};
|
||||
|
||||
return nameMap[residence] || residence;
|
||||
};
|
||||
|
||||
export interface PuzzleSystemProps {
|
||||
/** 是否可见 */
|
||||
visible: boolean;
|
||||
@@ -15,6 +29,96 @@ export interface PuzzleSystemProps {
|
||||
residence: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.puzzle-piece {
|
||||
transition: transform 0.2s ease-out;
|
||||
transform-origin: center;
|
||||
transform-box: fill-box; /* 确保 SVG 围绕自身中心缩放 */
|
||||
}
|
||||
|
||||
.puzzle-piece:hover {
|
||||
transform: scale(1.04) translate(-5px, -5px);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<g
|
||||
onClick={isUnlocked ? onClick : undefined}
|
||||
onMouseEnter={() => (isUnlocked ? onHover(pieceId) : undefined)}
|
||||
onMouseLeave={onLeave}
|
||||
style={{
|
||||
cursor: isUnlocked ? "pointer" : "not-allowed",
|
||||
}}
|
||||
className={cn(isUnlocked ? "puzzle-piece" : "")}
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={patternId}
|
||||
patternUnits="objectBoundingBox"
|
||||
patternContentUnits="objectBoundingBox"
|
||||
width={1}
|
||||
height={1}
|
||||
>
|
||||
<image
|
||||
href={pieceImage}
|
||||
width={1}
|
||||
height={1}
|
||||
preserveAspectRatio="none"
|
||||
style={{
|
||||
filter: isUnlocked ? "none" : "brightness(0.2)",
|
||||
transition: "filter 220ms ease",
|
||||
}}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
{/* 实际的路径:只有点击到路径填充区域才会触发 onClick */}
|
||||
<path
|
||||
d={pathData}
|
||||
fill={`url(#${patternId})`}
|
||||
stroke={isUnlocked ? (isSelected ? "#75d1fb" : "#fff") : "#888"}
|
||||
strokeWidth={isUnlocked ? (isSelected ? 3 : isHovered ? 2.3 : 2) : 1}
|
||||
opacity={isUnlocked ? 1 : 0.8}
|
||||
/>
|
||||
</g>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 拼图系统组件
|
||||
* - 全屏弹窗,显示拼图碎片
|
||||
@@ -27,6 +131,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
const { moduleName } = useParams<{ moduleName: string }>();
|
||||
const { isPuzzleUnlocked, getPuzzleCollectProgress } = usePuzzleStore();
|
||||
const [selectedPiece, setSelectedPiece] = useState<number | null>(null);
|
||||
const [hoveredPiece, setHoveredPiece] = useState<number | null>(null);
|
||||
|
||||
// 当弹窗关闭时重置选中状态
|
||||
useEffect(() => {
|
||||
@@ -74,6 +179,11 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
|
||||
// 生成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);
|
||||
@@ -119,7 +229,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
"[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)]"
|
||||
)}
|
||||
>
|
||||
陈云故居碎片
|
||||
{getResidenceDisplayName(residence)}故居碎片
|
||||
</span>
|
||||
<img
|
||||
className="absolute -top-10 left-0 -z-10"
|
||||
@@ -137,58 +247,36 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
|
||||
{/* 完整拼图底图 - 作为背景显示所有碎片拼好的样子 */}
|
||||
<div className="absolute inset-0 flex justify-end">
|
||||
<div className="flex items-center gap-4 px-6 pt-40">
|
||||
<div className="flex items-center gap-4 px-6 pt-30">
|
||||
<div className="w-200 h-200 perspective-[1000px]">
|
||||
{/* 拼图容器 - 使用绝对定位,根据配置放置每个碎片 */}
|
||||
{/* SVG拼图容器 - 使用3D变换和缩放 */}
|
||||
<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);
|
||||
<svg
|
||||
viewBox={getPuzzleViewBox(residence)}
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
filter: "drop-shadow(0px 2px 4px rgba(0,0,0,0.3))",
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
{renderPieces.map((pieceId) => {
|
||||
const isUnlocked = isPuzzleUnlocked(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",
|
||||
}}
|
||||
return (
|
||||
<PuzzlePieceSvg
|
||||
key={pieceId}
|
||||
residence={residence}
|
||||
pieceId={pieceId}
|
||||
isUnlocked={isUnlocked}
|
||||
isSelected={selectedPiece === pieceId}
|
||||
isHovered={hoveredPiece === pieceId}
|
||||
onClick={() => handlePieceClick(pieceId)}
|
||||
onHover={(id) => setHoveredPiece(id)}
|
||||
onLeave={() => setHoveredPiece(null)}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-2xl h-200">
|
||||
@@ -203,7 +291,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
{selectedPieceData && (
|
||||
<div
|
||||
className={cn(
|
||||
"text-white text-5xl font-bold whitespace-nowrap mt-6 h-20 space-y-4",
|
||||
"text-white text-5xl font-bold whitespace-nowrap mt-8 mb-4 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)]"
|
||||
)}
|
||||
>
|
||||
@@ -233,6 +321,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
{/* 前往按钮 */}
|
||||
{selectedPieceData && (
|
||||
<Image
|
||||
className="mt-4"
|
||||
src={getStaticImage("puzzle/点击前往浏览")}
|
||||
alt="前往按钮"
|
||||
button
|
||||
|
||||
Reference in new issue
Block a user