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

- 更新 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

+63 -301
View File
@@ -1,68 +1,17 @@
import { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { getStaticImage } from '@/lib/utils'
import { Image } from '@/components'
import LetterTemplate, { type LetterFieldConfig } from './LetterTemplate'
export interface LetterToChenyunProps {
/** 是否可见 */
visible?: boolean
/** 关闭回调 */
onClose?: () => void
/** 提交回调 */
onSubmit?: (values: string[]) => void
onAnimationEnd?: () => void
audioSrc?: string
showVideo?: boolean
videoSrc?: string
replyImageSrc?: string
}
/**
* LetterToChenyun 组件 - 寄给陈云爷爷的信
* 包含飞入和放大动画
*/
const LetterToChenyun = ({ visible = false, onClose, onSubmit }: LetterToChenyunProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([])
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
const [blankErrors, setBlankErrors] = useState<boolean[]>([])
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false)
// 校验输入:不能为空、每个不超过 10 个字符
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length
const nextErrors: boolean[] = Array(placeholderCount).fill(false)
const nextValues: string[] = [...blankValues]
let hasError = false
for (let i = 0; i < placeholderCount; i++) {
const raw = blankValues[i] || ''
const v = raw.trim()
// 为空 或 超过 10 个字,都视为不符合要求:清空并标红
if (!v || v.length > 10) {
nextValues[i] = ''
nextErrors[i] = true
hasError = true
}
}
setBlankValues(nextValues)
setBlankErrors(nextErrors)
return !hasError
}
// 弹窗关闭后重置内容,方便下次重新填写
useEffect(() => {
if (!visible) {
setSubmitted(false)
setBlankValues([])
setBlankErrors([])
}
}, [visible])
// 信件内容模板(根据设计稿)
const letterContent = `敬爱的陈云爷爷:
const letterContent = `敬爱的陈云爷爷:
您好!
我是____专业的学生。今天,我怀着崇敬又激动的心情,参观了位于上海市青浦区练塘镇的陈云纪念馆,学习了您“不唯上、不唯书、只唯____”的做事准则,还有“综合平衡”“人民至上”的经济思想。我深刻体会到,这些精神不仅是建设年代的智慧结晶,更是新时代指引我们前行的思想灯塔。
@@ -71,250 +20,63 @@ const LetterToChenyun = ({ visible = false, onClose, onSubmit }: LetterToChenyun
站在新时代的起点,我一定以____为志向,勤学苦练,传承精神,让您的务实担当在青春里扎根,让奋斗在建设祖国的实践中绽放光彩!`
const fields: Record<number, LetterFieldConfig> = {
0: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
readOnlyFallback: '实践',
},
1: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
readOnlyFallback: '勤学钻研',
},
2: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
readOnlyFallback: '人民利益',
},
3: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
readOnlyFallback: '专业',
},
4: {
type: 'input',
maxLength: 12,
widthClassName: 'w-[120px]',
readOnlyFallback: '实干报国',
},
}
const LetterToChenyun = ({
visible = false,
onClose,
onSubmit,
onAnimationEnd,
audioSrc = 'audio/letter/8如愿.m4a',
showVideo = true,
videoSrc = 'video/letter/neihua_jixin.mp4',
replyImageSrc = 'components/letter/点击信详情-陈云',
}: LetterToChenyunProps) => {
return (
<AnimatePresence>
{visible && (
<>
{/* 遮罩层 */}
<motion.div
className="fixed inset-0 bg-black/50 z-[100]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
onClick={onClose}
/>
{/* 信件容器 */}
<motion.div
className="fixed inset-0 z-[101] flex items-center justify-center pointer-events-none"
initial={{ opacity: 0, scale: 0.5, y: 100 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.5, y: 100 }}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
duration: 0.6,
}}
>
<div
className="relative w-[1200px] h-full pointer-events-auto"
onClick={(e) => e.stopPropagation()}
style={{ perspective: '1200px' }}
>
{/* 编辑态 / 只读态:Y 轴翻转切换 */}
<motion.div
className="relative w-full h-full"
initial={false}
animate={{ rotateY: submitted ? 180 : 0 }}
transition={{ duration: 0.7, ease: 'easeInOut' }}
style={{ transformStyle: 'preserve-3d' }}
>
{/* 前面:编辑态 */}
<div
className="absolute inset-0"
style={{
backfaceVisibility: 'hidden',
// 编辑态展示时才响应事件,避免与背面点击冲突
pointerEvents: submitted ? 'none' : 'auto',
}}
>
{/* 编辑态:信纸背景 */}
<img
src={getStaticImage('components/letter/信底')}
alt="信纸"
className="absolute inset-0 w-full h-full object-contain"
/>
{/* 标题 - 垂直显示在左侧 */}
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
<div
className="text-white text-6xl font-bold tracking-[20px]"
style={{
writingMode: 'vertical-rl',
textOrientation: 'upright',
}}
>
寄给陈云的信
</div>
</div>
{/* 信件内容区域(可编辑) */}
<div className="absolute w-[700px] left-[320px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0
let foundNiHao = false
return letterContent.split('\n').map((line, lineIndex) => {
// 检测是否已经找到"您好"行
if (line.includes('您好')) {
foundNiHao = true
}
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao && line.trim() !== '' && !line.includes('您好')
const segments = line.split('____')
return (
<div
key={lineIndex}
className="mb-6"
style={needsIndent ? { textIndent: '2em' } : {}}
>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1
if (isLast) {
return <span key={`part-${lineIndex}-${partIndex}`}>{part}</span>
}
const currentIndex = placeholderIndex++
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? 'border-red-500'
: 'border-[#4e402e]'
}`}
value={blankValues[currentIndex] ?? ''}
onChange={(e) => {
const next = [...blankValues]
next[currentIndex] = e.target.value
setBlankValues(next)
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors]
nextErrs[currentIndex] = false
setBlankErrors(nextErrs)
}
}}
/>
</span>
)
})}
</div>
)
})
})()}
</div>
</div>
{/* 笔的图片 */}
<div className="absolute bottom-[150px] -right-30">
<img
src={getStaticImage('components/letter/笔')}
alt="笔"
className="w-full h-full object-contain"
/>
</div>
{/* 提交按钮:切换到只读信件态 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage('components/letter/可提交状态')}
alt="可提交状态"
onClick={() => {
if (!validateBlanks()) return
setSubmitted(true)
}}
button
/>
</div>
</div>
{/* 背面:只读态 */}
<div
className="absolute inset-0"
style={{
transform: 'rotateY(180deg)',
backfaceVisibility: 'hidden',
// 只读态展示时才响应事件
pointerEvents: submitted ? 'auto' : 'none',
}}
>
{/* 只读态:新背景 */}
<img
src={getStaticImage('components/letter/信')}
alt="信件"
className="absolute inset-0 w-full h-full object-contain"
/>
<Image
src={getStaticImage('components/close')}
alt="信纸"
className="absolute right-38 top-0"
button
onClick={() => {
setSubmitted(false)
}}
/>
{/* 信件内容区域(只读) */}
<div className="absolute w-[700px] left-[260px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0
let foundNiHao = false
return letterContent.split('\n').map((line, lineIndex) => {
if (line.includes('您好')) {
foundNiHao = true
}
const needsIndent =
foundNiHao && line.trim() !== '' && !line.includes('您好')
const segments = line.split('____')
return (
<div
key={lineIndex}
className="mb-6"
style={needsIndent ? { textIndent: '2em' } : {}}
>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1
if (isLast) {
return <span key={`part-${lineIndex}-${partIndex}`}>{part}</span>
}
const currentIndex = placeholderIndex++
const value = blankValues[currentIndex] ?? ''
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
{value || '  '}
</span>
)
})}
</div>
)
})
})()}
</div>
</div>
{/* 寄出按钮:关闭窗口 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage('components/letter/寄出')}
alt="寄出"
onClick={() => {
onSubmit?.(blankValues)
}}
button
/>
</div>
</div>
</motion.div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
<LetterTemplate
visible={visible}
onClose={onClose}
onSubmit={onSubmit}
onAnimationEnd={onAnimationEnd}
title="寄给陈云的信"
letterContent={letterContent}
fields={fields}
audioSrc={audioSrc}
showVideo={showVideo}
videoSrc={videoSrc}
replyImageSrc={replyImageSrc}
/>
)
}