更新依赖项及历史人物相关内容资源
- 更新 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
@@ -1,397 +1,128 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { getStaticImage } from '@/lib/utils'
|
||||
import { Image, Select } from '@/components'
|
||||
import LetterTemplate, { type LetterFieldConfig } from './LetterTemplate'
|
||||
|
||||
export interface LetterToLuxunProps {
|
||||
/** 是否可见 */
|
||||
visible?: boolean
|
||||
/** 关闭回调 */
|
||||
onClose?: () => void
|
||||
/** 提交回调 */
|
||||
onSubmit?: (values: string[]) => void
|
||||
onAnimationEnd?: () => void
|
||||
audioSrc?: string
|
||||
showVideo?: boolean
|
||||
videoSrc?: string
|
||||
replyImageSrc?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* LetterToLuxun 组件 - 写给鲁迅先生的信
|
||||
* 包含飞入和放大动画
|
||||
*/
|
||||
const LetterToLuxun = ({ visible = false, onClose, onSubmit }: LetterToLuxunProps) => {
|
||||
// 下划线可填写内容
|
||||
const [blankValues, setBlankValues] = useState<string[]>([])
|
||||
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
|
||||
const [blankErrors, setBlankErrors] = useState<boolean[]>([])
|
||||
const commonSelectField: LetterFieldConfig = {
|
||||
type: 'select',
|
||||
placeholder: '请选择',
|
||||
fitContent: false,
|
||||
className: '!w-[120px] !min-w-[120px] shrink-0 px-0',
|
||||
options: [
|
||||
{ value: '旨在立人', label: '旨在立人' },
|
||||
{ value: '批判求真', label: '批判求真' },
|
||||
],
|
||||
}
|
||||
|
||||
// 是否已提交:提交后隐藏编辑信件,展示只读信件
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
// 填空类型定义:0=填空1(限填),1=填空2(限填),其他=任填
|
||||
const BLANK_1_ANSWER = '首在立人'
|
||||
const BLANK_2_ANSWER = '批判求真'
|
||||
|
||||
// 填空1的选项
|
||||
const BLANK_1_OPTIONS = [
|
||||
'首在立人',
|
||||
'批判求真',
|
||||
// 可以添加其他选项
|
||||
]
|
||||
|
||||
// 填空2的选项
|
||||
const BLANK_2_OPTIONS = [
|
||||
'首在立人',
|
||||
'批判求真',
|
||||
// 可以添加其他选项
|
||||
]
|
||||
|
||||
// 校验输入:确保所有填空符合标准
|
||||
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 = nextValues[i] || ''
|
||||
const v = raw.trim()
|
||||
|
||||
if (i === 0) {
|
||||
// 填空1:限填,必须选择"首在立人"
|
||||
if (!v || v !== BLANK_1_ANSWER) {
|
||||
nextValues[i] = ''
|
||||
nextErrors[i] = true
|
||||
hasError = true
|
||||
}
|
||||
} else if (i === 1) {
|
||||
// 填空2:限填,必须选择"批判求真"
|
||||
if (!v || v !== BLANK_2_ANSWER) {
|
||||
nextValues[i] = ''
|
||||
nextErrors[i] = true
|
||||
hasError = true
|
||||
}
|
||||
} else {
|
||||
// 其他填空:任填,不能为空,不超过 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 = `敬爱的鲁迅先生:
|
||||
|
||||
您好!
|
||||
|
||||
今天,我怀着崇敬的心情走进大陆新村9号,看着您亲手撰写的《呐喊》《且介亭杂文》《故事新编》等作品,您编辑的《萌芽》《前哨》等杂志,您翻译的《艺术论》《死魂灵》等译文,仿佛能看到您"吾将上下而求索"的身影。在这场跨越时空的对话中,我终于明白您坚守的"____",不是抽象的理念,而是我们安身立命的根本;您秉持的"____",不是无谓的锋芒,而是我们直面现实的勇气。
|
||||
今天,我怀着崇敬的心情走进大陆新村 9 号,看着您亲手撰写的《呐喊》《且介亭杂文》《故事新编》等作品,您编辑的《萌芽》《前哨》等杂志,您翻译的《艺术论》《死魂灵》等译文,仿佛能看到您“吾将上下而求索”的身影。在这场跨越时空的对话中,我终于明白您坚守的“____”,不是抽象的理念,而是我们安身立命的根本;您秉持的“____”,不是无谓的锋芒,而是我们直面现实的勇气。
|
||||
|
||||
我是____专业的学生,今后,我会以您"____千夫指"的骨气,在是非面前坚守立场,不随波逐流;我会以您"俯首甘为____"的热忱,参与____活动,用微光温暖他人;我更会记住您"____,走的人多了,便成了路"的箴言,在专业领域勇于探索,努力用____为民族复兴、文化繁荣添砖加瓦。
|
||||
我是____专业的学生,今后,我会以您“____于夫指”的骨气,在是非面前坚守立场,不随波逐流;我会以您“俯首甘为____”的热忱,参与____活动,用微光温暖他人;我更会记住您“____,走的人多了,也便成了路”的箴言,在专业领域勇于探索,努力用____为民族复兴、文化繁荣添砖加瓦。
|
||||
|
||||
请您放心,您播下的"立人"火种,我们会让它在新时代燎原;您毕生追求的"人国"愿景,我们会用青春和奋斗去守护!`
|
||||
请您放心,您播下的“立人”火种,我们会让它在新时代燎原;您毕生追求的“人国”愿景,我们会用青春和奋斗去守护!`
|
||||
|
||||
const fields: Record<number, LetterFieldConfig> = {
|
||||
0: {
|
||||
...commonSelectField,
|
||||
correctValue: '旨在立人',
|
||||
hints: {
|
||||
error: '请你结合上下文和所学在想想吧!',
|
||||
},
|
||||
readOnlyFallback: '旨在立人',
|
||||
},
|
||||
1: {
|
||||
...commonSelectField,
|
||||
correctValue: '批判求真',
|
||||
hints: {
|
||||
error: '请你结合上下文和所学在想想吧!',
|
||||
},
|
||||
readOnlyFallback: '批判求真',
|
||||
},
|
||||
2: {
|
||||
type: 'input',
|
||||
maxLength: 12,
|
||||
widthClassName: 'w-[120px]',
|
||||
readOnlyFallback: '中文',
|
||||
},
|
||||
3: {
|
||||
type: 'input',
|
||||
maxLength: 12,
|
||||
widthClassName: 'w-[120px]',
|
||||
correctValue: '横眉冷对',
|
||||
readOnlyFallback: '横眉冷对',
|
||||
hints: {
|
||||
error: '“横眉冷对于夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
},
|
||||
},
|
||||
4: {
|
||||
type: 'input',
|
||||
maxLength: 12,
|
||||
widthClassName: 'w-[120px]',
|
||||
correctValue: '孺子牛',
|
||||
readOnlyFallback: '孺子牛',
|
||||
hints: {
|
||||
error: '“横眉冷对于夫指,俯首甘为孺子牛”出自鲁迅先生的《自嘲》一诗。',
|
||||
},
|
||||
},
|
||||
5: {
|
||||
type: 'input',
|
||||
maxLength: 12,
|
||||
widthClassName: 'w-[120px]',
|
||||
readOnlyFallback: '志愿服务',
|
||||
},
|
||||
6: {
|
||||
type: 'input',
|
||||
maxLength: 16,
|
||||
widthClassName: 'w-[120px]',
|
||||
correctValue: '世上本没有路',
|
||||
readOnlyFallback: '世上本没有路',
|
||||
hints: {
|
||||
error: '鲁迅在小说《故乡》中写道:“地上本没有路,走的人多了,也便成了路。”',
|
||||
},
|
||||
},
|
||||
7: {
|
||||
type: 'input',
|
||||
maxLength: 12,
|
||||
widthClassName: 'w-[120px]',
|
||||
readOnlyFallback: '专业所长',
|
||||
},
|
||||
}
|
||||
|
||||
const LetterToLuxun = ({
|
||||
visible = false,
|
||||
onClose,
|
||||
onSubmit,
|
||||
onAnimationEnd,
|
||||
audioSrc = 'audio/letter/3我和我的祖国.m4a',
|
||||
showVideo = true,
|
||||
videoSrc = 'video/letter/neihua_jixin.mp4',
|
||||
replyImageSrc = 'components/letter/点击信详情-鲁迅',
|
||||
}: LetterToLuxunProps) => {
|
||||
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-2"
|
||||
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 isLimitedFill = currentIndex === 0 || currentIndex === 1 // 填空1和填空2为限填
|
||||
const options =
|
||||
currentIndex === 0 ? BLANK_1_OPTIONS : BLANK_2_OPTIONS
|
||||
|
||||
return (
|
||||
<span key={`part-${lineIndex}-${partIndex}`}>
|
||||
{part}
|
||||
{isLimitedFill ? (
|
||||
// 限填:下拉选择
|
||||
<Select
|
||||
className="inline"
|
||||
options={options.map((opt) => ({
|
||||
value: opt,
|
||||
label: opt,
|
||||
}))}
|
||||
value={blankValues[currentIndex] ?? ''}
|
||||
onChange={(selectedValue) => {
|
||||
const next = [...blankValues]
|
||||
next[currentIndex] = selectedValue
|
||||
setBlankValues(next)
|
||||
|
||||
// 用户再次选择时,清除该空的错误高亮
|
||||
if (blankErrors[currentIndex]) {
|
||||
const nextErrs = [...blankErrors]
|
||||
nextErrs[currentIndex] = false
|
||||
setBlankErrors(nextErrs)
|
||||
}
|
||||
}}
|
||||
placeholder="请选择"
|
||||
error={blankErrors[currentIndex]}
|
||||
zIndex={10000}
|
||||
fitContent={true}
|
||||
/>
|
||||
) : (
|
||||
// 任填:文本输入
|
||||
<input
|
||||
name={`blank-${currentIndex}`}
|
||||
className={`inline-block w-[100px] h-[36px] mx-2 mb-2 align-middle 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 className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
|
||||
<img
|
||||
src={getStaticImage("components/letter/寄信图标")}
|
||||
alt="寄信"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</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-2"
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user