feat:修改英文双引号
This commit is contained in:
commit
4a2be3b4d0
1351 files changed
+56547
No files matched your search
@@ -0,0 +1,564 @@
|
||||
import { useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Image,
|
||||
Video,
|
||||
type VideoAnchor,
|
||||
type VideoRef,
|
||||
type DragFillResult,
|
||||
LetterToSunzhongshan,
|
||||
Narration,
|
||||
EndingOverlay,
|
||||
} from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||
import { SuccessAlert, DragQuizPanel } from '@/components/layout'
|
||||
import { MultipleChoiceQuizPanel } 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 }[]
|
||||
// 拖拽题专用:空白之间的分隔符数组
|
||||
blankSeparators?: string[]
|
||||
// 拖拽题正确答案映射:blankId -> optionValue
|
||||
correctAnswersMap?: Record<string, string>
|
||||
explanation?: string
|
||||
}
|
||||
|
||||
// 星名固定为 sunzhongshan
|
||||
const starName = 'sunzhongshan'
|
||||
|
||||
// 锚点数据:可根据实际视频时长调整 time
|
||||
const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
{
|
||||
id: 'quiz-1',
|
||||
time: 60 + 14, //5.52分钟
|
||||
title: '题目1',
|
||||
prompt:
|
||||
'【拖拽】习近平总书记在纪念孙中山先生诞辰150周年大会上指出: "中国共产党人是孙中山先生革命事业最坚定的',
|
||||
promptSuffix: '、最忠实的继承者。"',
|
||||
type: 'drag',
|
||||
blanks: [
|
||||
{ id: 'blank-1', label: '最坚定的' },
|
||||
{ id: 'blank-2', label: '最忠诚的' },
|
||||
],
|
||||
options: [
|
||||
{
|
||||
value: '支持者',
|
||||
label: '支持者',
|
||||
isCorrect: true,
|
||||
},
|
||||
{
|
||||
value: '合作者',
|
||||
label: '合作者',
|
||||
isCorrect: true,
|
||||
},
|
||||
],
|
||||
blankSeparators: ['、最忠诚的'],
|
||||
correctAnswersMap: {
|
||||
'blank-1': '支持者',
|
||||
'blank-2': '合作者',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'quiz-2',
|
||||
time: 60 * 2, // 2分钟
|
||||
title: '题目2',
|
||||
prompt: '【多选】新时代青年传承“振兴中华”的精神,能够具体落实为:',
|
||||
type: 'multiple',
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '专注学习,提升自身技能与素养',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!青年通过专注学习、提升自身能力,用精益求精的精神服务于国家建设,才能成为“振兴中华”的践行者。',
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: '秉持精益求精的精神,在专业领域深耕',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!青年在自己的专业领域深耕细作,才能以扎实的能力为国家发展贡献力量。',
|
||||
},
|
||||
{
|
||||
value: 'C',
|
||||
label: '遇到困难不退缩,不因失败而灰心',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!不因失败而灰心,也从不因困难而退缩,这种坚韧的态度是践行“振兴中华”过程中不可或缺的品质。',
|
||||
},
|
||||
{
|
||||
value: 'D',
|
||||
label: '放弃个人职业规划,直接投身革命事业',
|
||||
isCorrect: false,
|
||||
explanation:
|
||||
'振兴中华要把个人理想融入国家命运,在自己的领域里成为“振兴中华”的践行者。请你再想想吧!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'quiz-3',
|
||||
time: 60 * 4 + 14, // 4分14秒
|
||||
title: '题目3',
|
||||
prompt: '【多选】新时代青年涵养“天下为公”的情怀能够具体落实为:',
|
||||
type: 'multiple',
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '做一名志愿者走进养老院服务老人',
|
||||
isCorrect: true,
|
||||
explanation: '恭喜你,选择正确!对他人的关爱与奉献是传承“天下为公”情怀的具体行动哦!',
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: '只专注于个人学业与职业发展,不参与公共事务',
|
||||
isCorrect: false,
|
||||
explanation: '这与“天下为公”的情怀完全相悖。请你再想想吧!',
|
||||
},
|
||||
{
|
||||
value: 'C',
|
||||
label: '用自己所学的技能为企业、社区提供服务',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!用自己所学的技能为企业、社区提供服务是青年将个人能力转化为公共服务、践行“天下为公”的重要方式。',
|
||||
},
|
||||
{
|
||||
value: 'D',
|
||||
label: '在日常生活中关心身边有需要的人',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!“天下为公”的情怀不需要宏大叙事,从身边小事做起就是具体落实。',
|
||||
},
|
||||
],
|
||||
explanation: '请再仔细分析一下题目,做出更全面的选择吧!',
|
||||
},
|
||||
{
|
||||
id: 'quiz-4',
|
||||
time: 60 * 5 + 40.5,
|
||||
title: '题目4',
|
||||
prompt: '【多选】新时代青年学习“与时俱进”的智慧能够具体落实为:',
|
||||
type: 'multiple',
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '主动学习新知识,关注科技等前沿领域',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!主动学习新知识、适应新趋势正是青年与时俱进、紧跟时代步伐的具体表现。',
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: '在工作中突破传统思维,用创新方法解决问题',
|
||||
isCorrect: true,
|
||||
explanation:
|
||||
'恭喜你,选择正确!在工作中突破传统思维,用创新解决问题正是与时俱进精神在职业场景中的落地。',
|
||||
},
|
||||
{
|
||||
value: 'C',
|
||||
label: '固执己见,拒绝接受新事物和新变化',
|
||||
isCorrect: false,
|
||||
explanation: '这与“与时俱进”的智慧完全相悖。请你再想想吧!',
|
||||
},
|
||||
],
|
||||
explanation: '请再仔细分析一下题目,做出更全面的选择吧!',
|
||||
},
|
||||
{
|
||||
id: 'quiz-5',
|
||||
time: 60 * 5 + 40.5,
|
||||
title: '题目5',
|
||||
prompt: '【多选】新时代的青年要从孙中山先生身上学习哪些精神?',
|
||||
type: 'multiple',
|
||||
options: [
|
||||
{
|
||||
value: 'A',
|
||||
label: '“振兴中华”的初心',
|
||||
isCorrect: true,
|
||||
},
|
||||
{
|
||||
value: 'B',
|
||||
label: '“天下为公”的情怀',
|
||||
isCorrect: true,
|
||||
},
|
||||
{ value: 'C', label: '“与时俱进”的智慧', isCorrect: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const SunzhongshanInternalization = () => {
|
||||
const navigate = useNavigate()
|
||||
const { moduleName } = useParams<{ moduleName: string }>()
|
||||
|
||||
// 获取碎片动画触发方法
|
||||
const { triggerPuzzleAnimation } = usePuzzleAnimation()
|
||||
|
||||
const [activeSuccess, setActiveSuccess] = useState<string | null>(null)
|
||||
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(true) // 是否显示旁白
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [showVideo, setShowVideo] = useState(false)
|
||||
const [postGuideVisible, setPostGuideVisible] = useState(false)
|
||||
const [showLetter, setShowLetter] = useState(false)
|
||||
const [showStudyGuide, setShowStudyGuide] = useState(false)
|
||||
const videoRef = useRef<VideoRef>(null)
|
||||
|
||||
// 旁白关闭后显示对话引导
|
||||
useEffect(() => {
|
||||
if (!showNarration) {
|
||||
setVisible(true)
|
||||
}
|
||||
}, [showNarration])
|
||||
|
||||
// 视频加载完成后自动播放
|
||||
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)
|
||||
setResumeCallback(() => resume)
|
||||
} else {
|
||||
setDragQuizErrors((prev) => ({
|
||||
...prev,
|
||||
[item.id]: '再想一想,选择正确答案~',
|
||||
}))
|
||||
}
|
||||
} 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)
|
||||
setResumeCallback(() => resume)
|
||||
} else {
|
||||
setDragQuizErrors((prev) => ({
|
||||
...prev,
|
||||
[item.id]: '再想一想,选择正确答案~',
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 || []}
|
||||
blankSeparators={item.blankSeparators}
|
||||
value={dragQuizValues[item.id] || []}
|
||||
error={dragQuizErrors[item.id]}
|
||||
onChange={handleDragChange}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content=""
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
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)
|
||||
triggerPuzzleAnimation(15)
|
||||
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.id === 'quiz-2'
|
||||
? (selectedValues: string[]) => {
|
||||
const picked = new Set(selectedValues)
|
||||
const pickedD = item.options.find((o) => o.value === 'D')
|
||||
if (picked.has('D')) return pickedD?.explanation || ''
|
||||
return '请再仔细分析一下题目,做出更全面的选择吧!'
|
||||
}
|
||||
: item.id === 'quiz-3'
|
||||
? (selectedValues: string[]) => {
|
||||
const picked = new Set(selectedValues)
|
||||
const pickedB = item.options.find((o) => o.value === 'B')
|
||||
if (picked.has('B')) return pickedB?.explanation || ''
|
||||
return item.explanation || '请再仔细分析一下题目,做出更全面的选择吧!'
|
||||
}
|
||||
: item.id === 'quiz-4'
|
||||
? (selectedValues: string[]) => {
|
||||
const picked = new Set(selectedValues)
|
||||
const pickedC = item.options.find((o) => o.value === 'C')
|
||||
if (picked.has('C')) return pickedC?.explanation || ''
|
||||
return (
|
||||
item.explanation || '请再仔细分析一下题目,做出更全面的选择吧!'
|
||||
)
|
||||
}
|
||||
: item.id === 'quiz-5'
|
||||
? (selectedValues: string[]) => {
|
||||
const picked = new Set(selectedValues)
|
||||
const pickedC = item.options.find((o) => o.value === 'C')
|
||||
if (picked.has('C')) return pickedC?.explanation || ''
|
||||
return (
|
||||
item.explanation || '请再仔细分析一下题目,做出更全面的选择吧!'
|
||||
)
|
||||
}
|
||||
: item.explanation || '再想一想,确保选择了所有正确答案~'
|
||||
}
|
||||
onSubmit={(values: string[], isCorrect: boolean) => {
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id)
|
||||
// if (item.id === "quiz-2") {
|
||||
// const picked = new Set(values);
|
||||
// const content = item.options
|
||||
// .filter((o) => picked.has(o.value))
|
||||
// .map((o) => o.explanation)
|
||||
// .filter(Boolean)
|
||||
// .join("\n");
|
||||
// setSuccessContent(content || "回答正确,请继续探索。");
|
||||
// } else {
|
||||
// setSuccessContent("回答正确,请继续探索。");
|
||||
// }
|
||||
setResumeCallback(() => resume)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
// title="恭喜你, 选择正确!"
|
||||
// content="回答正确,请继续探索。"
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null)
|
||||
const cb = resumeCallback
|
||||
setResumeCallback(null)
|
||||
triggerPuzzleAnimation(16)
|
||||
cb?.()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// 单选题处理(保留原有逻辑,以防万一)
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: 'remove',
|
||||
render: () => null,
|
||||
}
|
||||
}),
|
||||
[activeSuccess, resumeCallback, 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/sunzhongshan/sunzhongshan_neihua.mp4')}
|
||||
className="h-full w-full"
|
||||
anchors={anchors}
|
||||
playsInline
|
||||
controls
|
||||
onCanPlay={handleVideoCanPlay}
|
||||
onEnded={() => {
|
||||
setShowVideo(false)
|
||||
setPostGuideVisible(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 旁白 */}
|
||||
<Narration
|
||||
visible={showNarration}
|
||||
audioChannelId="sunzhongshan-internalization-narration"
|
||||
text="从翠亨村的农家院落到影响中国的革命征程,孙中山先生以'亟拯斯民于水火,切扶大厦之将倾'的担当,在近代中国的黑暗中点燃了民主共和的火种。他留下的不仅是推翻帝制、建立共和的历史功绩,更有'振兴中华'的初心、'天下为公'的情怀、'与时俱进'的智慧。今天,我们站在故居的青砖黛瓦间,不仅要回望他为民族独立、人民幸福奋斗的一生,更要让这份精神穿越时空,成为新时代青年砥砺前行的力量。"
|
||||
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_55.mp3')}
|
||||
onClose={() => {
|
||||
setShowNarration(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 对话引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
audioChannelId="sunzhongshan-internalization-guide-pre"
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible)
|
||||
if (!visible) {
|
||||
setShowVideo(true)
|
||||
handleVideoCanPlay()
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
>
|
||||
<GuideStepsItem
|
||||
speaker="student"
|
||||
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_56.mp3')}
|
||||
>
|
||||
小沪,参观完故居,我对孙中山先生从“医人”到“医国”的转变、《实业计划》里的宏大构想有了很多感触,但总觉得这些历史和精神离我们的生活还有些远,不知道该怎么真正理解和传承。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_57.mp3')}
|
||||
>
|
||||
你的感受特别真实!孙中山先生的精神不是停留在书本里的文字,而是能指导我们当下行动的思想财富。今天我们特别邀请到了上海孙中山故居纪念馆研究部主任孙旻老师,让她带我们一起解读先生精神的时代意义,把这份传承落到实处。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 视频结束后的引导 */}
|
||||
<GuideSteps
|
||||
visible={postGuideVisible}
|
||||
audioChannelId="sunzhongshan-internalization-guide-post"
|
||||
onVisibleChange={(visible) => {
|
||||
setPostGuideVisible(visible)
|
||||
// 引导结束后显示信件
|
||||
if (!visible) {
|
||||
setShowLetter(true)
|
||||
}
|
||||
}}
|
||||
showMask={false}
|
||||
zIndex={60}
|
||||
>
|
||||
<GuideStepsItem
|
||||
speaker="guide"
|
||||
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_58.mp3')}
|
||||
>
|
||||
再看香山路7号这座小楼,相信你有很多心里话想对孙中山先生说。现在,就请你把这份感悟浓缩成几句话,既呼应先生的精神,也给自己未来许下一个承诺吧!
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 信件组件:视频引导结束后弹出,带飞入和放大动画 */}
|
||||
<LetterToSunzhongshan
|
||||
visible={showLetter}
|
||||
onClose={() => {
|
||||
setShowLetter(false)
|
||||
}}
|
||||
onAnimationEnd={() => {
|
||||
setShowStudyGuide(true)
|
||||
}}
|
||||
showVideo={true}
|
||||
/>
|
||||
|
||||
<EndingOverlay
|
||||
visible={showStudyGuide}
|
||||
text="恭喜你圆满完成孙中山故居纪念馆的学习之旅,请带着研学感悟,前往鲁迅故居继续探访吧~"
|
||||
audioSrc={getPublicPath('audio/sunzhongshan/mp3_sunzhongshan_59.mp3')}
|
||||
animationSpeed={200}
|
||||
onContinue={() => {
|
||||
setShowStudyGuide(false)
|
||||
setCourseTimeProgress(6)
|
||||
navigate('/menu')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SunzhongshanInternalization
|
||||
Reference in new issue
Block a user