diff --git a/src/pages/Container/Map/components/RightUtils/index.js b/src/pages/Container/Map/components/RightUtils/index.js
index 84abee1..9318946 100644
--- a/src/pages/Container/Map/components/RightUtils/index.js
+++ b/src/pages/Container/Map/components/RightUtils/index.js
@@ -34,7 +34,11 @@ function RightUtils(props) {
currentBranchOffice && bottomUtilsCurrentIndex !== -1,
);
- const { controls: childMenuControls } = useChildMenuAnimation(
+ const {
+ controls: childMenuControls,
+ isAnimating: isChildMenuAnimating,
+ tryStartAnimation: tryStartChildMenuAnimation,
+ } = useChildMenuAnimation(
currentBranchOffice && bottomUtilsCurrentIndex !== -1 && isShowChildLevel,
);
@@ -178,8 +182,13 @@ function RightUtils(props) {
))}
setIsShowChildLevel(!isShowChildLevel)}
+ className={`bg ${isShowChildLevel ? "active" : ""} ${isChildMenuAnimating ? "disabled" : ""}`}
+ aria-disabled={isChildMenuAnimating}
+ onClick={() => {
+ if (!tryStartChildMenuAnimation())
+ return;
+ setIsShowChildLevel(visible => !visible);
+ }}
>
diff --git a/src/pages/Container/Map/components/RightUtils/index.less b/src/pages/Container/Map/components/RightUtils/index.less
index 703ce4b..e4fb64d 100644
--- a/src/pages/Container/Map/components/RightUtils/index.less
+++ b/src/pages/Container/Map/components/RightUtils/index.less
@@ -64,6 +64,12 @@
transform: rotate(0deg);
}
+ // 子菜单动画执行期间禁止重复点击,确保按钮方向与菜单展开状态始终一致。
+ &.disabled {
+ pointer-events: none;
+ cursor: not-allowed;
+ }
+
img {
width: 101px;
height: 100px;
diff --git a/src/pages/Container/Map/components/RightUtils/useChildMenuAnimation.js b/src/pages/Container/Map/components/RightUtils/useChildMenuAnimation.js
index 64a6ceb..b56848c 100644
--- a/src/pages/Container/Map/components/RightUtils/useChildMenuAnimation.js
+++ b/src/pages/Container/Map/components/RightUtils/useChildMenuAnimation.js
@@ -1,5 +1,5 @@
import { useAnimation } from "motion/react";
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
/**
* RightUtils 子菜单动画 Hook
@@ -60,6 +60,24 @@ export function useChildMenuAnimation(isVisible) {
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
const isFirstRender = useRef(true);
+ // 使用 ref 同步锁定点击,避免 React 状态尚未更新时连续点击穿透;state 负责更新按钮禁用样式。
+ // 初始即需要展开时直接锁定,避免首次 effect 执行前的点击空窗。
+ const animationLockRef = useRef(isVisible);
+ const [isAnimating, setIsAnimating] = useState(isVisible);
+
+ const lockAnimation = () => {
+ if (animationLockRef.current)
+ return false;
+ animationLockRef.current = true;
+ setIsAnimating(true);
+ return true;
+ };
+
+ const unlockAnimation = () => {
+ animationLockRef.current = false;
+ setIsAnimating(false);
+ };
+
// ==================== 主题:监听可见性变化并执行弹跳动画 ====================
//
// useEffect 的职责:
@@ -76,6 +94,7 @@ export function useChildMenuAnimation(isVisible) {
// ==================== 场景 1:首次渲染 ====================
if (isFirstRender.current) {
if (isVisible) {
+ lockAnimation();
// 执行展开动画:从下向上炫酷弹跳进入
controls.start({
// Y 轴位置关键帧(四个关键帧实现弹跳效果):
@@ -121,6 +140,7 @@ export function useChildMenuAnimation(isVisible) {
// 动画完成后标记状态
isExpanded.current = true; // 标记已展开
isFirstRender.current = false; // 标记首次渲染完成
+ unlockAnimation();
});
}
else {
@@ -132,6 +152,8 @@ export function useChildMenuAnimation(isVisible) {
// ==================== 场景 2:展开动画(非首次渲染)====================
if (isVisible && !isExpanded.current) {
+ // 正常点击时锁已由按钮同步占用;外部状态触发动画时在这里补充锁定。
+ lockAnimation();
// 从下向上炫酷弹跳进入
controls.start({
y: [80, -15, 5, 0], // 位置:下方80px -> 上方15px(过冲)-> 下方5px(回落)-> 正常
@@ -144,11 +166,13 @@ export function useChildMenuAnimation(isVisible) {
},
}).then(() => {
isExpanded.current = true; // 标记已展开
+ unlockAnimation();
});
}
// ==================== 场景 3:收起动画 ====================
else if (!isVisible && isExpanded.current) {
+ lockAnimation();
// 从上向下炫酷弹跳离开
controls.start({
// Y 轴位置关键帧:
@@ -191,6 +215,7 @@ export function useChildMenuAnimation(isVisible) {
},
}).then(() => {
isExpanded.current = false; // 标记已收起
+ unlockAnimation();
});
}
}, [isVisible, controls]);
@@ -198,5 +223,8 @@ export function useChildMenuAnimation(isVisible) {
// ==================== 返回值 ====================
return {
controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
+ isAnimating,
+ // 点击处理器调用后会立即占用锁,确保同一帧内的连续点击也会被拦截。
+ tryStartAnimation: lockAnimation,
};
}