feat: 更新光标组件和场景交互
- 在 Cursor 组件中添加禁用状态,增强用户交互体验。 - 更新多个场景以使用新的光标控制逻辑,确保光标的启用状态与展品访问状态相匹配。 - 在信件组件中添加输入校验功能,提升用户填写体验。 - 调整文本内容和引导对话,确保信息准确性和一致性。
This commit is contained in:
1 parent
a6570efa19
commit
d7ff7d3a45
25 files changed
+769
-158
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -6,44 +6,62 @@ const ORIGINAL_SIZES = {
|
|||||||
unread: { width: 120, height: 121 },
|
unread: { width: 120, height: 121 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const Cursor = ({
|
interface CursorProps {
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
read = false,
|
|
||||||
size = 1,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
/** 已阅读 / 已完成 状态;true 时显示预留的 div,由你自行实现效果 */
|
/** 已阅读 / 已完成 状态;true 时显示预留的 div,由你自行实现效果 */
|
||||||
read?: boolean;
|
read?: boolean;
|
||||||
/** 尺寸缩放比例,默认为 1(原始大小) */
|
/** 尺寸缩放比例,默认为 1(原始大小) */
|
||||||
size?: number;
|
size?: number;
|
||||||
}) => {
|
/** 禁用状态,禁用时无法点击但不影响 read 状态显示。当 read=true 时,disabled 不生效 */
|
||||||
// 根据 size 计算实际尺寸,保持原始比例
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Cursor = ({
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
read = false,
|
||||||
|
size = 1,
|
||||||
|
disabled = false,
|
||||||
|
}: CursorProps) => {
|
||||||
const baseSize = read ? ORIGINAL_SIZES.read : ORIGINAL_SIZES.unread;
|
const baseSize = read ? ORIGINAL_SIZES.read : ORIGINAL_SIZES.unread;
|
||||||
const width = baseSize.width * size;
|
// 当 read=true 时,disabled 不生效;只有 read=false 且 disabled=true 时才禁用
|
||||||
const height = baseSize.height * size;
|
const isDisabled = disabled && !read;
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer select-none",
|
"select-none flex justify-center items-center pl-0 transition-all duration-300",
|
||||||
read ? "" : "animate-[spin_3s_linear_infinite]",
|
isDisabled
|
||||||
"flex justify-center items-center pl-0",
|
? "opacity-50"
|
||||||
"transition-all duration-300 hover:scale-105 hover:brightness-110 hover:contrast-110 active:scale-95",
|
: "hover:scale-105 hover:brightness-110 hover:contrast-110 active:scale-95",
|
||||||
|
!read && "animate-[spin_3s_linear_infinite]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onTouchEnd={(e) => {
|
|
||||||
e.preventDefault(); // 防止 Safari 触发两次
|
|
||||||
onClick();
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
width: `${width}px`,
|
width: `${baseSize.width * size}px`,
|
||||||
height: `${height}px`,
|
height: `${baseSize.height * size}px`,
|
||||||
WebkitTapHighlightColor: "transparent",
|
WebkitTapHighlightColor: "transparent",
|
||||||
WebkitTouchCallout: "none",
|
WebkitTouchCallout: "none",
|
||||||
WebkitUserSelect: "none",
|
WebkitUserSelect: "none",
|
||||||
touchAction: "manipulation",
|
touchAction: isDisabled ? "none" : "manipulation",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||||
backgroundImage: getBackgroundImage(
|
backgroundImage: getBackgroundImage(
|
||||||
read ? "components/point_" : "components/border"
|
read ? "components/point_" : "components/border"
|
||||||
),
|
),
|
||||||
@@ -51,29 +69,11 @@ const Cursor = ({
|
|||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: "no-repeat",
|
||||||
}}
|
}}
|
||||||
onClick={onClick}
|
onClick={handleClick}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
>
|
>
|
||||||
{read ? (
|
{!read && (
|
||||||
// 已阅读 / 已完成 状态
|
|
||||||
// <div
|
|
||||||
// className="absolute -bottom-1 text-[#cf9c4d] text-2xl font-bold"
|
|
||||||
// style={{
|
|
||||||
// // 细一点的描边,避免把内部金色填充盖掉
|
|
||||||
// // WebkitTextStroke: "0.5px #ffffff",
|
|
||||||
// // 用多层白色阴影模拟更厚的外轮廓
|
|
||||||
// textShadow: [
|
|
||||||
// "0 0 2px #ffffff",
|
|
||||||
// "0 0 4px #ffffff",
|
|
||||||
// "0 0 6px rgba(0,0,0,0.35)",
|
|
||||||
// ].join(", "),
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// 已浏览
|
|
||||||
// </div>
|
|
||||||
<></>
|
|
||||||
) : (
|
|
||||||
// 未阅读 / 未完成 状态:保持当前效果
|
|
||||||
<img
|
<img
|
||||||
src={getStaticImage("components/point.png")}
|
src={getStaticImage("components/point.png")}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -82,6 +82,7 @@ const Cursor = ({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
objectFit: "contain",
|
objectFit: "contain",
|
||||||
objectPosition: "center",
|
objectPosition: "center",
|
||||||
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { getStaticImage } from "@/lib/utils";
|
import { getStaticImage } from "@/lib/utils";
|
||||||
import { Image } from "@/components";
|
import { Image } from "@/components";
|
||||||
@@ -23,6 +23,47 @@ const LetterToChenyun = ({
|
|||||||
}: LetterToChenyunProps) => {
|
}: LetterToChenyunProps) => {
|
||||||
// 下划线可填写内容
|
// 下划线可填写内容
|
||||||
const [blankValues, setBlankValues] = useState<string[]>([]);
|
const [blankValues, setBlankValues] = useState<string[]>([]);
|
||||||
|
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
|
||||||
|
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
|
||||||
|
|
||||||
|
// 是否已提交:提交后隐藏编辑信件,展示只读信件
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
// 校验输入:不能为空、每个不超过 10 个字符
|
||||||
|
const validateBlanks = () => {
|
||||||
|
// 计算占位总数,防止有些没被输入过而是 undefined
|
||||||
|
const placeholderCount = (letterContent.match(/____/g) || []).length;
|
||||||
|
|
||||||
|
const nextErrors: boolean[] = Array(placeholderCount).fill(false);
|
||||||
|
const nextValues: string[] = [...blankValues];
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < placeholderCount; i++) {
|
||||||
|
const raw = blankValues[i] || "";
|
||||||
|
const v = raw.trim();
|
||||||
|
|
||||||
|
// 为空 或 超过 10 个字,都视为不符合要求:清空并标红
|
||||||
|
if (!v || v.length > 10) {
|
||||||
|
nextValues[i] = "";
|
||||||
|
nextErrors[i] = true;
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlankValues(nextValues);
|
||||||
|
setBlankErrors(nextErrors);
|
||||||
|
|
||||||
|
return !hasError;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 弹窗关闭后重置内容,方便下次重新填写
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
setSubmitted(false);
|
||||||
|
setBlankValues([]);
|
||||||
|
setBlankErrors([]);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
// 信件内容模板(根据设计稿)
|
// 信件内容模板(根据设计稿)
|
||||||
const letterContent = `敬爱的陈云爷爷:
|
const letterContent = `敬爱的陈云爷爷:
|
||||||
@@ -64,112 +105,241 @@ const LetterToChenyun = ({
|
|||||||
<div
|
<div
|
||||||
className="relative w-[1200px] h-full pointer-events-auto"
|
className="relative w-[1200px] h-full pointer-events-auto"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{ perspective: "1200px" }}
|
||||||
>
|
>
|
||||||
{/* 信纸背景 */}
|
{/* 编辑态 / 只读态:Y 轴翻转切换 */}
|
||||||
<img
|
<motion.div
|
||||||
src={getStaticImage("components/letter/信底")}
|
className="relative w-full h-full"
|
||||||
alt="信纸"
|
initial={false}
|
||||||
className="absolute inset-0 w-full h-full object-contain"
|
animate={{ rotateY: submitted ? 180 : 0 }}
|
||||||
/>
|
transition={{ duration: 0.7, ease: "easeInOut" }}
|
||||||
|
style={{ transformStyle: "preserve-3d" }}
|
||||||
{/* 标题 - 垂直显示在左侧 */}
|
>
|
||||||
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
|
{/* 前面:编辑态 */}
|
||||||
<div
|
<div
|
||||||
className="text-white text-6xl font-bold tracking-[20px]"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
writingMode: "vertical-rl",
|
backfaceVisibility: "hidden",
|
||||||
textOrientation: "upright",
|
// 编辑态展示时才响应事件,避免与背面点击冲突
|
||||||
|
pointerEvents: submitted ? "none" : "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
寄给陈云爷爷的信
|
{/* 编辑态:信纸背景 */}
|
||||||
</div>
|
<img
|
||||||
</div>
|
src={getStaticImage("components/letter/信底")}
|
||||||
|
alt="信纸"
|
||||||
|
className="absolute inset-0 w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 信件内容区域 */}
|
{/* 标题 - 垂直显示在左侧 */}
|
||||||
<div className="absolute w-[700px] left-[320px] right-[180px] top-[80px] bottom-[180px] z-99">
|
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
|
||||||
<div className="text-[#4e402e] text-[26px] leading-relaxed">
|
<div
|
||||||
{(() => {
|
className="text-white text-6xl font-bold tracking-[20px]"
|
||||||
let placeholderIndex = 0;
|
style={{
|
||||||
let foundNiHao = false;
|
writingMode: "vertical-rl",
|
||||||
return letterContent.split("\n").map((line, lineIndex) => {
|
textOrientation: "upright",
|
||||||
// 检测是否已经找到"您好"行
|
}}
|
||||||
if (line.includes("您好")) {
|
>
|
||||||
foundNiHao = true;
|
寄给陈云爷爷的信
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// "您好"行之后的非空行需要首行缩进
|
{/* 信件内容区域(可编辑) */}
|
||||||
const needsIndent =
|
<div className="absolute w-[700px] left-[320px] right-[180px] top-[80px] bottom-[180px] z-99">
|
||||||
foundNiHao &&
|
<div className="text-[#4e402e] text-[26px] leading-relaxed">
|
||||||
line.trim() !== "" &&
|
{(() => {
|
||||||
!line.includes("您好");
|
let placeholderIndex = 0;
|
||||||
|
let foundNiHao = false;
|
||||||
const segments = line.split("____");
|
return letterContent
|
||||||
return (
|
.split("\n")
|
||||||
<div
|
.map((line, lineIndex) => {
|
||||||
key={lineIndex}
|
// 检测是否已经找到"您好"行
|
||||||
className="mb-6"
|
if (line.includes("您好")) {
|
||||||
style={needsIndent ? { textIndent: "2em" } : {}}
|
foundNiHao = true;
|
||||||
>
|
|
||||||
{segments.map((part, partIndex) => {
|
|
||||||
const isLast = partIndex === segments.length - 1;
|
|
||||||
if (isLast) {
|
|
||||||
return (
|
|
||||||
<span key={`part-${lineIndex}-${partIndex}`}>
|
|
||||||
{part}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = placeholderIndex++;
|
// "您好"行之后的非空行需要首行缩进
|
||||||
|
const needsIndent =
|
||||||
|
foundNiHao &&
|
||||||
|
line.trim() !== "" &&
|
||||||
|
!line.includes("您好");
|
||||||
|
|
||||||
|
const segments = line.split("____");
|
||||||
return (
|
return (
|
||||||
<span key={`part-${lineIndex}-${partIndex}`}>
|
<div
|
||||||
{part}
|
key={lineIndex}
|
||||||
<input
|
className="mb-6"
|
||||||
className="inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed border-[#4e402e] text-[24px] text-center outline-none"
|
style={needsIndent ? { textIndent: "2em" } : {}}
|
||||||
value={blankValues[currentIndex] ?? ""}
|
>
|
||||||
onChange={(e) => {
|
{segments.map((part, partIndex) => {
|
||||||
const next = [...blankValues];
|
const isLast =
|
||||||
next[currentIndex] = e.target.value;
|
partIndex === segments.length - 1;
|
||||||
setBlankValues(next);
|
if (isLast) {
|
||||||
}}
|
return (
|
||||||
/>
|
<span
|
||||||
</span>
|
key={`part-${lineIndex}-${partIndex}`}
|
||||||
|
>
|
||||||
|
{part}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = placeholderIndex++;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={`part-${lineIndex}-${partIndex}`}
|
||||||
|
>
|
||||||
|
{part}
|
||||||
|
<input
|
||||||
|
name={`blank-${currentIndex}`}
|
||||||
|
className={`inline-block w-[100px] h-[36px] mx-2 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>
|
||||||
})()}
|
|
||||||
|
{/* 笔的图片 */}
|
||||||
|
<div className="absolute bottom-[150px] -right-30">
|
||||||
|
<img
|
||||||
|
src={getStaticImage("components/letter/笔")}
|
||||||
|
alt="笔"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 提交按钮:切换到只读信件态 */}
|
||||||
|
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("components/letter/可提交状态")}
|
||||||
|
alt="可提交状态"
|
||||||
|
onClick={() => {
|
||||||
|
if (!validateBlanks()) return;
|
||||||
|
setSubmitted(true);
|
||||||
|
}}
|
||||||
|
button
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 笔的图片 */}
|
{/* 背面:只读态 */}
|
||||||
<div className="absolute bottom-[150px] -right-30">
|
<div
|
||||||
<img
|
className="absolute inset-0"
|
||||||
src={getStaticImage("components/letter/笔")}
|
style={{
|
||||||
alt="笔"
|
transform: "rotateY(180deg)",
|
||||||
className="w-full h-full object-contain"
|
backfaceVisibility: "hidden",
|
||||||
/>
|
// 只读态展示时才响应事件
|
||||||
</div>
|
pointerEvents: submitted ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 只读态:新背景 */}
|
||||||
|
<img
|
||||||
|
src={getStaticImage("components/letter/信")}
|
||||||
|
alt="信件"
|
||||||
|
className="absolute inset-0 w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("components/close")}
|
||||||
|
alt="信纸"
|
||||||
|
className="absolute right-38 top-0"
|
||||||
|
button
|
||||||
|
onClick={() => {
|
||||||
|
setSubmitted(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 提交按钮 */}
|
{/* 信件内容区域(只读) */}
|
||||||
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
|
<div className="absolute w-[700px] left-[260px] right-[180px] top-[80px] bottom-[180px] z-99">
|
||||||
<Image
|
<div className="text-[#4e402e] text-[26px] leading-relaxed">
|
||||||
src={getStaticImage("components/letter/可提交状态")}
|
{(() => {
|
||||||
alt="可提交状态"
|
let placeholderIndex = 0;
|
||||||
onClick={() => onSubmit?.(blankValues)}
|
let foundNiHao = false;
|
||||||
button
|
return letterContent
|
||||||
/>
|
.split("\n")
|
||||||
</div>
|
.map((line, lineIndex) => {
|
||||||
|
if (line.includes("您好")) {
|
||||||
|
foundNiHao = true;
|
||||||
|
}
|
||||||
|
|
||||||
{/* 寄信图标(可选,如果需要的话) */}
|
const needsIndent =
|
||||||
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
|
foundNiHao &&
|
||||||
<img
|
line.trim() !== "" &&
|
||||||
src={getStaticImage("components/letter/寄信图标")}
|
!line.includes("您好");
|
||||||
alt="寄信"
|
|
||||||
className="w-full h-full object-contain"
|
const segments = line.split("____");
|
||||||
/>
|
return (
|
||||||
</div>
|
<div
|
||||||
|
key={lineIndex}
|
||||||
|
className="mb-6"
|
||||||
|
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] ?? "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={`part-${lineIndex}-${partIndex}`}
|
||||||
|
>
|
||||||
|
{part}
|
||||||
|
{value || " "}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 寄出按钮:关闭窗口 */}
|
||||||
|
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("components/letter/寄出")}
|
||||||
|
alt="寄出"
|
||||||
|
onClick={() => {
|
||||||
|
onSubmit?.(blankValues);
|
||||||
|
}}
|
||||||
|
button
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { useDragAndDrop } from "./useDragAndDrop";
|
|||||||
export { usePageScale } from "./usePageScale";
|
export { usePageScale } from "./usePageScale";
|
||||||
export { useTouch } from "./useTouch";
|
export { useTouch } from "./useTouch";
|
||||||
export { useZoomAndDrag } from "./useZoomAndDrag";
|
export { useZoomAndDrag } from "./useZoomAndDrag";
|
||||||
|
export { useCursorController } from "./useCursorController";
|
||||||
export type {
|
export type {
|
||||||
UseTouchOptions,
|
UseTouchOptions,
|
||||||
UseTouchReturn,
|
UseTouchReturn,
|
||||||
@@ -12,3 +13,4 @@ export type {
|
|||||||
UseZoomAndDragOptions,
|
UseZoomAndDragOptions,
|
||||||
UseZoomAndDragReturn,
|
UseZoomAndDragReturn,
|
||||||
} from "./useZoomAndDrag";
|
} from "./useZoomAndDrag";
|
||||||
|
export type { CursorControllerConfig } from "./useCursorController";
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
|
/** Cursor 解锁规则 */
|
||||||
|
export interface CursorUnlockRule {
|
||||||
|
/** cursor ID */
|
||||||
|
id: string | number;
|
||||||
|
/** 依赖的展品 ID 列表,当这些展品都被访问后,自动解锁此 cursor */
|
||||||
|
dependsOn?: (string | number)[];
|
||||||
|
/** 初始是否启用(如果未配置 dependsOn,则使用此值) */
|
||||||
|
initialEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cursor 控制器配置 */
|
||||||
|
export interface CursorControllerConfig {
|
||||||
|
/** 方式1:使用解锁规则(推荐,更灵活) */
|
||||||
|
unlockRules?: CursorUnlockRule[];
|
||||||
|
/** 方式2:使用依赖映射(简化配置,格式:{ cursorId: [依赖的展品Id列表] }) */
|
||||||
|
dependencies?: Record<string | number, (string | number)[]>;
|
||||||
|
/** 已访问的展品 ID(支持 Set 或数组,内部会自动处理引用问题) */
|
||||||
|
visited?: Set<string | number> | (string | number)[];
|
||||||
|
/** 初始启用的 cursor ID 列表(向后兼容,如果没有配置 unlockRules 或 dependencies 则使用此值) */
|
||||||
|
initialEnabled?: (string | number)[];
|
||||||
|
/** cursor 完成时的回调函数 */
|
||||||
|
onCursorComplete?: (id: string | number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor 控制器 Hook(混合方案)
|
||||||
|
* 支持自动解锁和手动控制,提供两种配置方式
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* // 方式1:使用解锁规则
|
||||||
|
* const { isEnabled } = useCursorController({
|
||||||
|
* unlockRules: [
|
||||||
|
* { id: 1, initialEnabled: true }, // cursor 1 初始启用
|
||||||
|
* { id: 2, dependsOn: [1] }, // cursor 2 依赖展品1
|
||||||
|
* { id: 3, dependsOn: [1, 2] }, // cursor 3 依赖展品1和2
|
||||||
|
* ],
|
||||||
|
* visited: visited,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // 方式2:使用依赖映射(更简洁)
|
||||||
|
* const { isEnabled } = useCursorController({
|
||||||
|
* initialEnabled: [1],
|
||||||
|
* dependencies: {
|
||||||
|
* 2: [1], // cursor 2 依赖展品 1
|
||||||
|
* 3: [1, 2], // cursor 3 依赖展品 1 和 2
|
||||||
|
* },
|
||||||
|
* visited: visited, // 可以传入 Set 或数组,内部会自动处理
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // 方式3:向后兼容(手动控制)
|
||||||
|
* const { enable, isEnabled } = useCursorController({
|
||||||
|
* initialEnabled: [1],
|
||||||
|
* });
|
||||||
|
* enable(2);
|
||||||
|
*
|
||||||
|
* // 在组件中使用
|
||||||
|
* <Cursor disabled={!isEnabled(1)} />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const useCursorController = (config: CursorControllerConfig = {}) => {
|
||||||
|
const {
|
||||||
|
unlockRules = [],
|
||||||
|
dependencies = {},
|
||||||
|
visited,
|
||||||
|
initialEnabled = [],
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// 手动覆盖的状态(优先级高于自动解锁)
|
||||||
|
const [manualOverrides, setManualOverrides] = useState<
|
||||||
|
Map<string | number, boolean>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
// 将 visited(Set 或数组)序列化为稳定的 key,用于比较内容变化
|
||||||
|
// 无论传入 Set 还是数组,都转换为数组再序列化,避免引用问题
|
||||||
|
const visitedKey = useMemo(() => {
|
||||||
|
if (!visited) return "";
|
||||||
|
const visitedArray = Array.isArray(visited) ? visited : Array.from(visited);
|
||||||
|
if (visitedArray.length === 0) return "";
|
||||||
|
const sorted = [...visitedArray].sort((a, b) => Number(a) - Number(b));
|
||||||
|
return sorted.join(",");
|
||||||
|
}, [visited]);
|
||||||
|
|
||||||
|
// 使用 ref 存储上一次的 key,避免不必要的重新计算
|
||||||
|
const prevKeyRef = useRef<string>("");
|
||||||
|
|
||||||
|
// 使用 ref 存储最新的 visited,避免在依赖中使用对象引用
|
||||||
|
const visitedRef = useRef<Set<string | number>>(new Set());
|
||||||
|
|
||||||
|
// 将 visited(Set 或数组)转换为 Set,并更新 ref
|
||||||
|
// 只在内容变化时更新,避免引用问题
|
||||||
|
useMemo(() => {
|
||||||
|
if (!visited) {
|
||||||
|
visitedRef.current = new Set<string | number>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSet = Array.isArray(visited) ? new Set(visited) : visited;
|
||||||
|
visitedRef.current = newSet;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [visitedKey]); // 使用 visitedKey 而不是 visited 对象,避免引用问题
|
||||||
|
|
||||||
|
// 计算初始启用的 cursor
|
||||||
|
const getInitialEnabled = useCallback(() => {
|
||||||
|
const enabled = new Set<string | number>();
|
||||||
|
const currentVisitedSet = visitedRef.current; // 使用 ref 中的最新值
|
||||||
|
|
||||||
|
// 优先使用 unlockRules
|
||||||
|
if (unlockRules.length > 0) {
|
||||||
|
unlockRules.forEach((rule) => {
|
||||||
|
if (rule.dependsOn && rule.dependsOn.length > 0) {
|
||||||
|
// 检查依赖是否都满足
|
||||||
|
const allDepsMet = rule.dependsOn.every((dep) =>
|
||||||
|
currentVisitedSet.has(dep)
|
||||||
|
);
|
||||||
|
if (allDepsMet) {
|
||||||
|
enabled.add(rule.id);
|
||||||
|
}
|
||||||
|
} else if (rule.initialEnabled) {
|
||||||
|
enabled.add(rule.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其次使用 dependencies
|
||||||
|
if (Object.keys(dependencies).length > 0) {
|
||||||
|
Object.entries(dependencies).forEach(([cursorId, deps]) => {
|
||||||
|
const id = isNaN(Number(cursorId)) ? cursorId : Number(cursorId);
|
||||||
|
if (
|
||||||
|
deps.length === 0 ||
|
||||||
|
deps.every((dep) => currentVisitedSet.has(dep))
|
||||||
|
) {
|
||||||
|
enabled.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
initialEnabled.forEach((id) => enabled.add(id));
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后使用 initialEnabled(向后兼容)
|
||||||
|
initialEnabled.forEach((id) => enabled.add(id));
|
||||||
|
return enabled;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [unlockRules, dependencies, visitedKey, initialEnabled]);
|
||||||
|
|
||||||
|
// 将 manualOverrides 序列化为 key
|
||||||
|
const manualOverridesKey = useMemo(() => {
|
||||||
|
if (manualOverrides.size === 0) return "";
|
||||||
|
const sorted = Array.from(manualOverrides.entries())
|
||||||
|
.sort(([a], [b]) => Number(a) - Number(b))
|
||||||
|
.map(([id, enabled]) => `${id}:${enabled}`)
|
||||||
|
.join(",");
|
||||||
|
return sorted;
|
||||||
|
}, [manualOverrides]);
|
||||||
|
|
||||||
|
// 根据解锁规则和 visited 状态计算应该启用的 cursor
|
||||||
|
const calculateEnabledCursors = useCallback(() => {
|
||||||
|
const enabled = new Set<string | number>();
|
||||||
|
// 使用 ref 中的最新值,避免依赖问题
|
||||||
|
const currentVisitedSet = visitedRef.current;
|
||||||
|
|
||||||
|
// 优先使用 unlockRules
|
||||||
|
if (unlockRules.length > 0) {
|
||||||
|
unlockRules.forEach((rule) => {
|
||||||
|
// 手动覆盖优先
|
||||||
|
if (manualOverrides.has(rule.id)) {
|
||||||
|
if (manualOverrides.get(rule.id)) {
|
||||||
|
enabled.add(rule.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动解锁逻辑
|
||||||
|
if (rule.dependsOn && rule.dependsOn.length > 0) {
|
||||||
|
const allDepsMet = rule.dependsOn.every((dep) =>
|
||||||
|
currentVisitedSet.has(dep)
|
||||||
|
);
|
||||||
|
if (allDepsMet) {
|
||||||
|
enabled.add(rule.id);
|
||||||
|
}
|
||||||
|
} else if (rule.initialEnabled) {
|
||||||
|
enabled.add(rule.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其次使用 dependencies
|
||||||
|
if (Object.keys(dependencies).length > 0) {
|
||||||
|
const allCursorIds = new Set([
|
||||||
|
...initialEnabled,
|
||||||
|
...Object.keys(dependencies).map((key) =>
|
||||||
|
isNaN(Number(key)) ? key : Number(key)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
allCursorIds.forEach((id) => {
|
||||||
|
// 手动覆盖优先
|
||||||
|
if (manualOverrides.has(id)) {
|
||||||
|
if (manualOverrides.get(id)) {
|
||||||
|
enabled.add(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动解锁逻辑
|
||||||
|
const deps = dependencies[id];
|
||||||
|
if (deps && deps.length > 0) {
|
||||||
|
const allDepsMet = deps.every((dep) => currentVisitedSet.has(dep));
|
||||||
|
if (allDepsMet) {
|
||||||
|
enabled.add(id);
|
||||||
|
}
|
||||||
|
} else if (initialEnabled.includes(id)) {
|
||||||
|
enabled.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后使用 initialEnabled(向后兼容)
|
||||||
|
initialEnabled.forEach((id) => {
|
||||||
|
if (manualOverrides.has(id)) {
|
||||||
|
if (manualOverrides.get(id)) {
|
||||||
|
enabled.add(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enabled.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return enabled;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
unlockRules,
|
||||||
|
dependencies,
|
||||||
|
visitedKey,
|
||||||
|
initialEnabled,
|
||||||
|
manualOverridesKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [enabledCursors, setEnabledCursors] =
|
||||||
|
useState<Set<string | number>>(getInitialEnabled);
|
||||||
|
|
||||||
|
// 当 visited 或 manualOverrides 内容变化时,自动重新计算
|
||||||
|
useEffect(() => {
|
||||||
|
const currentKey = `${visitedKey}|${manualOverridesKey}`;
|
||||||
|
// 只在 key 真正变化时才重新计算
|
||||||
|
if (prevKeyRef.current !== currentKey) {
|
||||||
|
prevKeyRef.current = currentKey;
|
||||||
|
setEnabledCursors(calculateEnabledCursors());
|
||||||
|
}
|
||||||
|
}, [visitedKey, manualOverridesKey, calculateEnabledCursors]);
|
||||||
|
|
||||||
|
// 激活 cursor(手动控制,会覆盖自动解锁)
|
||||||
|
const enable = useCallback((ids: string | number | (string | number)[]) => {
|
||||||
|
setManualOverrides((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||||
|
idsArray.forEach((id) => {
|
||||||
|
newMap.set(id, true);
|
||||||
|
});
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
setEnabledCursors((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||||
|
idsArray.forEach((id) => {
|
||||||
|
newSet.add(id);
|
||||||
|
});
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 禁用 cursor
|
||||||
|
const disable = useCallback((ids: string | number | (string | number)[]) => {
|
||||||
|
setManualOverrides((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||||
|
idsArray.forEach((id) => {
|
||||||
|
newMap.set(id, false);
|
||||||
|
});
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
setEnabledCursors((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||||
|
idsArray.forEach((id) => {
|
||||||
|
newSet.delete(id);
|
||||||
|
});
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 检查是否启用
|
||||||
|
const isEnabled = useCallback(
|
||||||
|
(id: string | number) => {
|
||||||
|
return enabledCursors.has(id);
|
||||||
|
},
|
||||||
|
[enabledCursors]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取所有启用的 cursor
|
||||||
|
const getEnabledCursors = useCallback(() => {
|
||||||
|
return new Set(enabledCursors);
|
||||||
|
}, [enabledCursors]);
|
||||||
|
|
||||||
|
// 清除手动覆盖,恢复自动解锁
|
||||||
|
const clearManualOverride = useCallback((id: string | number) => {
|
||||||
|
setManualOverrides((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
newMap.delete(id);
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setManualOverrides(new Map());
|
||||||
|
setEnabledCursors(getInitialEnabled());
|
||||||
|
}, [getInitialEnabled]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enable,
|
||||||
|
disable,
|
||||||
|
isEnabled,
|
||||||
|
getEnabledCursors,
|
||||||
|
clearManualOverride,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -78,7 +78,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "quiz-3",
|
id: "quiz-3",
|
||||||
time: Math.ceil(16.04 * 60),
|
time: 16 * 60 + 26, //16:26
|
||||||
title: "题目3",
|
title: "题目3",
|
||||||
prompt: "新时代的青年可以从陈云身上学习到",
|
prompt: "新时代的青年可以从陈云身上学习到",
|
||||||
type: "drag",
|
type: "drag",
|
||||||
@@ -86,7 +86,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
|||||||
{ value: "积极的人生态度", label: "A.积极的人生态度", isCorrect: true },
|
{ value: "积极的人生态度", label: "A.积极的人生态度", isCorrect: true },
|
||||||
{ value: "刻苦的学习精神", label: "B.刻苦的学习精神", isCorrect: true },
|
{ value: "刻苦的学习精神", label: "B.刻苦的学习精神", isCorrect: true },
|
||||||
{ value: "朴素的节俭精神", label: "C.朴素的节俭精神", isCorrect: true },
|
{ value: "朴素的节俭精神", label: "C.朴素的节俭精神", isCorrect: true },
|
||||||
{ value: "实事求是的精神", label: "D.实事求是的精神" },
|
{ value: "只唯上的工作作风", label: "D.只唯上的工作作风" },
|
||||||
],
|
],
|
||||||
blanks: [
|
blanks: [
|
||||||
{ id: "blank-1", label: "____" },
|
{ id: "blank-1", label: "____" },
|
||||||
@@ -180,7 +180,8 @@ const ChenyunInternalization = () => {
|
|||||||
} else {
|
} else {
|
||||||
setDragQuizErrors((prev) => ({
|
setDragQuizErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[item.id]: "再想一想,确保选择了所有正确答案~",
|
[item.id]:
|
||||||
|
"再想一想,不唯上才是陈云的工作作风,不是只唯上!",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -191,7 +192,7 @@ const ChenyunInternalization = () => {
|
|||||||
visible={activeSuccess !== item.id}
|
visible={activeSuccess !== item.id}
|
||||||
zIndex={10000}
|
zIndex={10000}
|
||||||
questionPrefix={`【拖拽】${item.prompt}`}
|
questionPrefix={`【拖拽】${item.prompt}`}
|
||||||
questionSuffix="等精神。(ABC)"
|
questionSuffix="等精神。"
|
||||||
options={item.options.map((opt) => ({
|
options={item.options.map((opt) => ({
|
||||||
value: opt.value,
|
value: opt.value,
|
||||||
label: opt.label,
|
label: opt.label,
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import {
|
|||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import Cursor from "@/components/cursor";
|
import Cursor from "@/components/cursor";
|
||||||
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
||||||
import { useChenyunStore } from "@/store/chenyunStore";
|
import { useChenyunStore } from "@/store/chenyunStore";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
// starName 固定为 chenyun
|
// starName 固定为 chenyun
|
||||||
const starName = "chenyun";
|
const starName = "chenyun";
|
||||||
@@ -53,6 +54,16 @@ const ChenyunPlace1 = () => {
|
|||||||
const visitedExhibits = getVisitedExhibits(place);
|
const visitedExhibits = getVisitedExhibits(place);
|
||||||
const visited = new Set(visitedExhibits);
|
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) => {
|
const handleOpenExhibit = (index: 1 | 2) => {
|
||||||
setActiveExhibitId(index);
|
setActiveExhibitId(index);
|
||||||
// ExhibitOverlay 打开时会自动通过 onOpen 通知 ResidenceLayout 执行动画
|
// ExhibitOverlay 打开时会自动通过 onOpen 通知 ResidenceLayout 执行动画
|
||||||
@@ -104,12 +115,14 @@ const ChenyunPlace1 = () => {
|
|||||||
className="absolute left-145 top-100"
|
className="absolute left-145 top-100"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 展品2 */}
|
{/* 展品2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute left-198 top-100"
|
className="absolute left-198 top-100"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
{/* 展品3 */}
|
{/* 展品3 */}
|
||||||
{/* <Cursor
|
{/* <Cursor
|
||||||
@@ -150,6 +163,7 @@ const ChenyunPlace1 = () => {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
if (activeExhibitId !== null) {
|
if (activeExhibitId !== null) {
|
||||||
markExhibitVisited(place, activeExhibitId);
|
markExhibitVisited(place, activeExhibitId);
|
||||||
|
// cursor 状态会根据 visited 自动更新,无需手动调用 enable
|
||||||
}
|
}
|
||||||
setActiveExhibitId(null);
|
setActiveExhibitId(null);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useChenyunStore } from "@/store/chenyunStore";
|
import { useChenyunStore } from "@/store/chenyunStore";
|
||||||
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
// starName 固定为 chenyun
|
// starName 固定为 chenyun
|
||||||
const starName = "chenyun";
|
const starName = "chenyun";
|
||||||
@@ -21,11 +22,7 @@ const exhibitSrcMap: Record<1 | 2, string> = {
|
|||||||
2: "chenyun/place2/场景2展品2",
|
2: "chenyun/place2/场景2展品2",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 展品描述映射
|
// 展品描述映射(当前未使用,若后续需要可恢复)
|
||||||
const exhibitDescriptionMap: Record<1 | 2, string> = {
|
|
||||||
1: "",
|
|
||||||
2: "同学你看,这里有一幅陈云提写的颜安小学。陈云在青浦县乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。颜安小学实行的新式教育开阔了陈云的视野,五四运动后,颜安小学师生积极投入反帝爱国运动,为陈云日后加入中国共产党埋下了思想的种子。",
|
|
||||||
};
|
|
||||||
const ChenyunPlace2 = () => {
|
const ChenyunPlace2 = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { moduleName } = useParams<{
|
const { moduleName } = useParams<{
|
||||||
@@ -45,6 +42,11 @@ const ChenyunPlace2 = () => {
|
|||||||
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
||||||
const visitedExhibits = getVisitedExhibits(place);
|
const visitedExhibits = getVisitedExhibits(place);
|
||||||
const visited = new Set(visitedExhibits);
|
const visited = new Set(visitedExhibits);
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
// GuideSteps 显示状态
|
// GuideSteps 显示状态
|
||||||
const [guideVisible, setGuideVisible] = useState(false);
|
const [guideVisible, setGuideVisible] = useState(false);
|
||||||
// GuideSteps 当前步骤
|
// GuideSteps 当前步骤
|
||||||
@@ -114,12 +116,14 @@ const ChenyunPlace2 = () => {
|
|||||||
className="absolute left-100 top-100"
|
className="absolute left-100 top-100"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 展品2 */}
|
{/* 展品2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute left-210 top-100"
|
className="absolute left-210 top-100"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useChenyunStore } from "@/store/chenyunStore";
|
import { useChenyunStore } from "@/store/chenyunStore";
|
||||||
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
// starName 固定为 chenyun
|
// starName 固定为 chenyun
|
||||||
const starName = "chenyun";
|
const starName = "chenyun";
|
||||||
@@ -39,6 +40,11 @@ const ChenyunPlace3 = () => {
|
|||||||
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
||||||
const visitedExhibits = getVisitedExhibits(place);
|
const visitedExhibits = getVisitedExhibits(place);
|
||||||
const visited = new Set(visitedExhibits);
|
const visited = new Set(visitedExhibits);
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
const [videoVisible, setVideoVisible] = useState(false);
|
const [videoVisible, setVideoVisible] = useState(false);
|
||||||
// GuideSteps 显示状态
|
// GuideSteps 显示状态
|
||||||
const [guideVisible, setGuideVisible] = useState(false);
|
const [guideVisible, setGuideVisible] = useState(false);
|
||||||
@@ -103,12 +109,14 @@ const ChenyunPlace3 = () => {
|
|||||||
className="absolute right-200 bottom-75"
|
className="absolute right-200 bottom-75"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 展品2 */}
|
{/* 展品2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-130 bottom-75"
|
className="absolute right-130 bottom-75"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useChenyunStore } from "@/store/chenyunStore";
|
import { useChenyunStore } from "@/store/chenyunStore";
|
||||||
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
// starName 固定为 chenyun
|
// starName 固定为 chenyun
|
||||||
const starName = "chenyun";
|
const starName = "chenyun";
|
||||||
@@ -39,6 +40,11 @@ const ChenyunPlace4 = () => {
|
|||||||
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
||||||
const visitedExhibits = getVisitedExhibits(place);
|
const visitedExhibits = getVisitedExhibits(place);
|
||||||
const visited = new Set(visitedExhibits);
|
const visited = new Set(visitedExhibits);
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
// 展品1引导对话
|
// 展品1引导对话
|
||||||
const [guideVisible, setGuideVisible] = useState(false);
|
const [guideVisible, setGuideVisible] = useState(false);
|
||||||
const [guideStep, setGuideStep] = useState(0);
|
const [guideStep, setGuideStep] = useState(0);
|
||||||
@@ -110,12 +116,14 @@ const ChenyunPlace4 = () => {
|
|||||||
className="absolute left-180 top-120"
|
className="absolute left-180 top-120"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 展品2 */}
|
{/* 展品2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-2 top-100"
|
className="absolute right-2 top-100"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useChenyunStore } from "@/store/chenyunStore";
|
import { useChenyunStore } from "@/store/chenyunStore";
|
||||||
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
// starName 固定为 chenyun
|
// starName 固定为 chenyun
|
||||||
const starName = "chenyun";
|
const starName = "chenyun";
|
||||||
@@ -49,6 +50,11 @@ const ChenyunPlace5 = () => {
|
|||||||
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
||||||
const visitedExhibits = getVisitedExhibits(place);
|
const visitedExhibits = getVisitedExhibits(place);
|
||||||
const visited = new Set(visitedExhibits);
|
const visited = new Set(visitedExhibits);
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1, 2],
|
||||||
|
dependencies: { 3: [1, 2] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
const [videoVisible3, setVideoVisible3] = useState(false);
|
const [videoVisible3, setVideoVisible3] = useState(false);
|
||||||
|
|
||||||
// 展品1引导对话
|
// 展品1引导对话
|
||||||
@@ -109,12 +115,14 @@ const ChenyunPlace5 = () => {
|
|||||||
className="absolute left-30 top-70"
|
className="absolute left-30 top-70"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 展品2 */}
|
{/* 展品2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute left-107 top-72"
|
className="absolute left-107 top-72"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 展品3 */}
|
{/* 展品3 */}
|
||||||
@@ -122,6 +130,7 @@ const ChenyunPlace5 = () => {
|
|||||||
className="absolute right-170 top-84"
|
className="absolute right-170 top-84"
|
||||||
onClick={() => handleOpenExhibit(3)}
|
onClick={() => handleOpenExhibit(3)}
|
||||||
read={visited.has(3)}
|
read={visited.has(3)}
|
||||||
|
disabled={!isEnabled(3)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useChenyunStore } from "@/store/chenyunStore";
|
import { useChenyunStore } from "@/store/chenyunStore";
|
||||||
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
import { usePuzzleAnimation } from "@/contexts/PuzzleAnimationContext";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
// starName 固定为 chenyun
|
// starName 固定为 chenyun
|
||||||
const starName = "chenyun";
|
const starName = "chenyun";
|
||||||
@@ -39,6 +40,11 @@ const ChenyunPlace8 = () => {
|
|||||||
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
// 从 store 获取已访问的展品,转换为 Set 用于兼容现有代码
|
||||||
const visitedExhibits = getVisitedExhibits(place);
|
const visitedExhibits = getVisitedExhibits(place);
|
||||||
const visited = new Set(visitedExhibits);
|
const visited = new Set(visitedExhibits);
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
// 展品2的对话引导
|
// 展品2的对话引导
|
||||||
const [guideVisible, setGuideVisible] = useState(false);
|
const [guideVisible, setGuideVisible] = useState(false);
|
||||||
const [guideStep, setGuideStep] = useState(0);
|
const [guideStep, setGuideStep] = useState(0);
|
||||||
@@ -109,12 +115,14 @@ const ChenyunPlace8 = () => {
|
|||||||
className="absolute right-175 top-75"
|
className="absolute right-175 top-75"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 展品2 */}
|
{/* 展品2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-100 bottom-55"
|
className="absolute right-100 bottom-55"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||||
@@ -185,7 +193,7 @@ const ChenyunPlace8 = () => {
|
|||||||
zIndex={10001}
|
zIndex={10001}
|
||||||
>
|
>
|
||||||
<GuideStepsItem speaker="student">
|
<GuideStepsItem speaker="student">
|
||||||
您讲的"统购统销"打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗?
|
打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗?
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
<GuideStepsItem speaker="guide">
|
<GuideStepsItem speaker="guide">
|
||||||
一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。
|
一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。
|
||||||
|
|||||||
@@ -161,10 +161,10 @@ const ChenyunPlace9 = () => {
|
|||||||
我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他"适合老百姓的要求,做了一点事"的初心。
|
我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他"适合老百姓的要求,做了一点事"的初心。
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
<GuideStepsItem speaker="guide">
|
<GuideStepsItem speaker="guide">
|
||||||
接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去,在实践决策中体会。
|
接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去。
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
<GuideStepsItem speaker="guide">
|
<GuideStepsItem speaker="guide">
|
||||||
陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。
|
在实践决策中体会陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
</GuideSteps>
|
</GuideSteps>
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,14 @@ export const MimianFile = ({
|
|||||||
])}
|
])}
|
||||||
onClick={onFileClick}
|
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
|
<span
|
||||||
className={cn([
|
className={cn([
|
||||||
"absolute left-0 bottom-1/2",
|
"absolute left-0 bottom-1/2",
|
||||||
@@ -48,4 +55,3 @@ export const MimianFile = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题1(图1) */}
|
{/* 米棉之战 - 单选题1(图1) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz1}
|
visible={showQuiz1}
|
||||||
title={`(单选)投机商集中冲击"两白一黑"的核心目的是(\xa0\xa0\xa0\xa0)。`}
|
title={`【单选】投机商集中冲击"两白一黑"的核心目的是(\xa0\xa0\xa0\xa0)。`}
|
||||||
options={[
|
options={[
|
||||||
{ value: "A", label: "A. 满足市场需求" },
|
{ value: "A", label: "A. 满足市场需求" },
|
||||||
{ value: "B", label: "B. 扰乱经济秩序、动摇政权" },
|
{ value: "B", label: "B. 扰乱经济秩序、动摇政权" },
|
||||||
@@ -126,7 +126,7 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题2(图3) */}
|
{/* 米棉之战 - 单选题2(图3) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz2}
|
visible={showQuiz2}
|
||||||
title={`(单选)你觉得此时解决上海和全国面临的严峻经济形势问题的对策应该是(\xa0\xa0\xa0\xa0)。`}
|
title={`【单选】你觉得此时解决上海和全国面临的严峻经济形势问题的对策应该是(\xa0\xa0\xa0\xa0)。`}
|
||||||
options={[
|
options={[
|
||||||
{ value: "A", label: "A. 局部限购" },
|
{ value: "A", label: "A. 局部限购" },
|
||||||
{ value: "B", label: "B. 增发货币" },
|
{ value: "B", label: "B. 增发货币" },
|
||||||
@@ -174,7 +174,7 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题3(图5) */}
|
{/* 米棉之战 - 单选题3(图5) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz3}
|
visible={showQuiz3}
|
||||||
title='(单选题)某地方粮站反馈"本地冬储粮不足,不愿外运"。'
|
title='【单选】某地方粮站反馈"本地冬储粮不足,不愿外运"。'
|
||||||
options={[
|
options={[
|
||||||
{ value: "A", label: "A. 强制调运,否则追责" },
|
{ value: "A", label: "A. 强制调运,否则追责" },
|
||||||
{
|
{
|
||||||
@@ -209,7 +209,7 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题4(图6) */}
|
{/* 米棉之战 - 单选题4(图6) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz4}
|
visible={showQuiz4}
|
||||||
title="(单选题)某地方棉农担心外运后本地纱厂缺原料。"
|
title="【单选】某地方棉农担心外运后本地纱厂缺原料。"
|
||||||
options={[
|
options={[
|
||||||
{ value: "A", label: "A. 全国优先上海,本地暂时忍耐" },
|
{ value: "A", label: "A. 全国优先上海,本地暂时忍耐" },
|
||||||
{
|
{
|
||||||
@@ -252,7 +252,7 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题5(图8) */}
|
{/* 米棉之战 - 单选题5(图8) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz5}
|
visible={showQuiz5}
|
||||||
title="(单选题)此时,以下哪种举措能够一举获得胜利?"
|
title="【单选】此时,以下哪种举措能够一举获得胜利?"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: "A",
|
value: "A",
|
||||||
|
|||||||
@@ -60,7 +60,14 @@ export const YinyuanFile = ({
|
|||||||
])}
|
])}
|
||||||
onClick={onFileClick}
|
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
|
<span
|
||||||
className={cn([
|
className={cn([
|
||||||
"absolute left-0 bottom-1/2 z-10",
|
"absolute left-0 bottom-1/2 z-10",
|
||||||
@@ -108,4 +115,3 @@ export const YinyuanFile = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export const YinyuanQuizzes = ({
|
|||||||
{/* 多选题1 - 点击引导图标后显示 */}
|
{/* 多选题1 - 点击引导图标后显示 */}
|
||||||
<MultipleChoiceQuizPanel
|
<MultipleChoiceQuizPanel
|
||||||
visible={showQuiz1}
|
visible={showQuiz1}
|
||||||
title="(多选题)面对银元投机狂潮,你认为首要突破口是? "
|
title="【多选】面对银元投机狂潮,你认为首要突破口是? "
|
||||||
options={[
|
options={[
|
||||||
{ value: "A", label: "A. 政治攻势(警告、约谈)" },
|
{ value: "A", label: "A. 政治攻势(警告、约谈)" },
|
||||||
{ value: "B", label: "B. 经济手段(抛售银元,以银元打银元)" },
|
{ value: "B", label: "B. 经济手段(抛售银元,以银元打银元)" },
|
||||||
@@ -75,7 +75,7 @@ export const YinyuanQuizzes = ({
|
|||||||
{/* 多选题2(图3)- 关闭旁白后显示 */}
|
{/* 多选题2(图3)- 关闭旁白后显示 */}
|
||||||
<MultipleChoiceQuizPanel
|
<MultipleChoiceQuizPanel
|
||||||
visible={showQuiz2}
|
visible={showQuiz2}
|
||||||
title="(多选题)面对投机商的猖狂反扑,下一步应采取哪些决定性措施?"
|
title="【多选】面对投机商的猖狂反扑,下一步应采取哪些决定性措施?"
|
||||||
options={[
|
options={[
|
||||||
{ value: "A", label: "A. 加大银元抛售量,耗尽投机资本" },
|
{ value: "A", label: "A. 加大银元抛售量,耗尽投机资本" },
|
||||||
{ value: "B", label: "B. 查封投机核心场所,逮捕首要分子" },
|
{ value: "B", label: "B. 查封投机核心场所,逮捕首要分子" },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
const starName = "huangyanpei";
|
const starName = "huangyanpei";
|
||||||
|
|
||||||
@@ -25,6 +26,11 @@ const HuangyanpeiPlace2 = () => {
|
|||||||
|
|
||||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
|
|
||||||
// GuideSteps 当前步骤
|
// GuideSteps 当前步骤
|
||||||
const [guideStep, setGuideStep] = useState(0);
|
const [guideStep, setGuideStep] = useState(0);
|
||||||
@@ -87,11 +93,13 @@ const HuangyanpeiPlace2 = () => {
|
|||||||
className="absolute left-132 top-132"
|
className="absolute left-132 top-132"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute left-240 top-132"
|
className="absolute left-240 top-132"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{completed && (
|
{completed && (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { SuccessAlert } from "@/components/layout";
|
import { SuccessAlert } from "@/components/layout";
|
||||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
const starName = "huangyanpei";
|
const starName = "huangyanpei";
|
||||||
|
|
||||||
@@ -30,6 +31,11 @@ const HuangyanpeiPlace3 = () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1], 3: [1, 2] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
|
|
||||||
// 光标3的引导对话状态
|
// 光标3的引导对话状态
|
||||||
const [guide3Visible, setGuide3Visible] = useState(false);
|
const [guide3Visible, setGuide3Visible] = useState(false);
|
||||||
@@ -121,18 +127,21 @@ const HuangyanpeiPlace3 = () => {
|
|||||||
className="absolute left-32 top-105"
|
className="absolute left-32 top-105"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 光标2 */}
|
{/* 光标2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute left-172 top-90"
|
className="absolute left-172 top-90"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
{/* 光标3 */}
|
{/* 光标3 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-92 top-108"
|
className="absolute right-92 top-108"
|
||||||
onClick={() => handleOpenExhibit(3)}
|
onClick={() => handleOpenExhibit(3)}
|
||||||
read={visited.has(3)}
|
read={visited.has(3)}
|
||||||
|
disabled={!isEnabled(3)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{completed && (
|
{completed && (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { SuccessAlert } from "@/components/layout";
|
|||||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import type { VideoRef } from "@/components/video";
|
import type { VideoRef } from "@/components/video";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
const starName = "huangyanpei";
|
const starName = "huangyanpei";
|
||||||
|
|
||||||
@@ -34,6 +35,16 @@ const HuangyanpeiPlace4 = () => {
|
|||||||
1 | 2 | 3 | 4 | 5 | null
|
1 | 2 | 3 | 4 | 5 | null
|
||||||
>(null);
|
>(null);
|
||||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: {
|
||||||
|
3: [1],
|
||||||
|
4: [3],
|
||||||
|
5: [4],
|
||||||
|
// 2 已注释,不计入顺序
|
||||||
|
},
|
||||||
|
visited,
|
||||||
|
});
|
||||||
|
|
||||||
// 光标1的引导对话状态
|
// 光标1的引导对话状态
|
||||||
const [guide1Visible, setGuide1Visible] = useState(false);
|
const [guide1Visible, setGuide1Visible] = useState(false);
|
||||||
@@ -125,6 +136,7 @@ const HuangyanpeiPlace4 = () => {
|
|||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
size={1}
|
size={1}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 光标2 */}
|
{/* 光标2 */}
|
||||||
{/* <Cursor
|
{/* <Cursor
|
||||||
@@ -137,18 +149,21 @@ const HuangyanpeiPlace4 = () => {
|
|||||||
className="absolute left-125 top-100"
|
className="absolute left-125 top-100"
|
||||||
onClick={() => handleOpenExhibit(3)}
|
onClick={() => handleOpenExhibit(3)}
|
||||||
read={visited.has(3)}
|
read={visited.has(3)}
|
||||||
|
disabled={!isEnabled(3)}
|
||||||
/>
|
/>
|
||||||
{/* 光标4 */}
|
{/* 光标4 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-62 top-102"
|
className="absolute right-62 top-102"
|
||||||
onClick={() => handleOpenExhibit(4)}
|
onClick={() => handleOpenExhibit(4)}
|
||||||
read={visited.has(4)}
|
read={visited.has(4)}
|
||||||
|
disabled={!isEnabled(4)}
|
||||||
/>
|
/>
|
||||||
{/* 光标5 */}
|
{/* 光标5 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-205 top-110"
|
className="absolute right-205 top-110"
|
||||||
onClick={() => handleOpenExhibit(5)}
|
onClick={() => handleOpenExhibit(5)}
|
||||||
read={visited.has(5)}
|
read={visited.has(5)}
|
||||||
|
disabled={!isEnabled(5)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{completed && (
|
{completed && (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { SuccessAlert } from "@/components/layout";
|
|||||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import type { VideoRef } from "@/components/video";
|
import type { VideoRef } from "@/components/video";
|
||||||
|
import { useCursorController } from "@/hooks/useCursorController";
|
||||||
|
|
||||||
const starName = "huangyanpei";
|
const starName = "huangyanpei";
|
||||||
|
|
||||||
@@ -42,6 +43,13 @@ const HuangyanpeiPlace5 = () => {
|
|||||||
// 成功提示状态
|
// 成功提示状态
|
||||||
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
||||||
|
|
||||||
|
// 光标顺序:1 -> 2(需完成1后解锁2)
|
||||||
|
const { isEnabled } = useCursorController({
|
||||||
|
initialEnabled: [1],
|
||||||
|
dependencies: { 2: [1] },
|
||||||
|
visited,
|
||||||
|
});
|
||||||
|
|
||||||
const handleOpenExhibit = (index: 1 | 2) => {
|
const handleOpenExhibit = (index: 1 | 2) => {
|
||||||
if (index === 2) {
|
if (index === 2) {
|
||||||
// 展品2:点击时显示对话,再显示视频
|
// 展品2:点击时显示对话,再显示视频
|
||||||
@@ -101,12 +109,14 @@ const HuangyanpeiPlace5 = () => {
|
|||||||
className="absolute left-35 top-108"
|
className="absolute left-35 top-108"
|
||||||
onClick={() => handleOpenExhibit(1)}
|
onClick={() => handleOpenExhibit(1)}
|
||||||
read={visited.has(1)}
|
read={visited.has(1)}
|
||||||
|
disabled={!isEnabled(1)}
|
||||||
/>
|
/>
|
||||||
{/* 光标2 */}
|
{/* 光标2 */}
|
||||||
<Cursor
|
<Cursor
|
||||||
className="absolute right-112 top-108"
|
className="absolute right-112 top-108"
|
||||||
onClick={() => handleOpenExhibit(2)}
|
onClick={() => handleOpenExhibit(2)}
|
||||||
read={visited.has(2)}
|
read={visited.has(2)}
|
||||||
|
disabled={!isEnabled(2)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{completed && (
|
{completed && (
|
||||||
|
|||||||
Reference in new issue
Block a user