Files
shanghai-residences/src/components/puzzle/PuzzlePiecePortal.tsx
T
zhongyidian 76c67b492e 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 和其他组件文件,统一代码风格,提升可读性。
2026-03-05 13:50:47 +08:00

110 lines
3.1 KiB
TypeScript

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
/** 是否隐藏(飞行时隐藏原始碎片) */
hidden?: boolean
}
/**
* 碎片 Portal 组件
* 将碎片渲染到 document.body,确保在最上层显示
* 支持旋转+放大的初始出现动画
*/
const PuzzlePiecePortal = forwardRef<HTMLDivElement, PuzzlePiecePortalProps>(
({ residence, pieceId, hidden = false }, ref) => {
const [isAnimating, setIsAnimating] = useState(true)
// 当组件首次渲染时,标记动画开始
useEffect(() => {
setIsAnimating(true)
// 动画完成后,重置状态以便下次出现时重新播放
const timer = setTimeout(() => {
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效果更明显
zIndex: 99999,
pointerEvents: 'none',
}}
>
<motion.div
ref={ref}
style={{
width: '100%',
height: '100%',
transformStyle: 'preserve-3d', // 保持3D变换
}}
initial={
isAnimating
? {
scale: 0,
rotateY: -180,
opacity: 0,
}
: false
}
animate={
hidden
? { opacity: 0, visibility: 'hidden' as const }
: isAnimating
? {
scale: 1,
rotateY: 0,
opacity: 1,
visibility: 'visible' as const,
}
: {
opacity: 1,
visibility: 'visible' as const,
}
}
transition={{
duration: 0.8, // 稍微延长动画时间,让效果更舒缓
ease: [0.25, 0.46, 0.45, 0.94], // 平滑的缓动函数,缓缓出现
scale: {
duration: 0.8,
ease: [0.25, 0.46, 0.45, 0.94],
},
rotateY: {
duration: 0.8,
ease: [0.25, 0.46, 0.45, 0.94],
},
opacity: {
duration: 0.6,
ease: 'easeOut',
},
}}
>
<PuzzlePieceDisplay
residence={residence}
pieceId={pieceId}
showGlow={true}
className="w-full h-full"
/>
</motion.div>
</div>,
document.body
)
}
)
PuzzlePiecePortal.displayName = 'PuzzlePiecePortal'
export default PuzzlePiecePortal