更新依赖项及历史人物相关内容资源
- 更新 package.json 和 package-lock.json 中的依赖项 - 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件 - 新增场景和组件的图片资源 - 清理未使用的音频和图片文件 - 优化多个组件和页面的实现逻辑
This commit is contained in:
1 parent
b1d55b9464
commit
e0fa47d21a
629 files changed
+24390
-2991
No files matched your search
@@ -0,0 +1,208 @@
|
||||
import { cn, getStaticImage } from '@/lib/utils'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { FlipActionButton, Image } from '@/components'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const OVERLAY_STYLE: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
}
|
||||
|
||||
export interface ExhibitFlipOverlayProps {
|
||||
/** 展品正面图片地址,可以是静态资源路径或完整 URL */
|
||||
src: string
|
||||
/** 展品翻面(背面)图片地址,可以是静态资源路径或完整 URL */
|
||||
flipSrc?: string
|
||||
/**
|
||||
* 展品默认展示缩放(用于“默认放大”)
|
||||
* - 1 表示原尺寸逻辑
|
||||
* - > 1 会放大展品展示(仍受 max-w/max-h 约束)
|
||||
*/
|
||||
displayScale?: number
|
||||
/** 展品入场动画播放完成(背景/按钮出现前后) */
|
||||
onEnterComplete?: () => void
|
||||
/** 关闭回调 */
|
||||
onClose?: () => void
|
||||
/** 是否可见,默认 true */
|
||||
visible?: boolean
|
||||
/** 额外的自定义类 */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量展品翻转层
|
||||
* - 不使用 Portal
|
||||
* - 不支持缩放/拖拽
|
||||
* - 点击展品区域翻转显示另一张图(带 3D 翻转动画)
|
||||
*/
|
||||
const ExhibitFlipOverlay = ({
|
||||
src,
|
||||
flipSrc,
|
||||
displayScale = 1,
|
||||
onEnterComplete,
|
||||
onClose,
|
||||
visible = true,
|
||||
className,
|
||||
}: ExhibitFlipOverlayProps) => {
|
||||
const backSrc = getStaticImage('components/close')
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [flipped, setFlipped] = useState(false)
|
||||
const [showChrome, setShowChrome] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setFlipped(false)
|
||||
setShowChrome(false)
|
||||
}
|
||||
}, [visible, src, flipSrc])
|
||||
|
||||
const frontImageSrc =
|
||||
src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')
|
||||
? src
|
||||
: getStaticImage(src)
|
||||
|
||||
const backImageSrc = useMemo(() => {
|
||||
if (!flipSrc) return null
|
||||
return flipSrc.startsWith('http://') ||
|
||||
flipSrc.startsWith('https://') ||
|
||||
flipSrc.startsWith('/')
|
||||
? flipSrc
|
||||
: getStaticImage(flipSrc)
|
||||
}, [flipSrc])
|
||||
|
||||
const canFlip = Boolean(backImageSrc && backImageSrc !== frontImageSrc)
|
||||
|
||||
if (!mounted || !visible) return null
|
||||
|
||||
return (
|
||||
<div style={OVERLAY_STYLE}>
|
||||
<div
|
||||
className={cn('absolute inset-0 flex items-center justify-center', className)}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
{/* 第二段:背景与按钮在展品入场后再出现 */}
|
||||
{showChrome && (
|
||||
<>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/70"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.22, ease: 'easeOut' }}
|
||||
/>
|
||||
|
||||
{onClose && (
|
||||
<motion.div
|
||||
className="absolute top-5 right-5 z-20"
|
||||
initial={{ opacity: 0, y: -6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22, ease: 'easeOut' }}
|
||||
>
|
||||
<Image
|
||||
src={backSrc}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}}
|
||||
alt="返回"
|
||||
button
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="relative z-10 w-[900px] h-[900px] max-w-[90%] max-h-[90%] flex items-center justify-center select-none"
|
||||
style={{
|
||||
perspective: 1200,
|
||||
WebkitPerspective: '1200px',
|
||||
}}
|
||||
>
|
||||
{/* 第一段:像“获得装备”一样先把展品弹出 */}
|
||||
<motion.div
|
||||
className={cn(
|
||||
'relative w-full h-full',
|
||||
'transition-transform duration-500 ease-[cubic-bezier(0.2,0.8,0.2,1)]'
|
||||
)}
|
||||
initial={{ scale: 0.25, opacity: 0, rotateY: 0 }}
|
||||
animate={{ scale: displayScale, opacity: 1, rotateY: flipped ? 180 : 0 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 520,
|
||||
damping: 26,
|
||||
mass: 0.8,
|
||||
}}
|
||||
onAnimationComplete={() => {
|
||||
if (!showChrome) {
|
||||
setShowChrome(true)
|
||||
onEnterComplete?.()
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
WebkitTransformStyle: 'preserve-3d',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={frontImageSrc}
|
||||
alt="展品"
|
||||
className="max-w-full max-h-full object-contain drop-shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{canFlip && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden',
|
||||
transform: 'rotateY(180deg)',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={backImageSrc!}
|
||||
alt="展品背面"
|
||||
className="max-w-full max-h-full object-contain drop-shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{showChrome && canFlip && (
|
||||
<motion.div
|
||||
className="absolute bottom-50 right-0 z-20"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22, ease: 'easeOut' }}
|
||||
>
|
||||
<FlipActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setFlipped((v) => !v)
|
||||
}}
|
||||
>
|
||||
翻转一下
|
||||
</FlipActionButton>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExhibitFlipOverlay
|
||||
Reference in new issue
Block a user