feat: 更新拖拽题功能和图像资源,提升学习体验

- 在 DragQuizPanel 组件中添加空白分隔符功能,增强用户交互性。
- 更新多个场景中的图像资源,确保视觉内容的准确性和一致性。
- 调整引导对话文本,提升信息传达的清晰度。
- 引入新的拖拽题逻辑,优化用户填写体验。
This commit is contained in:
zhongyidian committed 2026-01-18 16:45:06 +08:00
1 parent 9184c41cc7
commit cf64e7ec50
25 files changed
+726 -241

No files matched your search

+2 -2
View File
@@ -317,13 +317,13 @@ const LetterToLuxun = ({
</div>
{/* 寄信图标(可选,如果需要的话) */}
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
{/* <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>
{/* 背面:只读态 */}
+219 -100
View File
@@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image } from "@/components";
import { Image, Select } from "@/components";
import { ErrorAlert } from "@/components/layout";
export interface LetterToSunzhongshanProps {
/** 是否可见 */
@@ -29,7 +30,25 @@ const LetterToSunzhongshan = ({
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
// 校验输入:不能为空、每个不超过 10 个字符
// ErrorAlert 相关状态
const [showErrorAlert, setShowErrorAlert] = useState(false);
const [errorAlertText, setErrorAlertText] = useState("");
// 前三个空的正确答案
const BLANK_0_ANSWER = "振兴中华";
const BLANK_1_ANSWER = "天下为公";
const BLANK_2_ANSWER = "与时俱进";
// 最后一个空的正确答案
const LAST_BLANK_ANSWER = "仍须努力";
// 前三个空的选项(所有空都用这三个选项)
const SELECT_OPTIONS = [
{ value: "振兴中华", label: "振兴中华" },
{ value: "天下为公", label: "天下为公" },
{ value: "与时俱进", label: "与时俱进" },
];
// 校验输入:前三个空必须选择正确答案,最后一个空必须是"仍须努力",其他空不能为空且不超过 10 个字符
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
@@ -42,11 +61,27 @@ const LetterToSunzhongshan = ({
const raw = blankValues[i] || "";
const v = raw.trim();
// 为空 或 超过 10 个字,都视为不符合要求:清空并标红
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
if (i < 3) {
// 前三个空:必须选择正确答案(已在 onChange 中验证,这里只检查是否为空)
if (!v) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
} else if (i === placeholderCount - 1) {
// 最后一个空:必须是"仍须努力"
if (!v || v !== LAST_BLANK_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
} else {
// 其他空:不能为空,不超过 10 个字
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
}
}
@@ -62,6 +97,8 @@ const LetterToSunzhongshan = ({
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
setShowErrorAlert(false);
setErrorAlertText("");
}
}, [visible]);
@@ -70,11 +107,11 @@ const LetterToSunzhongshan = ({
您好!
今天,我怀着崇敬的心情走进香山路7号,看着您亲手设计的房屋、勾画的《中国铁路全图》、撰写的《实业计划》手稿,仿佛能看到您为"振兴中华"奔走的身影。在李教授的解读中,我终于明白:您喊出的"____",不是遥远的历史,而是我们这代人正在接续的接力;您追求的"____(空里填天下为公)",不是抽象的理念,而是从服务身边人、做好专业事开始的担当;您秉持的"____(空里填与时俱进)",不是过去的选择,而是面对未来时必须有的勇气。
今天,我怀着崇敬的心情走进香山路7号,看着您亲手设计的房屋、勾画的《中国铁路全图》、撰写的《实业计划》手稿,仿佛能看到您为“振兴中华”奔走的身影。在这场跨越时空的对话中,我终于明白,您喊出的“____”,不是遥远的历史,而是我们这代人正在接续的接力;您追求的“____”,不是抽象的理念,而是从服务身边人、做好专业事开始的担当;您秉持的“____”,不是过去的选择,而是面对未来时必须有的勇气。
我是____专业的学生,今后,我会以您"知难行易"的信念,在专业学习中攻克难题,就像您当年为了革命理想屡败屡战;我会以您"心系民众"的情怀,参与____,帮身边人解决实际困难;我更会记住您"适乎世界之潮流"的智慧,紧跟时代步伐,努力用____为民族复兴添砖加瓦。
我是____专业的学生,今后,我会以您“知难行易”的信念,在专业学习中攻克难题,就像您当年为了革命理想屡败屡战;我会以您“心系民众”的情怀,参与____,帮身边人解决实际困难;我更会记住您“适乎世界之潮流”的智慧,紧跟时代步伐,努力用____为民族复兴添砖加瓦。
请您放心,"革命尚未成功,同志仍须努力"的嘱托,我们从未忘记;"振兴中华"的初心,我们会用一生践行!`;
请您放心,“革命尚未成功,同志____”的嘱托,我们从未忘记;“振兴中华”的初心,我们会用一生践行!`;
return (
<AnimatePresence>
@@ -151,66 +188,132 @@ const LetterToSunzhongshan = ({
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent.split("\n").map((line, lineIndex) => {
// 检测是否已经找到"您好"行
if (line.includes("您好")) {
foundNiHao = true;
}
return letterContent
.split("\n")
.map((line, lineIndex) => {
// 检测是否已经找到"您好"行
if (line.includes("您好")) {
foundNiHao = true;
}
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
// "您好"行之后的非空行需要首行缩进
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 isSelectFill = currentIndex < 3; // 前三个空使用 Select
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}`}>
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{isSelectFill ? (
// 前三个空:下拉选择
<Select
className="inline"
options={SELECT_OPTIONS}
value={
blankValues[currentIndex] ?? ""
}
onChange={(selectedValue) => {
const next = [...blankValues];
next[currentIndex] = selectedValue;
setBlankValues(next);
// 清除错误状态
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
// 立即验证选择是否正确
let correctAnswer = "";
if (currentIndex === 0) {
correctAnswer = BLANK_0_ANSWER;
} else if (currentIndex === 1) {
correctAnswer = BLANK_1_ANSWER;
} else if (currentIndex === 2) {
correctAnswer = BLANK_2_ANSWER;
}
if (
correctAnswer &&
selectedValue !== correctAnswer
) {
// 选择错误,显示错误提示
setErrorAlertText(
"请选择正确答案"
);
setShowErrorAlert(true);
// 清空选择
next[currentIndex] = "";
setBlankValues(next);
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = true;
setBlankErrors(nextErrs);
}
}}
placeholder="请选择"
error={blankErrors[currentIndex]}
zIndex={10000}
fitContent={true}
/>
) : (
// 其他空:文本输入
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[110px] 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>
);
}
const currentIndex = placeholderIndex++;
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-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>
@@ -238,13 +341,13 @@ const LetterToSunzhongshan = ({
</div>
{/* 寄信图标(可选,如果需要的话) */}
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
{/* <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>
{/* 背面:只读态 */}
@@ -279,46 +382,53 @@ const LetterToSunzhongshan = ({
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent.split("\n").map((line, lineIndex) => {
if (line.includes("您好")) {
foundNiHao = true;
}
return letterContent
.split("\n")
.map((line, lineIndex) => {
if (line.includes("您好")) {
foundNiHao = true;
}
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
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] ?? "";
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}`}>
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{value || "  "}
</span>
);
}
const currentIndex = placeholderIndex++;
const value = blankValues[currentIndex] ?? "";
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
{value || "  "}
</span>
);
})}
</div>
);
});
})}
</div>
);
});
})()}
</div>
</div>
@@ -338,6 +448,15 @@ const LetterToSunzhongshan = ({
</motion.div>
</div>
</motion.div>
{/* 错误提示弹窗 */}
<ErrorAlert
visible={showErrorAlert}
text={errorAlertText}
onClose={() => setShowErrorAlert(false)}
onMaskClick={() => setShowErrorAlert(false)}
zIndex={10002}
/>
</>
)}
</AnimatePresence>