更新依赖项及历史人物相关内容资源

- 更新 package.json 和 package-lock.json 中的依赖项
- 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件
- 新增场景和组件的图片资源
- 清理未使用的音频和图片文件
- 优化多个组件和页面的实现逻辑
This commit is contained in:
zhongyidian committed 2026-04-20 13:27:18 +08:00
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
+2
View File
@@ -1,4 +1,6 @@
export { default as ExhibitOverlay } from './ExhibitOverlay'
export type { ExhibitOverlayProps } from './ExhibitOverlay'
export { default as ExhibitFlipOverlay } from './ExhibitFlipOverlay'
export type { ExhibitFlipOverlayProps } from './ExhibitFlipOverlay'
export { default as ExhibitDescriptionNarration } from './ExhibitDescriptionNarration'
export type { ExhibitDescriptionNarrationProps } from './ExhibitDescriptionNarration'