feat: 更新光标组件和场景交互

- 在 Cursor 组件中添加禁用状态,增强用户交互体验。
- 更新多个场景以使用新的光标控制逻辑,确保光标的启用状态与展品访问状态相匹配。
- 在信件组件中添加输入校验功能,提升用户填写体验。
- 调整文本内容和引导对话,确保信息准确性和一致性。
This commit is contained in:
zhongyidian committed 2026-01-14 18:40:52 +08:00
1 parent a6570efa19
commit d7ff7d3a45
25 files changed
+769 -158

No files matched your search

+5 -4
View File
@@ -78,7 +78,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
},
{
id: "quiz-3",
time: Math.ceil(16.04 * 60),
time: 16 * 60 + 26, //16:26
title: "题目3",
prompt: "新时代的青年可以从陈云身上学习到",
type: "drag",
@@ -86,7 +86,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
{ value: "积极的人生态度", label: "A.积极的人生态度", isCorrect: true },
{ value: "刻苦的学习精神", label: "B.刻苦的学习精神", isCorrect: true },
{ value: "朴素的节俭精神", label: "C.朴素的节俭精神", isCorrect: true },
{ value: "实事求是的精神", label: "D.实事求是的精神" },
{ value: "只唯上的工作作风", label: "D.只唯上的工作作风" },
],
blanks: [
{ id: "blank-1", label: "____" },
@@ -180,7 +180,8 @@ const ChenyunInternalization = () => {
} else {
setDragQuizErrors((prev) => ({
...prev,
[item.id]: "再想一想,确保选择了所有正确答案~",
[item.id]:
"再想一想,不唯上才是陈云的工作作风,不是只唯上!",
}));
}
};
@@ -191,7 +192,7 @@ const ChenyunInternalization = () => {
visible={activeSuccess !== item.id}
zIndex={10000}
questionPrefix={`【拖拽】${item.prompt}`}
questionSuffix="等精神。(ABC)"
questionSuffix="等精神。"
options={item.options.map((opt) => ({
value: opt.value,
label: opt.label,
+15 -1
View File
@@ -9,10 +9,11 @@ import {
} from "@/components";
import { useNavigate, useParams } from "react-router-dom";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useState } from "react";
import { useState, useMemo } from "react";
import Cursor from "@/components/cursor";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useChenyunStore } from "@/store/chenyunStore";
import { useCursorController } from "@/hooks/useCursorController";
// starName 固定为 chenyun
const starName = "chenyun";
@@ -53,6 +54,16 @@ const ChenyunPlace1 = () => {
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
// Cursor 控制器:使用依赖映射实现自动解锁
// cursor 1 初始启用,cursor 2 依赖展品1(当展品1被访问后自动解锁)
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: {
2: [1], // cursor 2 依赖展品 1,当展品1被访问后自动解锁
},
visited, // 传入数组,而不是 Set
});
const handleOpenExhibit = (index: 1 | 2) => {
setActiveExhibitId(index);
// ExhibitOverlay 打开时会自动通过 onOpen 通知 ResidenceLayout 执行动画
@@ -104,12 +115,14 @@ const ChenyunPlace1 = () => {
className="absolute left-145 top-100"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 展品2 */}
<Cursor
className="absolute left-198 top-100"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 展品3 */}
{/* <Cursor
@@ -150,6 +163,7 @@ const ChenyunPlace1 = () => {
onClose={() => {
if (activeExhibitId !== null) {
markExhibitVisited(place, activeExhibitId);
// cursor 状态会根据 visited 自动更新,无需手动调用 enable
}
setActiveExhibitId(null);
}}
+9 -5
View File
@@ -11,6 +11,7 @@ import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController";
// starName 固定为 chenyun
const starName = "chenyun";
@@ -21,11 +22,7 @@ const exhibitSrcMap: Record<1 | 2, string> = {
2: "chenyun/place2/场景2展品2",
};
// 展品描述映射
const exhibitDescriptionMap: Record<1 | 2, string> = {
1: "",
2: "同学你看,这里有一幅陈云提写的颜安小学。陈云在青浦县乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。颜安小学实行的新式教育开阔了陈云的视野,五四运动后,颜安小学师生积极投入反帝爱国运动,为陈云日后加入中国共产党埋下了思想的种子。",
};
// 展品描述映射(当前未使用,若后续需要可恢复)
const ChenyunPlace2 = () => {
const navigate = useNavigate();
const { moduleName } = useParams<{
@@ -45,6 +42,11 @@ const ChenyunPlace2 = () => {
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
});
// GuideSteps 显示状态
const [guideVisible, setGuideVisible] = useState(false);
// GuideSteps 当前步骤
@@ -114,12 +116,14 @@ const ChenyunPlace2 = () => {
className="absolute left-100 top-100"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 展品2 */}
<Cursor
className="absolute left-210 top-100"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
+8
View File
@@ -12,6 +12,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController";
// starName 固定为 chenyun
const starName = "chenyun";
@@ -39,6 +40,11 @@ const ChenyunPlace3 = () => {
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
});
const [videoVisible, setVideoVisible] = useState(false);
// GuideSteps 显示状态
const [guideVisible, setGuideVisible] = useState(false);
@@ -103,12 +109,14 @@ const ChenyunPlace3 = () => {
className="absolute right-200 bottom-75"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 展品2 */}
<Cursor
className="absolute right-130 bottom-75"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
+8
View File
@@ -11,6 +11,7 @@ import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController";
// starName 固定为 chenyun
const starName = "chenyun";
@@ -39,6 +40,11 @@ const ChenyunPlace4 = () => {
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
});
// 展品1引导对话
const [guideVisible, setGuideVisible] = useState(false);
const [guideStep, setGuideStep] = useState(0);
@@ -110,12 +116,14 @@ const ChenyunPlace4 = () => {
className="absolute left-180 top-120"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 展品2 */}
<Cursor
className="absolute right-2 top-100"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
+9
View File
@@ -12,6 +12,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController";
// starName 固定为 chenyun
const starName = "chenyun";
@@ -49,6 +50,11 @@ const ChenyunPlace5 = () => {
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [1, 2],
dependencies: { 3: [1, 2] },
visited,
});
const [videoVisible3, setVideoVisible3] = useState(false);
// 展品1引导对话
@@ -109,12 +115,14 @@ const ChenyunPlace5 = () => {
className="absolute left-30 top-70"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 展品2 */}
<Cursor
className="absolute left-107 top-72"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 展品3 */}
@@ -122,6 +130,7 @@ const ChenyunPlace5 = () => {
className="absolute right-170 top-84"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
+9 -1
View File
@@ -11,6 +11,7 @@ import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useChenyunStore } from "@/store/chenyunStore";
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
import { useCursorController } from "@/hooks/useCursorController";
// starName 固定为 chenyun
const starName = "chenyun";
@@ -39,6 +40,11 @@ const ChenyunPlace8 = () => {
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
const visitedExhibits = getVisitedExhibits(place);
const visited = new Set(visitedExhibits);
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
});
// 展品2的对话引导
const [guideVisible, setGuideVisible] = useState(false);
const [guideStep, setGuideStep] = useState(0);
@@ -109,12 +115,14 @@ const ChenyunPlace8 = () => {
className="absolute right-175 top-75"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 展品2 */}
<Cursor
className="absolute right-100 bottom-55"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 继续前进 - 仅在完成所有展品后显示 */}
@@ -185,7 +193,7 @@ const ChenyunPlace8 = () => {
zIndex={10001}
>
<GuideStepsItem speaker="student">
您讲的"统购统销"打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗?
打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗?
</GuideStepsItem>
<GuideStepsItem speaker="guide">
一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。
+2 -2
View File
@@ -161,10 +161,10 @@ const ChenyunPlace9 = () => {
我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他"适合老百姓的要求,做了一点事"的初心。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去,在实践决策中体会。
接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去。
</GuideStepsItem>
<GuideStepsItem speaker="guide">
陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。
在实践决策中体会陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。
</GuideStepsItem>
</GuideSteps>
@@ -35,7 +35,14 @@ export const MimianFile = ({
])}
onClick={onFileClick}
>
<Image src={getStaticImage("chenyun/reflection/文件2")} />
<Image
className="transition-all duration-300"
src={
expandedFile
? getStaticImage("chenyun/reflection/文件2打开")
: getStaticImage("chenyun/reflection/文件2")
}
/>
<span
className={cn([
"absolute left-0 bottom-1/2",
@@ -48,4 +55,3 @@ export const MimianFile = ({
</div>
);
};
@@ -98,7 +98,7 @@ export const MimianQuizzes = ({
{/* 米棉之战 - 单选题1(图1) */}
<SingleChoiceQuizPanel
visible={showQuiz1}
title={`(单选)投机商集中冲击"两白一黑"的核心目的是(\xa0\xa0\xa0\xa0)。`}
title={`【单选】投机商集中冲击"两白一黑"的核心目的是(\xa0\xa0\xa0\xa0)。`}
options={[
{ value: "A", label: "A. 满足市场需求" },
{ value: "B", label: "B. 扰乱经济秩序、动摇政权" },
@@ -126,7 +126,7 @@ export const MimianQuizzes = ({
{/* 米棉之战 - 单选题2(图3) */}
<SingleChoiceQuizPanel
visible={showQuiz2}
title={`(单选)你觉得此时解决上海和全国面临的严峻经济形势问题的对策应该是(\xa0\xa0\xa0\xa0)。`}
title={`【单选】你觉得此时解决上海和全国面临的严峻经济形势问题的对策应该是(\xa0\xa0\xa0\xa0)。`}
options={[
{ value: "A", label: "A. 局部限购" },
{ value: "B", label: "B. 增发货币" },
@@ -174,7 +174,7 @@ export const MimianQuizzes = ({
{/* 米棉之战 - 单选题3(图5) */}
<SingleChoiceQuizPanel
visible={showQuiz3}
title='(单选题)某地方粮站反馈"本地冬储粮不足,不愿外运"。'
title='【单选】某地方粮站反馈"本地冬储粮不足,不愿外运"。'
options={[
{ value: "A", label: "A. 强制调运,否则追责" },
{
@@ -209,7 +209,7 @@ export const MimianQuizzes = ({
{/* 米棉之战 - 单选题4(图6) */}
<SingleChoiceQuizPanel
visible={showQuiz4}
title="(单选题)某地方棉农担心外运后本地纱厂缺原料。"
title="【单选】某地方棉农担心外运后本地纱厂缺原料。"
options={[
{ value: "A", label: "A. 全国优先上海,本地暂时忍耐" },
{
@@ -252,7 +252,7 @@ export const MimianQuizzes = ({
{/* 米棉之战 - 单选题5(图8) */}
<SingleChoiceQuizPanel
visible={showQuiz5}
title="(单选题)此时,以下哪种举措能够一举获得胜利?"
title="【单选】此时,以下哪种举措能够一举获得胜利?"
options={[
{
value: "A",
@@ -60,7 +60,14 @@ export const YinyuanFile = ({
])}
onClick={onFileClick}
>
<Image src={getStaticImage("chenyun/reflection/文件1")} />
<Image
className="transition-all duration-300"
src={
expandedFile
? getStaticImage("chenyun/reflection/文件1打开")
: getStaticImage("chenyun/reflection/文件1")
}
/>
<span
className={cn([
"absolute left-0 bottom-1/2 z-10",
@@ -108,4 +115,3 @@ export const YinyuanFile = ({
</div>
);
};
@@ -46,7 +46,7 @@ export const YinyuanQuizzes = ({
{/* 多选题1 - 点击引导图标后显示 */}
<MultipleChoiceQuizPanel
visible={showQuiz1}
title="(多选题)面对银元投机狂潮,你认为首要突破口是? "
title="【多选】面对银元投机狂潮,你认为首要突破口是? "
options={[
{ value: "A", label: "A. 政治攻势(警告、约谈)" },
{ value: "B", label: "B. 经济手段(抛售银元,以银元打银元)" },
@@ -75,7 +75,7 @@ export const YinyuanQuizzes = ({
{/* 多选题2(图3)- 关闭旁白后显示 */}
<MultipleChoiceQuizPanel
visible={showQuiz2}
title="(多选题)面对投机商的猖狂反扑,下一步应采取哪些决定性措施?"
title="【多选】面对投机商的猖狂反扑,下一步应采取哪些决定性措施?"
options={[
{ value: "A", label: "A. 加大银元抛售量,耗尽投机资本" },
{ value: "B", label: "B. 查封投机核心场所,逮捕首要分子" },
+8
View File
@@ -9,6 +9,7 @@ import {
} from "@/components";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useCursorController } from "@/hooks/useCursorController";
const starName = "huangyanpei";
@@ -25,6 +26,11 @@ const HuangyanpeiPlace2 = () => {
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
const [visited, setVisited] = useState<Set<number>>(new Set());
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
});
// GuideSteps 当前步骤
const [guideStep, setGuideStep] = useState(0);
@@ -87,11 +93,13 @@ const HuangyanpeiPlace2 = () => {
className="absolute left-132 top-132"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
<Cursor
className="absolute left-240 top-132"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{completed && (
+9
View File
@@ -11,6 +11,7 @@ import {
import { SuccessAlert } from "@/components/layout";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import { useCursorController } from "@/hooks/useCursorController";
const starName = "huangyanpei";
@@ -30,6 +31,11 @@ const HuangyanpeiPlace3 = () => {
null
);
const [visited, setVisited] = useState<Set<number>>(new Set());
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1], 3: [1, 2] },
visited,
});
// 光标3的引导对话状态
const [guide3Visible, setGuide3Visible] = useState(false);
@@ -121,18 +127,21 @@ const HuangyanpeiPlace3 = () => {
className="absolute left-32 top-105"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute left-172 top-90"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{/* 光标3 */}
<Cursor
className="absolute right-92 top-108"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{completed && (
+15
View File
@@ -13,6 +13,7 @@ import { SuccessAlert } from "@/components/layout";
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import type { VideoRef } from "@/components/video";
import { useCursorController } from "@/hooks/useCursorController";
const starName = "huangyanpei";
@@ -34,6 +35,16 @@ const HuangyanpeiPlace4 = () => {
1 | 2 | 3 | 4 | 5 | null
>(null);
const [visited, setVisited] = useState<Set<number>>(new Set());
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: {
3: [1],
4: [3],
5: [4],
// 2 已注释,不计入顺序
},
visited,
});
// 光标1的引导对话状态
const [guide1Visible, setGuide1Visible] = useState(false);
@@ -125,6 +136,7 @@ const HuangyanpeiPlace4 = () => {
onClick={() => handleOpenExhibit(1)}
size={1}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
{/* <Cursor
@@ -137,18 +149,21 @@ const HuangyanpeiPlace4 = () => {
className="absolute left-125 top-100"
onClick={() => handleOpenExhibit(3)}
read={visited.has(3)}
disabled={!isEnabled(3)}
/>
{/* 光标4 */}
<Cursor
className="absolute right-62 top-102"
onClick={() => handleOpenExhibit(4)}
read={visited.has(4)}
disabled={!isEnabled(4)}
/>
{/* 光标5 */}
<Cursor
className="absolute right-205 top-110"
onClick={() => handleOpenExhibit(5)}
read={visited.has(5)}
disabled={!isEnabled(5)}
/>
{completed && (
+10
View File
@@ -13,6 +13,7 @@ import { SuccessAlert } from "@/components/layout";
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
import { useNavigate, useParams } from "react-router-dom";
import type { VideoRef } from "@/components/video";
import { useCursorController } from "@/hooks/useCursorController";
const starName = "huangyanpei";
@@ -42,6 +43,13 @@ const HuangyanpeiPlace5 = () => {
// 成功提示状态
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
// 光标顺序:1 -> 2(需完成1后解锁2)
const { isEnabled } = useCursorController({
initialEnabled: [1],
dependencies: { 2: [1] },
visited,
});
const handleOpenExhibit = (index: 1 | 2) => {
if (index === 2) {
// 展品2:点击时显示对话,再显示视频
@@ -101,12 +109,14 @@ const HuangyanpeiPlace5 = () => {
className="absolute left-35 top-108"
onClick={() => handleOpenExhibit(1)}
read={visited.has(1)}
disabled={!isEnabled(1)}
/>
{/* 光标2 */}
<Cursor
className="absolute right-112 top-108"
onClick={() => handleOpenExhibit(2)}
read={visited.has(2)}
disabled={!isEnabled(2)}
/>
{completed && (