feat: 更新拖拽题功能和图像资源,提升学习体验
- 在 DragQuizPanel 组件中添加空白分隔符功能,增强用户交互性。 - 更新多个场景中的图像资源,确保视觉内容的准确性和一致性。 - 调整引导对话文本,提升信息传达的清晰度。 - 引入新的拖拽题逻辑,优化用户填写体验。
This commit is contained in:
1 parent
9184c41cc7
commit
cf64e7ec50
25 files changed
+726
-241
No files matched your search
@@ -5,12 +5,13 @@ import {
|
||||
Video,
|
||||
type VideoAnchor,
|
||||
type VideoRef,
|
||||
type DragFillResult,
|
||||
LetterToLuxun,
|
||||
StudyGuideModal,
|
||||
Narration,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage } from "@/lib/utils";
|
||||
import { SuccessAlert } from "@/components/layout";
|
||||
import { SuccessAlert, DragQuizPanel } from "@/components/layout";
|
||||
import {
|
||||
MultipleChoiceQuizPanel,
|
||||
SingleChoiceQuizPanel,
|
||||
@@ -29,8 +30,16 @@ type QuizAnchor = {
|
||||
time: number;
|
||||
title: string;
|
||||
prompt: string;
|
||||
type: "single" | "multiple";
|
||||
// 拖拽题专用:题干后缀(空白区域之后的文本)
|
||||
promptSuffix?: string;
|
||||
type: "single" | "multiple" | "drag";
|
||||
options: QuizOption[];
|
||||
// 拖拽题专用
|
||||
blanks?: { id: string; label: string }[];
|
||||
// 拖拽题专用:空白之间的分隔符数组
|
||||
blankSeparators?: string[];
|
||||
// 拖拽题正确答案映射:blankId -> optionValue
|
||||
correctAnswersMap?: Record<string, string>;
|
||||
explanation?: string;
|
||||
};
|
||||
|
||||
@@ -40,20 +49,40 @@ const starName = "luxun";
|
||||
const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
{
|
||||
id: "quiz-1",
|
||||
time: 155, // 2:35
|
||||
time: 60 + 41, // 1:41
|
||||
title: "",
|
||||
prompt: "【多选】以下属于鲁迅先生作品的是( )。",
|
||||
type: "multiple",
|
||||
options: [
|
||||
{ value: "A", label: "孔乙己", isCorrect: true },
|
||||
{ value: "B", label: "药", isCorrect: true },
|
||||
{ value: "C", label: "狂人日记", isCorrect: true },
|
||||
{ value: "D", label: "阿Q正传", isCorrect: true },
|
||||
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: 355, // 5:55
|
||||
time: 60 * 5 + 2, // 5:02
|
||||
title: "",
|
||||
prompt: "【多选】对于鼓励和培养青年一代,鲁迅先生的做法是( )。",
|
||||
type: "multiple",
|
||||
@@ -72,7 +101,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
},
|
||||
{
|
||||
id: "quiz-3",
|
||||
time: 420, // 7:00
|
||||
time: 60 * 6 + 39, // 6:39
|
||||
title: "",
|
||||
prompt: "【单选】以下那句诗出自鲁迅的作品( )。",
|
||||
type: "single",
|
||||
@@ -110,7 +139,14 @@ const LuxunInternalization = () => {
|
||||
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(
|
||||
null
|
||||
);
|
||||
const [showNarration, setShowNarration] = useState(true); // 是否显示旁白
|
||||
// 拖拽题状态
|
||||
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 [postGuideVisible, setPostGuideVisible] = useState(false);
|
||||
@@ -151,6 +187,108 @@ const LuxunInternalization = () => {
|
||||
.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 || []}
|
||||
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 {
|
||||
@@ -272,7 +410,13 @@ const LuxunInternalization = () => {
|
||||
render: () => null,
|
||||
};
|
||||
}),
|
||||
[activeSuccess, resumeCallback, successContent]
|
||||
[
|
||||
activeSuccess,
|
||||
resumeCallback,
|
||||
successContent,
|
||||
dragQuizValues,
|
||||
dragQuizErrors,
|
||||
]
|
||||
);
|
||||
|
||||
if (!moduleName) return null;
|
||||
@@ -343,7 +487,7 @@ const LuxunInternalization = () => {
|
||||
showMask={false}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
小沪,关于"首在立人"走进上海鲁迅故居的红砖小楼,解锁了先生从绍兴到上海的文学足迹,我们读懂了他以笔为刃的战斗姿态,也见证了他与进步人士的革命情谊。
|
||||
关于"首在立人"走进上海鲁迅故居的红砖小楼,解锁了先生从绍兴到上海的文学足迹,我们读懂了他以笔为刃的战斗姿态,也见证了他与进步人士的革命情谊。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
那些针砭时弊的杂文、借古讽今的篇章,还有藏在6平方米客房里的红色坚守,都在诉说着先生的热忱。
|
||||
|
||||
Reference in new issue
Block a user