feat:修改英文双引号
This commit is contained in:
commit
4a2be3b4d0
1351 files changed
+56547
No files matched your search
@@ -0,0 +1,560 @@
|
||||
import { useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Image,
|
||||
Video,
|
||||
type VideoAnchor,
|
||||
type VideoRef,
|
||||
type DragFillResult,
|
||||
LetterToLuxun,
|
||||
Narration,
|
||||
EndingOverlay,
|
||||
} from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { SuccessAlert, DragQuizPanel } from '@/components/layout'
|
||||
import { MultipleChoiceQuizPanel, SingleChoiceQuizPanel } from '@/components/layout/quiz'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components'
|
||||
import { usePuzzleAnimation } from '@/contexts/PuzzleAnimationContext'
|
||||
import { setCourseTimeProgress } from '@/lib/utils/api-common'
|
||||
|
||||
type QuizOption = {
|
||||
value: string
|
||||
label: string
|
||||
isCorrect?: boolean
|
||||
explanation?: string
|
||||
}
|
||||
|
||||
type QuizAnchor = {
|
||||
id: string
|
||||
time: number
|
||||
title: string
|
||||
prompt: string
|
||||
// 拖拽题专用:题干后缀(空白区域之后的文本)
|
||||
promptSuffix?: string
|
||||
type: 'single' | 'multiple' | 'drag'
|
||||
options: QuizOption[]
|
||||
// 拖拽题专用
|
||||
blanks?: { id: string; label: string }[]
|
||||
// 拖拽题专用:每个空前面的前缀文案
|
||||
blankPrefixes?: string[]
|
||||
// 拖拽题专用:空白之间的分隔符数组
|
||||
blankSeparators?: string[]
|
||||
// 拖拽题正确答案映射:blankId -> optionValue
|
||||
correctAnswersMap?: Record<string, string>
|
||||
explanation?: string
|
||||
}
|
||||
|
||||
const starName = 'luxun'
|
||||
|
||||
// 锚点数据:根据视频时长调整 time
|
||||
const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
{
|
||||
id: 'quiz-1',
|
||||
time: 60 + 41, // 1:41
|
||||
title: '',
|
||||
prompt: '【拖拽】有学者认为鲁迅有一个宏大的知识大众化计划,对于文盲,鲁迅推荐的是',
|
||||
promptSuffix: '。',
|
||||
type: 'drag',
|
||||
blanks: [
|
||||
{ id: 'blank-1', label: '___' },
|
||||
{ id: 'blank-2', label: '___' },
|
||||
{ id: 'blank-3', label: '___' },
|
||||
],
|
||||
options: [
|
||||
{ value: '电影、讲演、戏剧演出', label: '电影、讲演、戏剧演出' },
|
||||
{ value: '木刻艺术', label: '木刻艺术' },
|
||||
{
|
||||
value: '努力推行新文字和文学通俗化',
|
||||
label: '努力推行新文字和文学通俗化',
|
||||
},
|
||||
],
|
||||
blankSeparators: [';在介于文盲和和识字者之间,鲁迅运用的是', ';鲁迅对作家们的要求是'],
|
||||
correctAnswersMap: {
|
||||
'blank-1': '电影、讲演、戏剧演出',
|
||||
'blank-2': '木刻艺术',
|
||||
'blank-3': '努力推行新文字和文学通俗化',
|
||||
},
|
||||
explanation:
|
||||
'有学者认为鲁迅有一个宏大的知识大众化计划,对于文盲,鲁迅推荐的是电影、讲演、戏剧演出;在介于文盲和识字者之间,鲁迅运用的是木刻艺术;鲁迅对作家们的要求是努力推行新文字和文学通俗化。',
|
||||
},
|
||||
{
|
||||
id: 'quiz-2',
|
||||
time: 60 * 5 + 2, // 5:02
|
||||
title: '',
|
||||
prompt: '【多选】对于鼓励和培养青年一代,鲁迅先生的做法是:',
|
||||
type: 'multiple',
|
||||
options: [
|
||||
{ value: 'A', label: '组织文学社团', isCorrect: true },
|
||||
{ value: 'B', label: '木刻运动', isCorrect: true },
|
||||
{ value: 'C', label: '写纪念烈士文章', isCorrect: true },
|
||||
// {
|
||||
// value: "D",
|
||||
// label: "让青年都去参军",
|
||||
// },
|
||||
],
|
||||
// explanation:
|
||||
// "虽然鲁迅热爱革命,但他主要通过文学社团、木刻艺术和纪念文章来教育和启发青年,而不是通过参军来培养青年。",
|
||||
explanation: '请你再想想吧!',
|
||||
},
|
||||
{
|
||||
id: 'quiz-3',
|
||||
time: 60 * 6 + 39, // 6:39
|
||||
title: '',
|
||||
prompt: '【单选】以下那句诗出自鲁迅的作品:',
|
||||
type: 'single',
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '寄意寒星荃不察,我以我血荐轩辕',
|
||||
isCorrect: true,
|
||||
explanation: '本句出自鲁迅的《自题小像》',
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: '我自横刀向天笑,去留肝胆两昆仑',
|
||||
explanation: '本句出自谭嗣同的《狱中题壁》,请你再想想吧!',
|
||||
},
|
||||
{
|
||||
value: 'C',
|
||||
label: '此去泉台招旧部,旌旗十万斩阎罗',
|
||||
explanation: '本句出自陈毅的《梅岭三章》,请你再想想吧!',
|
||||
},
|
||||
{
|
||||
value: 'D',
|
||||
label: '雄关漫道真如铁,而今迈步从头越',
|
||||
explanation: '本句出自毛泽东的《忆秦娥・娄山关》,请你再想想吧!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'quiz-4',
|
||||
time: 60 * 7 + 59, // 7:59
|
||||
title: '',
|
||||
prompt: '【拖拽】鲁迅先生一生为国为民,写下了大量抨击黑暗社会的檄文',
|
||||
promptSuffix:
|
||||
',他以无所畏惧的姿态和极大的热情,参加反对封建专制、反对帝国主义侵略和国民党反动统治的实际斗争。',
|
||||
type: 'drag',
|
||||
blanks: [
|
||||
{ id: 'blank-1', label: '___' },
|
||||
{ id: 'blank-2', label: '___' },
|
||||
],
|
||||
blankPrefixes: [',如', '如'],
|
||||
blankSeparators: [''],
|
||||
options: [
|
||||
{ value: '七首', label: '七首' },
|
||||
{ value: '投枪', label: '投枪' },
|
||||
{ value: '清泉', label: '清泉' },
|
||||
],
|
||||
correctAnswersMap: {
|
||||
'blank-1': '七首',
|
||||
'blank-2': '投枪',
|
||||
},
|
||||
explanation: '“清泉”不是鲁迅文章的意象。请你再想想吧!',
|
||||
},
|
||||
{
|
||||
id: 'quiz-5',
|
||||
time: 60 * 9 + 53.5, // 9:53
|
||||
title: '',
|
||||
prompt: '【多选】我们能从鲁迅身上学习到:',
|
||||
type: 'multiple',
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '至诚爱国,为民族独立与人民幸福奋斗不息的精神',
|
||||
isCorrect: true,
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: '追求光明、追求真理的人生追求',
|
||||
isCorrect: true,
|
||||
},
|
||||
{
|
||||
value: 'C',
|
||||
label: '博大的文化胸襟与崇高的人格力量',
|
||||
isCorrect: true,
|
||||
},
|
||||
],
|
||||
explanation: '请再仔细分析一下题目,做出更全面的选择吧!',
|
||||
},
|
||||
]
|
||||
|
||||
const LuxunInternalization = () => {
|
||||
const navigate = useNavigate()
|
||||
const { moduleName } = useParams<{ moduleName: string }>()
|
||||
const [activeSuccess, setActiveSuccess] = useState<string | null>(null)
|
||||
const [successContent, setSuccessContent] = useState<string>('')
|
||||
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(null)
|
||||
// 拖拽题状态
|
||||
const [dragQuizValues, setDragQuizValues] = useState<Record<string, DragFillResult[]>>({})
|
||||
const [dragQuizErrors, setDragQuizErrors] = useState<Record<string, string>>({})
|
||||
const [showNarration, setShowNarration] = useState(false) // 是否显示旁白
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [showVideo, setShowVideo] = useState(false)
|
||||
const [showLetter, setShowLetter] = useState(false)
|
||||
const [showStudyGuide, setShowStudyGuide] = useState(false)
|
||||
const videoRef = useRef<VideoRef>(null)
|
||||
//碎片动画触发方法
|
||||
const { triggerPuzzleAnimation } = usePuzzleAnimation()
|
||||
// 旁白关闭后显示对话引导
|
||||
useEffect(() => {
|
||||
setShowNarration(true)
|
||||
}, [])
|
||||
|
||||
const handleVideoCanPlay = () => {
|
||||
if (showVideo && videoRef.current) {
|
||||
videoRef.current.play().catch((error) => {
|
||||
console.warn('视频播放失败:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const anchors: VideoAnchor[] = useMemo(
|
||||
() =>
|
||||
QUIZ_ANCHORS.map((item) => {
|
||||
const correctAnswers = item.options.filter((opt) => opt.isCorrect).map((opt) => opt.value)
|
||||
|
||||
// 拖拽题处理
|
||||
if (item.type === 'drag' && item.blanks) {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: 'remove',
|
||||
render: ({ resume }) => {
|
||||
const handleDragChange = (val: DragFillResult[]) => {
|
||||
setDragQuizValues((prev) => ({ ...prev, [item.id]: val }))
|
||||
setDragQuizErrors((prev) => ({ ...prev, [item.id]: '' }))
|
||||
|
||||
if (!item.blanks || val.length < item.blanks.length) return
|
||||
|
||||
// 如果有位置特定的答案映射,使用它进行验证
|
||||
if (item.correctAnswersMap) {
|
||||
const isCorrect = item.blanks.every((blank) => {
|
||||
const filledResult = val.find((v) => v.blankId === blank.id)
|
||||
const correctAnswer = item.correctAnswersMap![blank.id]
|
||||
return filledResult?.optionValue === correctAnswer
|
||||
})
|
||||
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id)
|
||||
setSuccessContent('')
|
||||
setResumeCallback(() => resume)
|
||||
} else {
|
||||
setDragQuizErrors((prev) => ({
|
||||
...prev,
|
||||
[item.id]: item.explanation || '再想一想,选择正确答案~',
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
// 如果没有位置特定的映射,使用原来的逻辑(检查是否包含了所有正确答案)
|
||||
const filledAnswers = val.map((v) => v.optionValue)
|
||||
const answerSet = new Set(correctAnswers)
|
||||
const filledSet = new Set(filledAnswers)
|
||||
const isCorrect =
|
||||
answerSet.size === filledSet.size &&
|
||||
[...answerSet].every((answer) => filledSet.has(answer))
|
||||
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id)
|
||||
setSuccessContent('')
|
||||
setResumeCallback(() => resume)
|
||||
} else {
|
||||
setDragQuizErrors((prev) => ({
|
||||
...prev,
|
||||
[item.id]: item.explanation || '再想一想,选择正确答案~',
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DragQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title=""
|
||||
questionPrefix={item.prompt}
|
||||
questionSuffix={item.promptSuffix || ''}
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
blanks={item.blanks || []}
|
||||
blankPrefixes={item.blankPrefixes}
|
||||
blankSeparators={item.blankSeparators}
|
||||
value={dragQuizValues[item.id] || []}
|
||||
error={dragQuizErrors[item.id]}
|
||||
onChange={handleDragChange}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
// title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
setSuccessContent('')
|
||||
setDragQuizValues((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[item.id]
|
||||
return next
|
||||
})
|
||||
setDragQuizErrors((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[item.id]
|
||||
return next
|
||||
})
|
||||
const cb = resumeCallback
|
||||
setResumeCallback(null)
|
||||
cb?.()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 单选题处理
|
||||
if (item.type === 'single') {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: 'remove',
|
||||
render: ({ resume }) => (
|
||||
<>
|
||||
<SingleChoiceQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title={`${item.prompt}`}
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: `${opt.value}. ${opt.label}`,
|
||||
}))}
|
||||
correctAnswer={correctAnswers[0]}
|
||||
errorMessage="再思考一下,选择正确答案。"
|
||||
onWrongSelect={(value: string) => {
|
||||
// 为题目3提供详细的选项解释
|
||||
if (item.id === 'quiz-3') {
|
||||
const selectedOption = item.options.find((opt) => opt.value === value)
|
||||
return selectedOption?.explanation || '再思考一下,选择正确答案。'
|
||||
}
|
||||
return undefined
|
||||
}}
|
||||
onSubmit={(_value: string, isCorrect: boolean) => {
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id)
|
||||
// 为题目3提供正确答案的详细解释
|
||||
if (item.id === 'quiz-3') {
|
||||
const correctOption = item.options.find((opt) => opt.isCorrect)
|
||||
setSuccessContent(correctOption?.explanation || '')
|
||||
} else {
|
||||
setSuccessContent('')
|
||||
}
|
||||
setResumeCallback(() => resume)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
// title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
setSuccessContent('')
|
||||
const cb = resumeCallback
|
||||
setResumeCallback(null)
|
||||
cb?.()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// 多选题处理
|
||||
if (item.type === 'multiple') {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: 'remove',
|
||||
render: ({ resume }) => (
|
||||
<>
|
||||
<MultipleChoiceQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title={`${item.prompt}`}
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: `${opt.value}. ${opt.label}`,
|
||||
}))}
|
||||
correctAnswer={correctAnswers}
|
||||
errorMessage={item.explanation || '再思考一下,注意选择所有正确答案。'}
|
||||
onSubmit={(_values: string[], isCorrect: boolean) => {
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id)
|
||||
setSuccessContent('')
|
||||
setResumeCallback(() => resume)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
// title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
setSuccessContent('')
|
||||
const cb = resumeCallback
|
||||
setResumeCallback(null)
|
||||
triggerPuzzleAnimation(16)
|
||||
cb?.()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: 'remove',
|
||||
render: () => null,
|
||||
}
|
||||
}),
|
||||
[
|
||||
activeSuccess,
|
||||
resumeCallback,
|
||||
successContent,
|
||||
dragQuizValues,
|
||||
dragQuizErrors,
|
||||
triggerPuzzleAnimation,
|
||||
]
|
||||
)
|
||||
|
||||
if (!moduleName) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full w-full bg-black"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage('components/internalization/bg'),
|
||||
}}
|
||||
>
|
||||
{/* 离开按钮 */}
|
||||
<Image
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage('components/离开')}
|
||||
button
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
|
||||
/>
|
||||
|
||||
{showVideo && (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={getPublicPath('video/luxun/luxun_neihua.mp4')}
|
||||
className="h-full w-full"
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
setShowLetter(true)
|
||||
// setPostGuideVisible(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 旁白 */}
|
||||
<Narration
|
||||
visible={showNarration}
|
||||
audioSrc={getPublicPath('audio/luxun/mp3_luxun_35.mp3')}
|
||||
text={`走进上海鲁迅故居的红砖小楼,解锁了先生从绍兴到上海的文学足迹,我们读懂了他以笔为刃的战斗姿态,也见证了他与进步人士的革命情谊。那些针砭时弊的杂文、借古讽今的篇章,还有藏在6平方米客房里的红色坚守,都在诉说着先生的热忱。1907年,鲁迅先生在《文化偏至论》里写到:“是故将生存两间,角逐列国是务,其首在立人,人立而后凡事举;若其道术,乃必尊个性而张精神。”`}
|
||||
onClose={() => {
|
||||
setShowNarration(false)
|
||||
setShowVideo(true)
|
||||
// handleVideoCanPlay()
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 对话引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible)
|
||||
if (!visible) {
|
||||
setShowVideo(true)
|
||||
handleVideoCanPlay()
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
>
|
||||
{/* <GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/luxun/mp3_luxun_35.mp3')}>
|
||||
走进上海鲁迅故居的红砖小楼,解锁了先生从绍兴到上海的文学足迹,我们读懂了他以笔为刃的战斗姿态,也见证了他与进步人士的革命情谊。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
那些针砭时弊的杂文、借古讽今的篇章,还有藏在6平方米客房里的红色坚守,都在诉说着先生的热忱。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
1907年,鲁迅先生在《文化偏至论》里写到:“是故将生存两间,角逐列国是务,其首在立人,人立而后凡事举;若其道术,乃必尊个性而张精神。”
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
先生认为,中华民族要屹立于世界民族之林,“首在立人”,“立人”思想从此贯穿在鲁迅先生的人生和作品中。”先生认为,中华民族要屹立于世界民族之林,“首在立人”,“立人”思想从此贯穿在鲁迅先生的人生和作品中。
|
||||
</GuideStepsItem> */}
|
||||
<GuideStepsItem speaker="student" audioSrc={getPublicPath('audio/luxun/mp3_luxun_36.mp3')}>
|
||||
小沪,关于“首在立人”这四个字我还是有些不理解,立人为什么这么重要?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide" audioSrc={getPublicPath('audio/luxun/mp3_luxun_37.mp3')}>
|
||||
“首在立人”这四个字,可以说是鲁迅先生一生的价值观。今天我们有幸邀请到了上海鲁迅纪念馆研究馆员施晓燕。关于你的问题,我想施老师会给你指点迷津!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 视频结束后的引导 */}
|
||||
{/* <GuideSteps
|
||||
visible={postGuideVisible}
|
||||
onVisibleChange={(visible) => {
|
||||
setPostGuideVisible(visible);
|
||||
if (!visible) {
|
||||
setShowLetter(true);
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
zIndex={60}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
听完施老师的讲解,相信你对鲁迅先生的"立人"精神有了更深的理解。现在,就请你把这份感悟写下来,既总结参观的收获,也给自己未来许下一个承诺吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps> */}
|
||||
|
||||
{/* 信件组件 */}
|
||||
<LetterToLuxun
|
||||
visible={showLetter}
|
||||
onClose={() => {
|
||||
setShowLetter(false)
|
||||
}}
|
||||
onAnimationEnd={() => {
|
||||
setShowStudyGuide(true)
|
||||
}}
|
||||
showVideo={true}
|
||||
/>
|
||||
|
||||
<EndingOverlay
|
||||
visible={showStudyGuide}
|
||||
text="恭喜你圆满完成鲁迅故居的学习之旅,请带着研学感悟,前往黄炎培故居继续探访吧~"
|
||||
animationSpeed={200}
|
||||
onContinue={() => {
|
||||
setShowStudyGuide(false)
|
||||
setCourseTimeProgress(9)
|
||||
navigate('/menu')
|
||||
}}
|
||||
audioSrc={getPublicPath('audio/luxun/mp3_luxun_38.mp3')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LuxunInternalization
|
||||
Reference in new issue
Block a user