From b544d62dd18136ccc94bf16ae5c7044bed830c67 Mon Sep 17 00:00:00 2001 From: LiuJiaNan <15703339975@163.com> Date: Wed, 15 Jul 2026 09:36:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(RightUtils):=20=E4=BC=98=E5=8C=96=E5=AD=90?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E5=8A=A8=E7=94=BB=E7=82=B9=E5=87=BB=E9=94=81?= =?UTF-8?q?=E5=AE=9A=E4=B8=8E=E7=95=8C=E9=9D=A2=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 isAnimating 状态和 tryStartAnimation 方法防止动画期间重复点击 - 子菜单背景按钮禁用时添加 disabled 样式及无障碍 aria-disabled 属性 - 按钮点击事件中增加动画锁定判断,避免动画未完成前状态切换 - useChildMenuAnimation Hook 内部实现动画锁定机制,保证动画开始至结束期间禁用重复操作 - 在首次渲染和动画完成处正确管理动画锁,确保状态与动画同步 - index.less 中新增 disabled 样式防止用户交互,保持按钮和菜单状态一致 --- .../Map/components/RightUtils/index.js | 15 ++++++++-- .../Map/components/RightUtils/index.less | 6 ++++ .../RightUtils/useChildMenuAnimation.js | 30 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) 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, }; }