diff --git a/src/assets/images/chenyun/reflection/文件1打开.png b/src/assets/images/chenyun/reflection/文件1打开.png new file mode 100644 index 0000000..f0900fc Binary files /dev/null and b/src/assets/images/chenyun/reflection/文件1打开.png differ diff --git a/src/assets/images/chenyun/reflection/文件2打开.png b/src/assets/images/chenyun/reflection/文件2打开.png new file mode 100644 index 0000000..4afc005 Binary files /dev/null and b/src/assets/images/chenyun/reflection/文件2打开.png differ diff --git a/src/assets/images/components/letter/信.png b/src/assets/images/components/letter/信.png new file mode 100644 index 0000000..4f7de7f Binary files /dev/null and b/src/assets/images/components/letter/信.png differ diff --git a/src/assets/images/components/letter/寄出.png b/src/assets/images/components/letter/寄出.png new file mode 100644 index 0000000..4438d05 Binary files /dev/null and b/src/assets/images/components/letter/寄出.png differ diff --git a/src/assets/images/huangyanpei/place4/6_6&7.png b/src/assets/images/huangyanpei/place4/6_6&7.png new file mode 100644 index 0000000..24bd934 Binary files /dev/null and b/src/assets/images/huangyanpei/place4/6_6&7.png differ diff --git a/src/components/cursor/index.tsx b/src/components/cursor/index.tsx index e48d462..b80331e 100644 --- a/src/components/cursor/index.tsx +++ b/src/components/cursor/index.tsx @@ -6,44 +6,62 @@ const ORIGINAL_SIZES = { unread: { width: 120, height: 121 }, } as const; -const Cursor = ({ - className, - onClick, - read = false, - size = 1, -}: { +interface CursorProps { className?: string; onClick: () => void; /** 已阅读 / 已完成 状态;true 时显示预留的 div,由你自行实现效果 */ read?: boolean; /** 尺寸缩放比例,默认为 1(原始大小) */ size?: number; -}) => { - // 根据 size 计算实际尺寸,保持原始比例 + /** 禁用状态,禁用时无法点击但不影响 read 状态显示。当 read=true 时,disabled 不生效 */ + disabled?: boolean; +} + +const Cursor = ({ + className, + onClick, + read = false, + size = 1, + disabled = false, +}: CursorProps) => { const baseSize = read ? ORIGINAL_SIZES.read : ORIGINAL_SIZES.unread; - const width = baseSize.width * size; - const height = baseSize.height * size; + // 当 read=true 时,disabled 不生效;只有 read=false 且 disabled=true 时才禁用 + 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 (
{ - e.preventDefault(); // 防止 Safari 触发两次 - onClick(); - }} style={{ - width: `${width}px`, - height: `${height}px`, + width: `${baseSize.width * size}px`, + height: `${baseSize.height * size}px`, WebkitTapHighlightColor: "transparent", WebkitTouchCallout: "none", WebkitUserSelect: "none", - touchAction: "manipulation", + touchAction: isDisabled ? "none" : "manipulation", + pointerEvents: "auto", + cursor: isDisabled ? "not-allowed" : "pointer", backgroundImage: getBackgroundImage( read ? "components/point_" : "components/border" ), @@ -51,29 +69,11 @@ const Cursor = ({ backgroundPosition: "center", backgroundRepeat: "no-repeat", }} - onClick={onClick} + onClick={handleClick} + onTouchEnd={handleTouchEnd} draggable={false} > - {read ? ( - // 已阅读 / 已完成 状态 - //
- // 已浏览 - //
- <> - ) : ( - // 未阅读 / 未完成 状态:保持当前效果 + {!read && ( )} diff --git a/src/components/letter/LetterToChenyun.tsx b/src/components/letter/LetterToChenyun.tsx index f0db563..0e51caa 100644 --- a/src/components/letter/LetterToChenyun.tsx +++ b/src/components/letter/LetterToChenyun.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { getStaticImage } from "@/lib/utils"; import { Image } from "@/components"; @@ -23,6 +23,47 @@ const LetterToChenyun = ({ }: LetterToChenyunProps) => { // 下划线可填写内容 const [blankValues, setBlankValues] = useState([]); + // 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字) + const [blankErrors, setBlankErrors] = useState([]); + + // 是否已提交:提交后隐藏编辑信件,展示只读信件 + 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 = `敬爱的陈云爷爷: @@ -64,112 +105,241 @@ const LetterToChenyun = ({
e.stopPropagation()} + style={{ perspective: "1200px" }} > - {/* 信纸背景 */} - 信纸 - - {/* 标题 - 垂直显示在左侧 */} -
+ {/* 编辑态 / 只读态:Y 轴翻转切换 */} + + {/* 前面:编辑态 */}
- 寄给陈云爷爷的信 -
-
+ {/* 编辑态:信纸背景 */} + 信纸 - {/* 信件内容区域 */} -
-
- {(() => { - let placeholderIndex = 0; - let foundNiHao = false; - return letterContent.split("\n").map((line, lineIndex) => { - // 检测是否已经找到"您好"行 - if (line.includes("您好")) { - foundNiHao = true; - } + {/* 标题 - 垂直显示在左侧 */} +
+
+ 寄给陈云爷爷的信 +
+
- // "您好"行之后的非空行需要首行缩进 - const needsIndent = - foundNiHao && - line.trim() !== "" && - !line.includes("您好"); - - const segments = line.split("____"); - return ( -
- {segments.map((part, partIndex) => { - const isLast = partIndex === segments.length - 1; - if (isLast) { - return ( - - {part} - - ); + {/* 信件内容区域(可编辑) */} +
+
+ {(() => { + let placeholderIndex = 0; + let foundNiHao = false; + return letterContent + .split("\n") + .map((line, lineIndex) => { + // 检测是否已经找到"您好"行 + if (line.includes("您好")) { + foundNiHao = true; } - const currentIndex = placeholderIndex++; + // "您好"行之后的非空行需要首行缩进 + const needsIndent = + foundNiHao && + line.trim() !== "" && + !line.includes("您好"); + + const segments = line.split("____"); return ( - - {part} - { - const next = [...blankValues]; - next[currentIndex] = e.target.value; - setBlankValues(next); - }} - /> - +
+ {segments.map((part, partIndex) => { + const isLast = + partIndex === segments.length - 1; + if (isLast) { + return ( + + {part} + + ); + } + + const currentIndex = placeholderIndex++; + return ( + + {part} + { + const next = [...blankValues]; + next[currentIndex] = e.target.value; + setBlankValues(next); + + // 用户再次输入时,清除该空的错误高亮 + if (blankErrors[currentIndex]) { + const nextErrs = [...blankErrors]; + nextErrs[currentIndex] = false; + setBlankErrors(nextErrs); + } + }} + /> + + ); + })} +
); - })} -
- ); - }); - })()} + }); + })()} +
+
+ + {/* 笔的图片 */} +
+ 笔 +
+ + {/* 提交按钮:切换到只读信件态 */} +
+ 可提交状态 { + if (!validateBlanks()) return; + setSubmitted(true); + }} + button + /> +
-
- {/* 笔的图片 */} -
- 笔 -
+ {/* 背面:只读态 */} +
+ {/* 只读态:新背景 */} + 信件 + 信纸 { + setSubmitted(false); + }} + /> - {/* 提交按钮 */} -
- 可提交状态 onSubmit?.(blankValues)} - button - /> -
+ {/* 信件内容区域(只读) */} +
+
+ {(() => { + let placeholderIndex = 0; + let foundNiHao = false; + return letterContent + .split("\n") + .map((line, lineIndex) => { + if (line.includes("您好")) { + foundNiHao = true; + } - {/* 寄信图标(可选,如果需要的话) */} -
- 寄信 -
+ const needsIndent = + foundNiHao && + line.trim() !== "" && + !line.includes("您好"); + + const segments = line.split("____"); + return ( +
+ {segments.map((part, partIndex) => { + const isLast = + partIndex === segments.length - 1; + if (isLast) { + return ( + + {part} + + ); + } + + const currentIndex = placeholderIndex++; + const value = blankValues[currentIndex] ?? ""; + + return ( + + {part} + {value || "  "} + + ); + })} +
+ ); + }); + })()} +
+
+ + {/* 寄出按钮:关闭窗口 */} +
+ 寄出 { + onSubmit?.(blankValues); + }} + button + /> +
+
+
diff --git a/src/hooks/index.ts b/src/hooks/index.ts index da8d2ba..dda8623 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,6 +2,7 @@ export { useDragAndDrop } from "./useDragAndDrop"; export { usePageScale } from "./usePageScale"; export { useTouch } from "./useTouch"; export { useZoomAndDrag } from "./useZoomAndDrag"; +export { useCursorController } from "./useCursorController"; export type { UseTouchOptions, UseTouchReturn, @@ -12,3 +13,4 @@ export type { UseZoomAndDragOptions, UseZoomAndDragReturn, } from "./useZoomAndDrag"; +export type { CursorControllerConfig } from "./useCursorController"; diff --git a/src/hooks/useCursorController.ts b/src/hooks/useCursorController.ts new file mode 100644 index 0000000..32f869e --- /dev/null +++ b/src/hooks/useCursorController.ts @@ -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; + /** 已访问的展品 ID(支持 Set 或数组,内部会自动处理引用问题) */ + visited?: Set | (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); + * + * // 在组件中使用 + * + * ``` + */ +export const useCursorController = (config: CursorControllerConfig = {}) => { + const { + unlockRules = [], + dependencies = {}, + visited, + initialEnabled = [], + } = config; + + // 手动覆盖的状态(优先级高于自动解锁) + const [manualOverrides, setManualOverrides] = useState< + Map + >(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(""); + + // 使用 ref 存储最新的 visited,避免在依赖中使用对象引用 + const visitedRef = useRef>(new Set()); + + // 将 visited(Set 或数组)转换为 Set,并更新 ref + // 只在内容变化时更新,避免引用问题 + useMemo(() => { + if (!visited) { + visitedRef.current = new Set(); + 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(); + 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(); + // 使用 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>(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, + }; +}; diff --git a/src/pages/chenyun/internalization.tsx b/src/pages/chenyun/internalization.tsx index 585cc13..83e86fc 100644 --- a/src/pages/chenyun/internalization.tsx +++ b/src/pages/chenyun/internalization.tsx @@ -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, diff --git a/src/pages/chenyun/place/place1.tsx b/src/pages/chenyun/place/place1.tsx index 670e89a..e92332b 100644 --- a/src/pages/chenyun/place/place1.tsx +++ b/src/pages/chenyun/place/place1.tsx @@ -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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 展品3 */} {/* { onClose={() => { if (activeExhibitId !== null) { markExhibitVisited(place, activeExhibitId); + // cursor 状态会根据 visited 自动更新,无需手动调用 enable } setActiveExhibitId(null); }} diff --git a/src/pages/chenyun/place/place2.tsx b/src/pages/chenyun/place/place2.tsx index 638e394..9292027 100644 --- a/src/pages/chenyun/place/place2.tsx +++ b/src/pages/chenyun/place/place2.tsx @@ -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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 继续前进 - 仅在完成所有展品后显示 */} diff --git a/src/pages/chenyun/place/place3.tsx b/src/pages/chenyun/place/place3.tsx index febbaf5..c2ef741 100644 --- a/src/pages/chenyun/place/place3.tsx +++ b/src/pages/chenyun/place/place3.tsx @@ -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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 继续前进 - 仅在完成所有展品后显示 */} diff --git a/src/pages/chenyun/place/place4.tsx b/src/pages/chenyun/place/place4.tsx index 5d032c1..f86de71 100644 --- a/src/pages/chenyun/place/place4.tsx +++ b/src/pages/chenyun/place/place4.tsx @@ -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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 继续前进 - 仅在完成所有展品后显示 */} diff --git a/src/pages/chenyun/place/place5.tsx b/src/pages/chenyun/place/place5.tsx index 6239a2d..2c4061c 100644 --- a/src/pages/chenyun/place/place5.tsx +++ b/src/pages/chenyun/place/place5.tsx @@ -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 */} 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)} /> {/* 继续前进 - 仅在完成所有展品后显示 */} diff --git a/src/pages/chenyun/place/place8.tsx b/src/pages/chenyun/place/place8.tsx index b97938c..4dc51dd 100644 --- a/src/pages/chenyun/place/place8.tsx +++ b/src/pages/chenyun/place/place8.tsx @@ -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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 继续前进 - 仅在完成所有展品后显示 */} @@ -185,7 +193,7 @@ const ChenyunPlace8 = () => { zIndex={10001} > - 您讲的"统购统销"打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗? + 打赢“银元之战”“两白一黑”战役的过程真的是彰显了中国共产党治理经济的卓越能力!这里有一份“一五”计划报告的手稿,陈云在“一五”计划的编制中做出了什么贡献吗? 一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。 diff --git a/src/pages/chenyun/place/place9.tsx b/src/pages/chenyun/place/place9.tsx index 93a4985..e0a1d98 100644 --- a/src/pages/chenyun/place/place9.tsx +++ b/src/pages/chenyun/place/place9.tsx @@ -161,10 +161,10 @@ const ChenyunPlace9 = () => { 我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他"适合老百姓的要求,做了一点事"的初心。 - 接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去,在实践决策中体会。 + 接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去。 - 陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。 + 在实践决策中体会陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。 diff --git a/src/pages/chenyun/reflection/mimian/MimianFile.tsx b/src/pages/chenyun/reflection/mimian/MimianFile.tsx index 6a37245..754c2e2 100644 --- a/src/pages/chenyun/reflection/mimian/MimianFile.tsx +++ b/src/pages/chenyun/reflection/mimian/MimianFile.tsx @@ -35,7 +35,14 @@ export const MimianFile = ({ ])} onClick={onFileClick} > - + ); }; - diff --git a/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx b/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx index dbf1bed..31d494c 100644 --- a/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx +++ b/src/pages/chenyun/reflection/mimian/MimianQuizzes.tsx @@ -98,7 +98,7 @@ export const MimianQuizzes = ({ {/* 米棉之战 - 单选题1(图1) */} - + ); }; - diff --git a/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx b/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx index 9b8b87e..03aeadd 100644 --- a/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx +++ b/src/pages/chenyun/reflection/yinyuan/YinyuanQuizzes.tsx @@ -46,7 +46,7 @@ export const YinyuanQuizzes = ({ {/* 多选题1 - 点击引导图标后显示 */} { const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null); const [visited, setVisited] = useState>(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)} /> handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {completed && ( diff --git a/src/pages/huangyanpei/place/place3.tsx b/src/pages/huangyanpei/place/place3.tsx index 1576e4f..0191552 100644 --- a/src/pages/huangyanpei/place/place3.tsx +++ b/src/pages/huangyanpei/place/place3.tsx @@ -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>(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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {/* 光标3 */} handleOpenExhibit(3)} read={visited.has(3)} + disabled={!isEnabled(3)} /> {completed && ( diff --git a/src/pages/huangyanpei/place/place4.tsx b/src/pages/huangyanpei/place/place4.tsx index 3d8871f..359c9b9 100644 --- a/src/pages/huangyanpei/place/place4.tsx +++ b/src/pages/huangyanpei/place/place4.tsx @@ -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>(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 */} {/* { className="absolute left-125 top-100" onClick={() => handleOpenExhibit(3)} read={visited.has(3)} + disabled={!isEnabled(3)} /> {/* 光标4 */} handleOpenExhibit(4)} read={visited.has(4)} + disabled={!isEnabled(4)} /> {/* 光标5 */} handleOpenExhibit(5)} read={visited.has(5)} + disabled={!isEnabled(5)} /> {completed && ( diff --git a/src/pages/huangyanpei/place/place5.tsx b/src/pages/huangyanpei/place/place5.tsx index 12207f5..8c8b87c 100644 --- a/src/pages/huangyanpei/place/place5.tsx +++ b/src/pages/huangyanpei/place/place5.tsx @@ -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 */} handleOpenExhibit(2)} read={visited.has(2)} + disabled={!isEnabled(2)} /> {completed && (