diff --git a/public/audio/chenyun/mp3_chenyun_40.mp3 b/public/audio/chenyun/mp3_chenyun_40.mp3 new file mode 100644 index 0000000..6c16d39 Binary files /dev/null and b/public/audio/chenyun/mp3_chenyun_40.mp3 differ diff --git a/public/audio/huangyanpei/mp3_huangyanpei_31.mp3 b/public/audio/huangyanpei/mp3_huangyanpei_31.mp3 new file mode 100644 index 0000000..2563e62 Binary files /dev/null and b/public/audio/huangyanpei/mp3_huangyanpei_31.mp3 differ diff --git a/src/components/index.ts b/src/components/index.ts index 0ddafed..51867bf 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -62,15 +62,6 @@ export type { export { Narration } from "./narration"; export type { NarrationProps } from "./narration"; -// Toast component -export { toast, Toast, useToast, ToastProvider } from "./toast"; -export type { - ToastType, - ToastPosition, - ToastOptions, - ToastInstance, -} from "./toast"; - // Clue components export { ClueHintModal, ClueBookModal } from "./clue"; export type { ClueHintModalProps, ClueHint, ClueBookModalProps } from "./clue"; @@ -79,6 +70,10 @@ export type { ClueHintModalProps, ClueHint, ClueBookModalProps } from "./clue"; export { flyToAnimation } from "./animation"; export type { FlyToAnimationOptions, FlyToPosition } from "./animation"; +// Select component +export { Select } from "./select"; +export type { SelectProps, SelectOption } from "./select"; + // Letter components export { LetterToChenyun, diff --git a/src/components/layout/ResidenceLayout.tsx b/src/components/layout/ResidenceLayout.tsx index c29c240..e74c83c 100644 --- a/src/components/layout/ResidenceLayout.tsx +++ b/src/components/layout/ResidenceLayout.tsx @@ -11,6 +11,12 @@ import { Image, PuzzleSystem, flyToAnimation } from "@/components"; import { usePuzzleStore } from "@/store/puzzleStore"; import PuzzlePiecePortal from "@/components/puzzle/PuzzlePiecePortal"; +/** + * 支持拼图盒功能的故居列表 + * 后续如需为其他故居添加拼图盒,只需在此数组中添加对应的故居名称即可 + */ +const RESIDENCES_WITH_PUZZLE = ["chenyun"] as const; + /** * 故居布局组件 * 用于所有故居页面的统一布局容器 @@ -48,112 +54,131 @@ const ResidenceLayoutContent = () => { return ""; }, [location.pathname]); - // 处理碎片动画触发 + // 判断当前故居是否支持拼图盒功能 + const hasPuzzleFeature = useMemo( + () => RESIDENCES_WITH_PUZZLE.includes(residence as any), + [residence] + ); + + // 动画配置常量 + const ANIMATION_CONFIG = { + APPEAR_DURATION: 800, // 初始出现动画持续时间(3D旋转+放大,毫秒) + GLOW_DURATION: 800, // 发光动画持续时间(毫秒) + FLY_DURATION: 1400, // 飞行动画持续时间(毫秒) + RETRY_DELAY: 100, // 重试延迟(毫秒) + MAX_RETRIES: 3, // 最大重试次数 + SCALE: { from: 1, to: 0.25 }, // 缩放比例(更小的目标尺寸,效果更明显) + OPACITY: [1, 1, 0.6, 0] as number[], // 透明度变化(更平滑的淡出) + }; + + // 清理动画状态 + const cleanupAnimation = () => { + setIsAnimationPlaying(false); + setShowPuzzlePiece(false); + setPuzzlePieceId(null); + setIsFlying(false); + animationInProgressRef.current = false; + }; + + // 执行飞行动画 + const executeFlyAnimation = (pieceId: number) => { + if (!residence || !puzzleBoxRef.current || !puzzlePieceRef.current) { + return false; + } + + setIsFlying(true); + + // 使用 requestAnimationFrame 确保元素已完全渲染 + requestAnimationFrame(() => { + if (!puzzlePieceRef.current || !residence) { + return; + } + + flyToAnimation({ + fromRef: puzzlePieceRef, + toRef: puzzleBoxRef, + targetPosition: "center", + duration: ANIMATION_CONFIG.FLY_DURATION, + scale: ANIMATION_CONFIG.SCALE, + opacity: ANIMATION_CONFIG.OPACITY, + ease: [0.25, 0.1, 0.25, 1], // 自定义缓动函数,更自然的加速和减速 + onComplete: () => { + if (residence) { + unlockPuzzle(residence, pieceId); + } + cleanupAnimation(); + }, + }); + }); + + return true; + }; + + // 等待元素准备就绪并执行动画 + const waitForElementsAndAnimate = async ( + pieceId: number, + retryCount = 0 + ): Promise => { + if (!residence) { + cleanupAnimation(); + return; + } + + // 检查元素是否已准备好 + if (puzzleBoxRef.current && puzzlePieceRef.current) { + const success = executeFlyAnimation(pieceId); + if (success) return; + } + + // 如果元素未准备好且未超过最大重试次数,则重试 + if (retryCount < ANIMATION_CONFIG.MAX_RETRIES) { + await new Promise((resolve) => + setTimeout(resolve, ANIMATION_CONFIG.RETRY_DELAY) + ); + return waitForElementsAndAnimate(pieceId, retryCount + 1); + } + + // 超过最大重试次数,直接解锁并清理 + console.warn("Puzzle animation: 元素未准备好,直接解锁"); + unlockPuzzle(residence, pieceId); + cleanupAnimation(); + }; + + // 处理碎片动画触发(仅支持拼图功能的故居) const handleTriggerPuzzleAnimation = (pieceId: number) => { - if (!residence) return; + if (!residence || !hasPuzzleFeature) return; // 防止重复执行动画 if (animationInProgressRef.current) return; // 检查碎片是否已解锁 - if (!isPuzzleUnlocked(residence, pieceId)) { - // 标记动画正在进行 - animationInProgressRef.current = true; + if (isPuzzleUnlocked(residence, pieceId)) return; - // 开始动画流程:禁用所有操作 - setIsAnimationPlaying(true); - setPuzzlePieceId(pieceId); + // 标记动画正在进行 + animationInProgressRef.current = true; - // 显示碎片在屏幕正中间 - setShowPuzzlePiece(true); + // 开始动画流程:禁用所有操作 + setIsAnimationPlaying(true); + setPuzzlePieceId(pieceId); + setShowPuzzlePiece(true); - // 使用双重 requestAnimationFrame 确保 Portal 渲染完成 - requestAnimationFrame(() => { - requestAnimationFrame(() => { - // 延迟执行飞行动画,让发光动画先显示 - setTimeout(() => { - if (puzzleBoxRef.current && puzzlePieceRef.current) { - // 标记开始飞行,隐藏原始碎片 - setIsFlying(true); + // 使用双重 requestAnimationFrame 确保 Portal 渲染完成 + requestAnimationFrame(() => { + requestAnimationFrame(async () => { + // 等待初始出现动画(旋转+放大)完成 + await new Promise((resolve) => + setTimeout(resolve, ANIMATION_CONFIG.APPEAR_DURATION) + ); - // 使用 requestAnimationFrame 确保隐藏操作在下一帧执行,并且元素已完全渲染 - requestAnimationFrame(() => { - // 再次确保元素已渲染并获取准确尺寸 - if (puzzlePieceRef.current) { - flyToAnimation({ - fromRef: puzzlePieceRef, - toRef: puzzleBoxRef, - targetPosition: "center", - duration: 1200, - scale: { from: 1, to: 0.3 }, - opacity: [1, 0.8, 0], - onComplete: () => { - // 动画完成后解锁碎片并恢复交互 - unlockPuzzle(residence, pieceId); - setShowPuzzlePiece(false); - setPuzzlePieceId(null); - setIsAnimationPlaying(false); - setIsFlying(false); - animationInProgressRef.current = false; - }, - }); - } else { - // 如果 ref 不存在,直接解锁并恢复 - unlockPuzzle(residence, pieceId); - setIsAnimationPlaying(false); - setShowPuzzlePiece(false); - setPuzzlePieceId(null); - setIsFlying(false); - animationInProgressRef.current = false; - } - }); - } else { - // 如果 ref 不存在,等待一下再试 - setTimeout(() => { - if (puzzleBoxRef.current && puzzlePieceRef.current) { - setIsFlying(true); - requestAnimationFrame(() => { - if (puzzlePieceRef.current) { - flyToAnimation({ - fromRef: puzzlePieceRef, - toRef: puzzleBoxRef, - targetPosition: "center", - duration: 1200, - scale: { from: 1, to: 0.3 }, - opacity: [1, 0.8, 0], - onComplete: () => { - unlockPuzzle(residence, pieceId); - setShowPuzzlePiece(false); - setPuzzlePieceId(null); - setIsAnimationPlaying(false); - setIsFlying(false); - animationInProgressRef.current = false; - }, - }); - } else { - unlockPuzzle(residence, pieceId); - setIsAnimationPlaying(false); - setShowPuzzlePiece(false); - setPuzzlePieceId(null); - setIsFlying(false); - animationInProgressRef.current = false; - } - }); - } else { - // 如果还是不存在,直接解锁并恢复 - unlockPuzzle(residence, pieceId); - setIsAnimationPlaying(false); - setShowPuzzlePiece(false); - setPuzzlePieceId(null); - setIsFlying(false); - animationInProgressRef.current = false; - } - }, 100); - } - }, 500); // 发光动画显示 500ms 后开始飞行 - }); + // 等待发光动画显示一段时间,让用户看到碎片 + await new Promise((resolve) => + setTimeout(resolve, ANIMATION_CONFIG.GLOW_DURATION) + ); + + // 等待元素准备就绪并执行飞行动画 + await waitForElementsAndAnimate(pieceId); }); - } + }); }; // 获取当前路由的视频路径 @@ -262,19 +287,21 @@ const ResidenceLayoutContent = () => { } transition-opacity duration-300 ease-in`} > - {/* 拼图盒 - 独立于内容层,确保始终可点击 */} -
{ - e.stopPropagation(); - if (residence) { - setIsPuzzleSystemVisible(true); - } - }} - > - -
+ {/* 拼图盒 - 独立于内容层,确保始终可点击(仅支持拼图功能的故居显示) */} + {hasPuzzleFeature && ( +
{ + e.stopPropagation(); + if (residence) { + setIsPuzzleSystemVisible(true); + } + }} + > + +
+ )} {/* 视频层 - 只在播放时渲染,z-index 动态调整 */} @@ -300,8 +327,8 @@ const ResidenceLayoutContent = () => { )} - {/* 拼图系统弹窗 */} - {residence && ( + {/* 拼图系统弹窗(仅支持拼图功能的故居显示) */} + {hasPuzzleFeature && residence && ( setIsPuzzleSystemVisible(false)} @@ -309,15 +336,18 @@ const ResidenceLayoutContent = () => { /> )} - {/* 碎片显示 - 用于飞行动画,显示在屏幕正中间 */} - {showPuzzlePiece && puzzlePieceId && residence && ( -