feat: 更新组件和引导对话,增强用户体验
- 在多个组件中添加新的选择和填空功能,提升互动性。 - 更新引导对话文本,确保信息准确性和一致性。 - 引入新的选择组件,优化用户填写体验。 - 清理冗余代码,提升可维护性。
This commit is contained in:
1 parent
a8723ab4aa
commit
5bdfdadede
27 files changed
+880
-1252
No files matched your search
@@ -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<void> => {
|
||||
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`}
|
||||
>
|
||||
<Outlet />
|
||||
{/* 拼图盒 - 独立于内容层,确保始终可点击 */}
|
||||
<div
|
||||
ref={puzzleBoxRef}
|
||||
className="absolute left-4 bottom-4 z-[1] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (residence) {
|
||||
setIsPuzzleSystemVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image src={getStaticImage("components/拼图盒")} button />
|
||||
</div>
|
||||
{/* 拼图盒 - 独立于内容层,确保始终可点击(仅支持拼图功能的故居显示) */}
|
||||
{hasPuzzleFeature && (
|
||||
<div
|
||||
ref={puzzleBoxRef}
|
||||
className="absolute left-4 bottom-4 z-[1] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (residence) {
|
||||
setIsPuzzleSystemVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image src={getStaticImage("components/拼图盒")} button />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 视频层 - 只在播放时渲染,z-index 动态调整 */}
|
||||
@@ -300,8 +327,8 @@ const ResidenceLayoutContent = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 拼图系统弹窗 */}
|
||||
{residence && (
|
||||
{/* 拼图系统弹窗(仅支持拼图功能的故居显示) */}
|
||||
{hasPuzzleFeature && residence && (
|
||||
<PuzzleSystem
|
||||
visible={isPuzzleSystemVisible}
|
||||
onClose={() => setIsPuzzleSystemVisible(false)}
|
||||
@@ -309,15 +336,18 @@ const ResidenceLayoutContent = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 碎片显示 - 用于飞行动画,显示在屏幕正中间 */}
|
||||
{showPuzzlePiece && puzzlePieceId && residence && (
|
||||
<PuzzlePiecePortal
|
||||
ref={puzzlePieceRef}
|
||||
residence={residence}
|
||||
pieceId={puzzlePieceId}
|
||||
hidden={isFlying}
|
||||
/>
|
||||
)}
|
||||
{/* 碎片显示 - 用于飞行动画,显示在屏幕正中间(仅支持拼图功能的故居显示) */}
|
||||
{hasPuzzleFeature &&
|
||||
showPuzzlePiece &&
|
||||
puzzlePieceId &&
|
||||
residence && (
|
||||
<PuzzlePiecePortal
|
||||
ref={puzzlePieceRef}
|
||||
residence={residence}
|
||||
pieceId={puzzlePieceId}
|
||||
hidden={isFlying}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 动画遮罩层 - 动画播放时盖住所有元素,防止操作 */}
|
||||
{isAnimationPlaying &&
|
||||
|
||||
Reference in new issue
Block a user