feat: 更新组件和引导对话,增强用户体验

- 在多个组件中添加新的选择和填空功能,提升互动性。
- 更新引导对话文本,确保信息准确性和一致性。
- 引入新的选择组件,优化用户填写体验。
- 清理冗余代码,提升可维护性。
This commit is contained in:
zhongyidian committed 2026-01-16 17:54:51 +08:00
1 parent a8723ab4aa
commit 5bdfdadede
27 files changed
+880 -1252

No files matched your search

+149 -119
View File
@@ -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 &&