feat(RightUtils): 优化子菜单动画点击锁定与界面交互
- 新增 isAnimating 状态和 tryStartAnimation 方法防止动画期间重复点击 - 子菜单背景按钮禁用时添加 disabled 样式及无障碍 aria-disabled 属性 - 按钮点击事件中增加动画锁定判断,避免动画未完成前状态切换 - useChildMenuAnimation Hook 内部实现动画锁定机制,保证动画开始至结束期间禁用重复操作 - 在首次渲染和动画完成处正确管理动画锁,确保状态与动画同步 - index.less 中新增 disabled 样式防止用户交互,保持按钮和菜单状态一致master
parent
42961e417c
commit
b544d62dd1
|
|
@ -34,7 +34,11 @@ function RightUtils(props) {
|
||||||
currentBranchOffice && bottomUtilsCurrentIndex !== -1,
|
currentBranchOffice && bottomUtilsCurrentIndex !== -1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { controls: childMenuControls } = useChildMenuAnimation(
|
const {
|
||||||
|
controls: childMenuControls,
|
||||||
|
isAnimating: isChildMenuAnimating,
|
||||||
|
tryStartAnimation: tryStartChildMenuAnimation,
|
||||||
|
} = useChildMenuAnimation(
|
||||||
currentBranchOffice && bottomUtilsCurrentIndex !== -1 && isShowChildLevel,
|
currentBranchOffice && bottomUtilsCurrentIndex !== -1 && isShowChildLevel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -178,8 +182,13 @@ function RightUtils(props) {
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<div
|
<div
|
||||||
className={`bg ${isShowChildLevel ? "active" : ""}`}
|
className={`bg ${isShowChildLevel ? "active" : ""} ${isChildMenuAnimating ? "disabled" : ""}`}
|
||||||
onClick={() => setIsShowChildLevel(!isShowChildLevel)}
|
aria-disabled={isChildMenuAnimating}
|
||||||
|
onClick={() => {
|
||||||
|
if (!tryStartChildMenuAnimation())
|
||||||
|
return;
|
||||||
|
setIsShowChildLevel(visible => !visible);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img src={buttonBg} alt="" />
|
<img src={buttonBg} alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,12 @@
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 子菜单动画执行期间禁止重复点击,确保按钮方向与菜单展开状态始终一致。
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 101px;
|
width: 101px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAnimation } from "motion/react";
|
import { useAnimation } from "motion/react";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RightUtils 子菜单动画 Hook
|
* RightUtils 子菜单动画 Hook
|
||||||
|
|
@ -60,6 +60,24 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
||||||
const isFirstRender = useRef(true);
|
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 的职责:
|
// useEffect 的职责:
|
||||||
|
|
@ -76,6 +94,7 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
// ==================== 场景 1:首次渲染 ====================
|
// ==================== 场景 1:首次渲染 ====================
|
||||||
if (isFirstRender.current) {
|
if (isFirstRender.current) {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
|
lockAnimation();
|
||||||
// 执行展开动画:从下向上炫酷弹跳进入
|
// 执行展开动画:从下向上炫酷弹跳进入
|
||||||
controls.start({
|
controls.start({
|
||||||
// Y 轴位置关键帧(四个关键帧实现弹跳效果):
|
// Y 轴位置关键帧(四个关键帧实现弹跳效果):
|
||||||
|
|
@ -121,6 +140,7 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
// 动画完成后标记状态
|
// 动画完成后标记状态
|
||||||
isExpanded.current = true; // 标记已展开
|
isExpanded.current = true; // 标记已展开
|
||||||
isFirstRender.current = false; // 标记首次渲染完成
|
isFirstRender.current = false; // 标记首次渲染完成
|
||||||
|
unlockAnimation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -132,6 +152,8 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
|
|
||||||
// ==================== 场景 2:展开动画(非首次渲染)====================
|
// ==================== 场景 2:展开动画(非首次渲染)====================
|
||||||
if (isVisible && !isExpanded.current) {
|
if (isVisible && !isExpanded.current) {
|
||||||
|
// 正常点击时锁已由按钮同步占用;外部状态触发动画时在这里补充锁定。
|
||||||
|
lockAnimation();
|
||||||
// 从下向上炫酷弹跳进入
|
// 从下向上炫酷弹跳进入
|
||||||
controls.start({
|
controls.start({
|
||||||
y: [80, -15, 5, 0], // 位置:下方80px -> 上方15px(过冲)-> 下方5px(回落)-> 正常
|
y: [80, -15, 5, 0], // 位置:下方80px -> 上方15px(过冲)-> 下方5px(回落)-> 正常
|
||||||
|
|
@ -144,11 +166,13 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
},
|
},
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
isExpanded.current = true; // 标记已展开
|
isExpanded.current = true; // 标记已展开
|
||||||
|
unlockAnimation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 场景 3:收起动画 ====================
|
// ==================== 场景 3:收起动画 ====================
|
||||||
else if (!isVisible && isExpanded.current) {
|
else if (!isVisible && isExpanded.current) {
|
||||||
|
lockAnimation();
|
||||||
// 从上向下炫酷弹跳离开
|
// 从上向下炫酷弹跳离开
|
||||||
controls.start({
|
controls.start({
|
||||||
// Y 轴位置关键帧:
|
// Y 轴位置关键帧:
|
||||||
|
|
@ -191,6 +215,7 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
},
|
},
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
isExpanded.current = false; // 标记已收起
|
isExpanded.current = false; // 标记已收起
|
||||||
|
unlockAnimation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isVisible, controls]);
|
}, [isVisible, controls]);
|
||||||
|
|
@ -198,5 +223,8 @@ export function useChildMenuAnimation(isVisible) {
|
||||||
// ==================== 返回值 ====================
|
// ==================== 返回值 ====================
|
||||||
return {
|
return {
|
||||||
controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
|
controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
|
||||||
|
isAnimating,
|
||||||
|
// 点击处理器调用后会立即占用锁,确保同一帧内的连续点击也会被拦截。
|
||||||
|
tryStartAnimation: lockAnimation,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue