feat: 更新 ESLint 和 Prettier 配置,增强代码质量管理
- 在 .gitignore 中添加 public/audio/* 以忽略音频文件。 - 更新 bun.lock,添加 eslint-config-prettier 和 eslint-plugin-prettier 依赖,确保 ESLint 与 Prettier 的兼容性。 - 修改 eslint.config.ts,整合 Prettier 规则,优化代码风格一致性。 - 在 package.json 中添加 lint:fix 和 format 脚本,简化代码格式化和修复流程。 - 更新 tailwind.config.ts 和其他组件文件,统一代码风格,提升可读性。
This commit is contained in:
1 parent
5d00adc1a5
commit
76c67b492e
216 files changed
+12454
-13976
No files matched your search
@@ -1,51 +1,50 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { usePuzzleStore } from "@/store/puzzleStore";
|
||||
import { Image } from "@/components";
|
||||
import { useMemo, useState } from 'react'
|
||||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||
import { Image } from '@/components'
|
||||
|
||||
/**
|
||||
* 六边形进度条组件
|
||||
* 根据进度值显示不同颜色:100%绿色,50%-99%橙色,0%-49%灰色
|
||||
*/
|
||||
interface HexagonProgressBarProps {
|
||||
progress: number; // 0-100
|
||||
progress: number // 0-100
|
||||
}
|
||||
|
||||
const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||||
const progressPercent = Math.min(100, Math.max(0, progress));
|
||||
const progressText = progressPercent.toFixed(2) === "100.00"
|
||||
? "进度100%"
|
||||
: `进度${Math.round(progressPercent)}%`;
|
||||
const progressPercent = Math.min(100, Math.max(0, progress))
|
||||
const progressText =
|
||||
progressPercent.toFixed(2) === '100.00' ? '进度100%' : `进度${Math.round(progressPercent)}%`
|
||||
|
||||
// 根据进度值确定填充颜色
|
||||
const getProgressColor = () => {
|
||||
if (progressPercent >= 100) {
|
||||
return "#90B86B"; // 浅橄榄绿色(100%完成)
|
||||
return '#90B86B' // 浅橄榄绿色(100%完成)
|
||||
} else if (progressPercent >= 50) {
|
||||
return "#EEB362"; // 橙色(50%-99%)
|
||||
return '#EEB362' // 橙色(50%-99%)
|
||||
} else {
|
||||
return "#E0E0E0"; // 浅灰色(0%-49%)
|
||||
return '#E0E0E0' // 浅灰色(0%-49%)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fillColor = getProgressColor();
|
||||
const fillColor = getProgressColor()
|
||||
// 六边形路径,细长的水平六边形,左右两端是尖角
|
||||
// 正确的六边形路径:从左顶点开始,顺时针绘制
|
||||
// 左顶点(0%, 50%) -> 左上角(3%, 0%) -> 右上角(97%, 0%) -> 右顶点(100%, 50%) -> 右下角(97%, 100%) -> 左下角(3%, 100%)
|
||||
// 外层路径(包含边框)
|
||||
const outerHexagonPath = "polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)";
|
||||
const outerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||||
// 内层路径(边框内部)
|
||||
const innerHexagonPath = "polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)";
|
||||
const borderWidth = 4; // 增加边框宽度从2px到3px
|
||||
const innerHexagonPath = 'polygon(0% 50%, 3% 0%, 97% 0%, 100% 50%, 97% 100%, 3% 100%)'
|
||||
const borderWidth = 4 // 增加边框宽度从2px到3px
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: "260px", height: "50px" }}>
|
||||
<div className="relative" style={{ width: '260px', height: '50px' }}>
|
||||
{/* 外层:白色边框(使用 clip-path 裁剪形状) */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
clipPath: outerHexagonPath,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
/>
|
||||
{/* 内容容器(边框内部区域) */}
|
||||
@@ -63,7 +62,7 @@ const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: "#757575",
|
||||
backgroundColor: '#757575',
|
||||
}}
|
||||
/>
|
||||
{/* 填充层(根据进度百分比从左侧填充) */}
|
||||
@@ -78,7 +77,7 @@ const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: fillColor,
|
||||
transition: "width 0.3s ease, background-color 0.3s ease",
|
||||
transition: 'width 0.3s ease, background-color 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -88,42 +87,42 @@ const HexagonProgressBar = ({ progress }: HexagonProgressBarProps) => {
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center text-white text-2xl font-bold pointer-events-none"
|
||||
style={{
|
||||
textShadow: "0 2px 4px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.8)",
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.8)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export interface MenuPuzzleCenterProps {
|
||||
/** 是否可见 */
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void;
|
||||
onClose: () => void
|
||||
/** 选择某个故居后的回调(进入对应的碎片系统) */
|
||||
onSelectResidence: (residence: string) => void;
|
||||
onSelectResidence: (residence: string) => void
|
||||
}
|
||||
|
||||
interface ResidenceConfig {
|
||||
/** 拼图存储用的英文 key(如 chenyun) */
|
||||
residence: string;
|
||||
residence: string
|
||||
/** 显示名称 */
|
||||
displayName: string;
|
||||
displayName: string
|
||||
/** 在大地图上的水平位置(百分比字符串,如 "20%") */
|
||||
left: string;
|
||||
left: string
|
||||
/** 在大地图上的垂直位置(百分比字符串,如 "40%") */
|
||||
top: string;
|
||||
top: string
|
||||
/** 显示用图片路径(相对于 puzzle/home/ 目录) */
|
||||
image: string;
|
||||
image: string
|
||||
/** 是否已经开放拼图(未开放时只展示,不可点击) */
|
||||
hasPuzzle: boolean;
|
||||
hasPuzzle: boolean
|
||||
/** 是否有导图资源 */
|
||||
hasMap?: boolean;
|
||||
hasMap?: boolean
|
||||
/** 导图资源文件名(位于 puzzle/home/map 下) */
|
||||
mapImage?: string;
|
||||
mapImage?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,91 +132,87 @@ interface ResidenceConfig {
|
||||
*/
|
||||
const RESIDENCE_LIST: ResidenceConfig[] = [
|
||||
{
|
||||
residence: "maozedong",
|
||||
displayName: "毛泽东旧居",
|
||||
left: "30%",
|
||||
top: "35%",
|
||||
image: "毛泽东",
|
||||
residence: 'maozedong',
|
||||
displayName: '毛泽东旧居',
|
||||
left: '30%',
|
||||
top: '35%',
|
||||
image: '毛泽东',
|
||||
hasPuzzle: true,
|
||||
hasMap: true,
|
||||
mapImage: "毛泽东导图",
|
||||
mapImage: '毛泽东导图',
|
||||
},
|
||||
{
|
||||
residence: "sunzhongshan",
|
||||
displayName: "孙中山故居",
|
||||
left: "12%",
|
||||
top: "51%",
|
||||
image: "孙中山",
|
||||
residence: 'sunzhongshan',
|
||||
displayName: '孙中山故居',
|
||||
left: '12%',
|
||||
top: '51%',
|
||||
image: '孙中山',
|
||||
hasPuzzle: true,
|
||||
hasMap: true,
|
||||
mapImage: "孙中山导图",
|
||||
mapImage: '孙中山导图',
|
||||
},
|
||||
{
|
||||
residence: "luxun",
|
||||
displayName: "鲁迅故居",
|
||||
left: "26.3%",
|
||||
top: "74%",
|
||||
image: "鲁迅",
|
||||
residence: 'luxun',
|
||||
displayName: '鲁迅故居',
|
||||
left: '26.3%',
|
||||
top: '74%',
|
||||
image: '鲁迅',
|
||||
hasPuzzle: true,
|
||||
hasMap: true,
|
||||
mapImage: "鲁迅导图",
|
||||
mapImage: '鲁迅导图',
|
||||
},
|
||||
{
|
||||
residence: "huangyanpei",
|
||||
displayName: "黄炎培故居",
|
||||
left: "44%",
|
||||
top: "67%",
|
||||
image: "黄炎培",
|
||||
residence: 'huangyanpei',
|
||||
displayName: '黄炎培故居',
|
||||
left: '44%',
|
||||
top: '67%',
|
||||
image: '黄炎培',
|
||||
hasPuzzle: true,
|
||||
hasMap: true,
|
||||
mapImage: "黄炎培导图",
|
||||
mapImage: '黄炎培导图',
|
||||
},
|
||||
{
|
||||
residence: "nieer",
|
||||
displayName: "聂耳旧居",
|
||||
left: "62%",
|
||||
top: "60%",
|
||||
image: "聂耳",
|
||||
residence: 'nieer',
|
||||
displayName: '聂耳旧居',
|
||||
left: '62%',
|
||||
top: '60%',
|
||||
image: '聂耳',
|
||||
hasPuzzle: true,
|
||||
hasMap: false,
|
||||
},
|
||||
{
|
||||
residence: "zhangleping",
|
||||
displayName: "张乐平故居",
|
||||
left: "73%",
|
||||
top: "33%",
|
||||
image: "张乐平",
|
||||
residence: 'zhangleping',
|
||||
displayName: '张乐平故居',
|
||||
left: '73%',
|
||||
top: '33%',
|
||||
image: '张乐平',
|
||||
hasPuzzle: true,
|
||||
hasMap: false,
|
||||
},
|
||||
{
|
||||
residence: "rongdesheng",
|
||||
displayName: "荣德生旧居",
|
||||
left: "90%",
|
||||
top: "45%",
|
||||
image: "荣德生",
|
||||
residence: 'rongdesheng',
|
||||
displayName: '荣德生旧居',
|
||||
left: '90%',
|
||||
top: '45%',
|
||||
image: '荣德生',
|
||||
hasPuzzle: true,
|
||||
hasMap: false,
|
||||
},
|
||||
{
|
||||
residence: "chenyun",
|
||||
displayName: "陈云故居",
|
||||
left: "80%",
|
||||
top: "76%",
|
||||
image: "陈云",
|
||||
residence: 'chenyun',
|
||||
displayName: '陈云故居',
|
||||
left: '80%',
|
||||
top: '76%',
|
||||
image: '陈云',
|
||||
hasPuzzle: true,
|
||||
hasMap: true,
|
||||
mapImage: "陈云导图",
|
||||
mapImage: '陈云导图',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const MenuPuzzleCenter = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSelectResidence,
|
||||
}: MenuPuzzleCenterProps) => {
|
||||
const { getPuzzleCollectProgress } = usePuzzleStore();
|
||||
const [activeMapImage, setActiveMapImage] = useState<string | null>(null);
|
||||
const MenuPuzzleCenter = ({ visible, onClose, onSelectResidence }: MenuPuzzleCenterProps) => {
|
||||
const { getPuzzleCollectProgress } = usePuzzleStore()
|
||||
const [activeMapImage, setActiveMapImage] = useState<string | null>(null)
|
||||
|
||||
const residenceProgress = useMemo(
|
||||
() =>
|
||||
@@ -227,37 +222,40 @@ const MenuPuzzleCenter = ({
|
||||
progress: item.hasPuzzle ? getPuzzleCollectProgress(item.residence) : 0,
|
||||
})),
|
||||
[getPuzzleCollectProgress]
|
||||
);
|
||||
)
|
||||
|
||||
if (!visible) return null;
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-90 flex items-center justify-center"
|
||||
style={{ background: "rgba(0,0,0,0.8)" }}
|
||||
style={{ background: 'rgba(0,0,0,0.8)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: getBackgroundImage("puzzle/home/底图") }}
|
||||
style={{ backgroundImage: getBackgroundImage('puzzle/home/底图') }}
|
||||
>
|
||||
{/* 箭头指示底图 */}
|
||||
<img className="absolute top-84 left-55" src={getStaticImage("puzzle/home/箭头指引")} alt="" />
|
||||
<img
|
||||
className="absolute top-84 left-55"
|
||||
src={getStaticImage('puzzle/home/箭头指引')}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<Image
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage("components/关闭")}
|
||||
src={getStaticImage('components/关闭')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
|
||||
{/* 故居列表:按照设计图进行绝对定位 */}
|
||||
<div className="absolute inset-0">
|
||||
{residenceProgress.map(
|
||||
@@ -278,10 +276,7 @@ const MenuPuzzleCenter = ({
|
||||
style={{ left, top }}
|
||||
>
|
||||
{/* 故居图片(资源在 puzzle/home 下,可根据实际名称调整) */}
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/${image}`)}
|
||||
alt={displayName}
|
||||
/>
|
||||
<Image src={getStaticImage(`puzzle/home/${image}`)} alt={displayName} />
|
||||
|
||||
{/* 进度展示,仅对已开放拼图的故居显示 */}
|
||||
{hasPuzzle && (
|
||||
@@ -292,20 +287,24 @@ const MenuPuzzleCenter = ({
|
||||
|
||||
{/* 图标 */}
|
||||
<div className="flex justify-center gap-6">
|
||||
<Image src={getStaticImage(`puzzle/home/拼图`)} alt="拼图" button
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/拼图`)}
|
||||
alt="拼图"
|
||||
button
|
||||
onClick={(e) => {
|
||||
if (!hasPuzzle) return;
|
||||
e.stopPropagation();
|
||||
onSelectResidence(residence);
|
||||
}} />
|
||||
if (!hasPuzzle) return
|
||||
e.stopPropagation()
|
||||
onSelectResidence(residence)
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src={getStaticImage(`puzzle/home/导图`)}
|
||||
alt="导图"
|
||||
button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!hasMap || !mapImage) return;
|
||||
setActiveMapImage(mapImage);
|
||||
e.stopPropagation()
|
||||
if (!hasMap || !mapImage) return
|
||||
setActiveMapImage(mapImage)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -318,10 +317,10 @@ const MenuPuzzleCenter = ({
|
||||
{activeMapImage && (
|
||||
<div
|
||||
className="fixed inset-0 z-110 flex items-center justify-center"
|
||||
style={{ background: "rgba(0,0,0,0.75)" }}
|
||||
style={{ background: 'rgba(0,0,0,0.75)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setActiveMapImage(null);
|
||||
setActiveMapImage(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -329,25 +328,20 @@ const MenuPuzzleCenter = ({
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
className="absolute -right-4 -top-8 z-10"
|
||||
src={getStaticImage("components/close")}
|
||||
src={getStaticImage('components/close')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={() => setActiveMapImage(null)}
|
||||
/>
|
||||
|
||||
<img
|
||||
src={getStaticImage(`puzzle/home/map/${activeMapImage}`)}
|
||||
alt="导图"
|
||||
/>
|
||||
<img src={getStaticImage(`puzzle/home/map/${activeMapImage}`)} alt="导图" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuPuzzleCenter;
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuPuzzleCenter
|
||||
@@ -1,15 +1,15 @@
|
||||
import { forwardRef } from "react";
|
||||
import { getStaticImage } from "@/lib/utils";
|
||||
import { forwardRef } from 'react'
|
||||
import { getStaticImage } from '@/lib/utils'
|
||||
|
||||
export interface PuzzlePieceDisplayProps {
|
||||
/** 故居名称,如 'chenyun' */
|
||||
residence: string;
|
||||
residence: string
|
||||
/** 碎片ID */
|
||||
pieceId: number;
|
||||
pieceId: number
|
||||
/** 是否显示发光动画 */
|
||||
showGlow?: boolean;
|
||||
showGlow?: boolean
|
||||
/** 自定义类名 */
|
||||
className?: string;
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,16 +19,16 @@ export interface PuzzlePieceDisplayProps {
|
||||
* - 可以被 ref 引用,用于飞行动画
|
||||
*/
|
||||
const PuzzlePieceDisplay = forwardRef<HTMLDivElement, PuzzlePieceDisplayProps>(
|
||||
({ residence, pieceId, showGlow = true, className = "" }, ref) => {
|
||||
({ residence, pieceId, showGlow = true, className = '' }, ref) => {
|
||||
// 碎片图片路径
|
||||
const pieceImage = getStaticImage(`puzzle/${residence}/${pieceId}`);
|
||||
const pieceImage = getStaticImage(`puzzle/${residence}/${pieceId}`)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`relative inline-block ${className}`}
|
||||
style={{
|
||||
animation: showGlow ? "puzzleGlow 1s ease-in-out infinite" : "none",
|
||||
animation: showGlow ? 'puzzleGlow 1s ease-in-out infinite' : 'none',
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@@ -43,10 +43,10 @@ const PuzzlePieceDisplay = forwardRef<HTMLDivElement, PuzzlePieceDisplayProps>(
|
||||
`}</style>
|
||||
<img src={pieceImage} alt={`碎片 ${pieceId}`} className="w-full" />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
PuzzlePieceDisplay.displayName = "PuzzlePieceDisplay";
|
||||
PuzzlePieceDisplay.displayName = 'PuzzlePieceDisplay'
|
||||
|
||||
export default PuzzlePieceDisplay;
|
||||
export default PuzzlePieceDisplay
|
||||
@@ -1,13 +1,13 @@
|
||||
import { forwardRef, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import PuzzlePieceDisplay from "./PuzzlePieceDisplay";
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import PuzzlePieceDisplay from './PuzzlePieceDisplay'
|
||||
|
||||
interface PuzzlePiecePortalProps {
|
||||
residence: string;
|
||||
pieceId: number;
|
||||
residence: string
|
||||
pieceId: number
|
||||
/** 是否隐藏(飞行时隐藏原始碎片) */
|
||||
hidden?: boolean;
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,38 +17,38 @@ interface PuzzlePiecePortalProps {
|
||||
*/
|
||||
const PuzzlePiecePortal = forwardRef<HTMLDivElement, PuzzlePiecePortalProps>(
|
||||
({ residence, pieceId, hidden = false }, ref) => {
|
||||
const [isAnimating, setIsAnimating] = useState(true);
|
||||
const [isAnimating, setIsAnimating] = useState(true)
|
||||
|
||||
// 当组件首次渲染时,标记动画开始
|
||||
useEffect(() => {
|
||||
setIsAnimating(true);
|
||||
setIsAnimating(true)
|
||||
// 动画完成后,重置状态以便下次出现时重新播放
|
||||
const timer = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
}, 800); // 与动画时长一致
|
||||
return () => clearTimeout(timer);
|
||||
}, [pieceId]); // 当 pieceId 变化时重新播放动画
|
||||
setIsAnimating(false)
|
||||
}, 800) // 与动画时长一致
|
||||
return () => clearTimeout(timer)
|
||||
}, [pieceId]) // 当 pieceId 变化时重新播放动画
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
width: "120px",
|
||||
height: "120px",
|
||||
transform: "translate(-50%, -50%)",
|
||||
perspective: "1000px", // 添加透视,让3D效果更明显
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
perspective: '1000px', // 添加透视,让3D效果更明显
|
||||
zIndex: 99999,
|
||||
pointerEvents: "none",
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transformStyle: "preserve-3d", // 保持3D变换
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transformStyle: 'preserve-3d', // 保持3D变换
|
||||
}}
|
||||
initial={
|
||||
isAnimating
|
||||
@@ -61,18 +61,18 @@ const PuzzlePiecePortal = forwardRef<HTMLDivElement, PuzzlePiecePortalProps>(
|
||||
}
|
||||
animate={
|
||||
hidden
|
||||
? { opacity: 0, visibility: "hidden" as const }
|
||||
? { opacity: 0, visibility: 'hidden' as const }
|
||||
: isAnimating
|
||||
? {
|
||||
scale: 1,
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
visibility: "visible" as const,
|
||||
}
|
||||
: {
|
||||
opacity: 1,
|
||||
visibility: "visible" as const,
|
||||
}
|
||||
? {
|
||||
scale: 1,
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
visibility: 'visible' as const,
|
||||
}
|
||||
: {
|
||||
opacity: 1,
|
||||
visibility: 'visible' as const,
|
||||
}
|
||||
}
|
||||
transition={{
|
||||
duration: 0.8, // 稍微延长动画时间,让效果更舒缓
|
||||
@@ -87,23 +87,23 @@ const PuzzlePiecePortal = forwardRef<HTMLDivElement, PuzzlePiecePortalProps>(
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.6,
|
||||
ease: "easeOut",
|
||||
ease: 'easeOut',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PuzzlePieceDisplay
|
||||
residence={residence}
|
||||
pieceId={pieceId}
|
||||
showGlow={true}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
}}
|
||||
>
|
||||
<PuzzlePieceDisplay
|
||||
residence={residence}
|
||||
pieceId={pieceId}
|
||||
showGlow={true}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
)
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
PuzzlePiecePortal.displayName = "PuzzlePiecePortal";
|
||||
PuzzlePiecePortal.displayName = 'PuzzlePiecePortal'
|
||||
|
||||
export default PuzzlePiecePortal;
|
||||
export default PuzzlePiecePortal
|
||||
@@ -1,32 +1,32 @@
|
||||
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 { Image } from "@/components";
|
||||
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 { Image } from '@/components'
|
||||
|
||||
/**
|
||||
* 将英文故居名称转换为中文显示名称
|
||||
*/
|
||||
const getResidenceDisplayName = (residence: string): string => {
|
||||
const nameMap: Record<string, string> = {
|
||||
chenyun: "陈云",
|
||||
huangyanpei: "黄炎培",
|
||||
luxun: "鲁迅",
|
||||
sunzhongshan: "孙中山",
|
||||
};
|
||||
chenyun: '陈云',
|
||||
huangyanpei: '黄炎培',
|
||||
luxun: '鲁迅',
|
||||
sunzhongshan: '孙中山',
|
||||
}
|
||||
|
||||
return nameMap[residence] || residence;
|
||||
};
|
||||
return nameMap[residence] || residence
|
||||
}
|
||||
|
||||
export interface PuzzleSystemProps {
|
||||
/** 是否可见 */
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void;
|
||||
onClose: () => void
|
||||
/** 故居名称,如 'chenyun' */
|
||||
residence: string;
|
||||
residence: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,24 +42,24 @@ const PuzzlePieceSvg = ({
|
||||
onHover,
|
||||
onLeave,
|
||||
}: {
|
||||
residence: string;
|
||||
pieceId: number;
|
||||
isUnlocked: boolean;
|
||||
isSelected: boolean;
|
||||
isHovered: boolean;
|
||||
onClick: () => void;
|
||||
onHover: (id: number) => void;
|
||||
onLeave: () => void;
|
||||
residence: string
|
||||
pieceId: number
|
||||
isUnlocked: boolean
|
||||
isSelected: boolean
|
||||
isHovered: boolean
|
||||
onClick: () => void
|
||||
onHover: (id: number) => void
|
||||
onLeave: () => void
|
||||
}) => {
|
||||
const pathData = getPuzzlePiecePath(residence, pieceId);
|
||||
const pathData = getPuzzlePiecePath(residence, pieceId)
|
||||
|
||||
if (!pathData) return null;
|
||||
if (!pathData) return null
|
||||
|
||||
// 为每个碎片生成唯一的pattern ID
|
||||
const patternId = `pattern-${residence}-${pieceId}`;
|
||||
const patternId = `pattern-${residence}-${pieceId}`
|
||||
|
||||
// 碎片图片路径
|
||||
const pieceImage = getStaticImage(`puzzle/${residence}/${pieceId}`);
|
||||
const pieceImage = getStaticImage(`puzzle/${residence}/${pieceId}`)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,9 +81,9 @@ const PuzzlePieceSvg = ({
|
||||
onMouseEnter={() => (isUnlocked ? onHover(pieceId) : undefined)}
|
||||
onMouseLeave={onLeave}
|
||||
style={{
|
||||
cursor: isUnlocked ? "pointer" : "not-allowed",
|
||||
cursor: isUnlocked ? 'pointer' : 'not-allowed',
|
||||
}}
|
||||
className={cn(isUnlocked ? "puzzle-piece" : "")}
|
||||
className={cn(isUnlocked ? 'puzzle-piece' : '')}
|
||||
>
|
||||
<defs>
|
||||
{/* Safari/iOS 对 SVG image 的 CSS filter 支持有限,使用滤镜定义保证生效 */}
|
||||
@@ -116,7 +116,7 @@ const PuzzlePieceSvg = ({
|
||||
preserveAspectRatio="none"
|
||||
filter={isUnlocked ? undefined : `url(#${patternId}-dim)`}
|
||||
style={{
|
||||
transition: "filter 220ms ease",
|
||||
transition: 'filter 220ms ease',
|
||||
}}
|
||||
/>
|
||||
</pattern>
|
||||
@@ -126,14 +126,14 @@ const PuzzlePieceSvg = ({
|
||||
<path
|
||||
d={pathData}
|
||||
fill={`url(#${patternId})`}
|
||||
stroke={isUnlocked ? (isSelected ? "#75d1fb" : "#fff") : "#888"}
|
||||
stroke={isUnlocked ? (isSelected ? '#75d1fb' : '#fff') : '#888'}
|
||||
strokeWidth={isUnlocked ? (isSelected ? 3 : isHovered ? 2.3 : 2) : 1}
|
||||
opacity={isUnlocked ? 1 : 0.8}
|
||||
/>
|
||||
</g>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼图系统组件
|
||||
@@ -143,36 +143,36 @@ const PuzzlePieceSvg = ({
|
||||
* - 点击碎片可跳转到对应页面
|
||||
*/
|
||||
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);
|
||||
const [hoveredPiece, setHoveredPiece] = useState<number | null>(null);
|
||||
const navigate = useNavigate()
|
||||
const { moduleName } = useParams<{ moduleName: string }>()
|
||||
const { isPuzzleUnlocked, getPuzzleCollectProgress } = usePuzzleStore()
|
||||
const [selectedPiece, setSelectedPiece] = useState<number | null>(null)
|
||||
const [hoveredPiece, setHoveredPiece] = useState<number | null>(null)
|
||||
|
||||
// 当弹窗关闭时重置选中状态
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedPiece(null);
|
||||
setSelectedPiece(null)
|
||||
}
|
||||
}, [visible]);
|
||||
}, [visible])
|
||||
|
||||
if (!visible) return null;
|
||||
if (!visible) return null
|
||||
|
||||
// 背景图片
|
||||
const backgroundImage = getStaticImage("puzzle/底图");
|
||||
const backgroundImage = getStaticImage('puzzle/底图')
|
||||
|
||||
// 处理碎片点击
|
||||
const handlePieceClick = (pieceId: number) => {
|
||||
const isUnlocked = isPuzzleUnlocked(residence, pieceId);
|
||||
const isUnlocked = isPuzzleUnlocked(residence, pieceId)
|
||||
|
||||
if (isUnlocked) {
|
||||
// 显示选中效果,更新详情框
|
||||
setSelectedPiece(pieceId);
|
||||
setSelectedPiece(pieceId)
|
||||
} else {
|
||||
// 未解锁的碎片,可以显示提示或不做任何操作
|
||||
// 暂时不做处理,或者可以添加提示
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 处理前往按钮点击
|
||||
const handleGoToPage = () => {
|
||||
@@ -180,40 +180,36 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
// console.log(residence, selectedPiece, moduleName);
|
||||
// const route = getPuzzleRoute(residence, selectedPiece, moduleName);
|
||||
// console.log(route);
|
||||
const route = selectedPieceData?.route;
|
||||
const route = selectedPieceData?.route
|
||||
if (route) {
|
||||
navigate(`/${moduleName}/${residence}/${route}`);
|
||||
onClose();
|
||||
navigate(`/${moduleName}/${residence}/${route}`)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 获取当前选中碎片的数据
|
||||
const selectedPieceData = selectedPiece
|
||||
? getPuzzlePieceData(residence, selectedPiece)
|
||||
: null;
|
||||
const selectedPieceData = selectedPiece ? getPuzzlePieceData(residence, selectedPiece) : null
|
||||
|
||||
// 生成16块碎片(1-16)
|
||||
const pieces = Array.from({ length: 16 }, (_, i) => i + 1);
|
||||
const pieces = Array.from({ length: 16 }, (_, i) => i + 1)
|
||||
// 将悬停的碎片放到最后渲染,保证 hover 层级在最上
|
||||
const renderPieces =
|
||||
hoveredPiece != null
|
||||
? [...pieces.filter((p) => p !== hoveredPiece), hoveredPiece]
|
||||
: pieces;
|
||||
hoveredPiece != null ? [...pieces.filter((p) => p !== hoveredPiece), hoveredPiece] : pieces
|
||||
|
||||
// 获取收集进度
|
||||
const collectProgress = getPuzzleCollectProgress(residence);
|
||||
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)",
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// 点击背景关闭
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -227,7 +223,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
{/* 关闭按钮 */}
|
||||
<Image
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage("components/关闭")}
|
||||
src={getStaticImage('components/关闭')}
|
||||
alt="关闭"
|
||||
button
|
||||
onClick={onClose}
|
||||
@@ -236,27 +232,27 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
{/* 收集进度 */}
|
||||
<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/收集进度") }}
|
||||
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)]"
|
||||
'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)]'
|
||||
)}
|
||||
>
|
||||
{getResidenceDisplayName(residence)}故居碎片
|
||||
</span>
|
||||
<img
|
||||
className="absolute -top-10 left-0 -z-10"
|
||||
src={getStaticImage("puzzle/碎片装饰")}
|
||||
src={getStaticImage('puzzle/碎片装饰')}
|
||||
alt="碎片装饰"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[#490000] text-4xl font-bold">
|
||||
{/* 收集进度 */}
|
||||
{collectProgress.toFixed(2) == "1.00"
|
||||
? "已集齐"
|
||||
{collectProgress.toFixed(2) == '1.00'
|
||||
? '已集齐'
|
||||
: `收集进度${collectProgress.toFixed(2)}%`}
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,12 +267,12 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
viewBox={getPuzzleViewBox(residence)}
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
filter: "drop-shadow(0px 2px 4px rgba(0,0,0,0.3))",
|
||||
overflow: "visible",
|
||||
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.3))',
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
{renderPieces.map((pieceId) => {
|
||||
const isUnlocked = isPuzzleUnlocked(residence, pieceId);
|
||||
const isUnlocked = isPuzzleUnlocked(residence, pieceId)
|
||||
|
||||
return (
|
||||
<PuzzlePieceSvg
|
||||
@@ -290,7 +286,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
onHover={(id) => setHoveredPiece(id)}
|
||||
onLeave={() => setHoveredPiece(null)}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
@@ -300,36 +296,30 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
<div
|
||||
className="w-[657px] h-[776px] bg-no-repeat bg-cover p-10 text-center"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage("puzzle/详情框"),
|
||||
backgroundImage: getBackgroundImage('puzzle/详情框'),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
{selectedPieceData && (
|
||||
<div
|
||||
className={cn(
|
||||
"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)]"
|
||||
'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)]'
|
||||
)}
|
||||
>
|
||||
{/* 空格换行 */}
|
||||
{selectedPieceData.title
|
||||
.split(" ")
|
||||
.map((word: string, index: number) => (
|
||||
<div key={index}>
|
||||
{word}
|
||||
{index <
|
||||
selectedPieceData.title.split(" ").length - 1 &&
|
||||
" "}
|
||||
</div>
|
||||
))}
|
||||
{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}`
|
||||
)}
|
||||
src={getStaticImage(`puzzle/${selectedPieceData.detailImage}`)}
|
||||
alt={selectedPieceData.title}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
@@ -338,7 +328,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
{selectedPieceData && (
|
||||
<Image
|
||||
className="mt-4"
|
||||
src={getStaticImage("puzzle/点击前往浏览")}
|
||||
src={getStaticImage('puzzle/点击前往浏览')}
|
||||
alt="前往按钮"
|
||||
button
|
||||
onClick={handleGoToPage}
|
||||
@@ -351,7 +341,7 @@ const PuzzleSystem = ({ visible, onClose, residence }: PuzzleSystemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PuzzleSystem;
|
||||
export default PuzzleSystem
|
||||
@@ -1,6 +1,5 @@
|
||||
export { default as PuzzleSystem } from "./PuzzleSystem";
|
||||
export type { PuzzleSystemProps } from "./PuzzleSystem";
|
||||
export { default as PuzzlePieceDisplay } from "./PuzzlePieceDisplay";
|
||||
export type { PuzzlePieceDisplayProps } from "./PuzzlePieceDisplay";
|
||||
export { default as PuzzlePiecePortal } from "./PuzzlePiecePortal";
|
||||
|
||||
export { default as PuzzleSystem } from './PuzzleSystem'
|
||||
export type { PuzzleSystemProps } from './PuzzleSystem'
|
||||
export { default as PuzzlePieceDisplay } from './PuzzlePieceDisplay'
|
||||
export type { PuzzlePieceDisplayProps } from './PuzzlePieceDisplay'
|
||||
export { default as PuzzlePiecePortal } from './PuzzlePiecePortal'
|
||||
Reference in new issue
Block a user