From d1de5b0d43afeddd30c00d16896d28f4e8cfa335 Mon Sep 17 00:00:00 2001 From: LiuJiaNan <15703339975@163.com> Date: Wed, 25 Feb 2026 10:52:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomUtils/useBottomUtilsAnimation.js | 60 +++++--- .../BottomUtils/usePortUtilsAnimation.js | 128 +++++++++++++----- .../CenterUtils/useCenterUtilsAnimation.js | 89 +++++++----- 3 files changed, 186 insertions(+), 91 deletions(-) diff --git a/src/pages/Container/Map/components/BottomUtils/useBottomUtilsAnimation.js b/src/pages/Container/Map/components/BottomUtils/useBottomUtilsAnimation.js index 35fec07..a4826dd 100644 --- a/src/pages/Container/Map/components/BottomUtils/useBottomUtilsAnimation.js +++ b/src/pages/Container/Map/components/BottomUtils/useBottomUtilsAnimation.js @@ -5,30 +5,40 @@ import { useEffect, useRef, useState } from "react"; * BottomUtils 组件动画 Hook * * 功能说明: - * 1. 监听显示/隐藏状态变化 - * 2. 执行从下向上进入、从上向下离开的动画效果 - * 3. 支持模式切换(港口工具 ↔ 分公司工具) - * 4. 支持首次加载动画 + * 1. 监听显示/隐藏状态变化,执行从下向上进入、从上向下离开的滑动动画 + * 2. 支持模式切换(港口工具 ↔ 分公司工具),实现平滑过渡 + * 3. 支持首次加载动画,初始化时自动进入 + * 4. 使用延迟显示模式,确保动画过渡完整可见 * - * 动画效果: - * - 进入动画:从下方 100px 处滑入,同时淡入(y: 100 → 0, opacity: 0 → 1) - * - 离开动画:向下方 100px 处滑出,同时淡出(y: 0 → 100, opacity: 1 → 0) + * 动画参数配置: + * - 进入动画:y: 100 → 0(从下向上滑入),opacity: 0 → 1(淡入) + * 时长:0.5s,缓动:easeOut(先快后慢,营造舒适的进入体验) + * - 离开动画:y: 0 → 100(从上向下滑出),opacity: 1 → 0(淡出) + * 时长:0.3s,缓动:easeIn(先慢后快,让用户感觉到响应迅速) * - * 模式切换流程: - * 1. 旧模式执行离开动画(0.3秒) - * 2. 离开动画完成后,更新 displayedMode 为新模式 - * 3. 设置元素到初始位置(下方 100px) - * 4. 执行新模式的进入动画(0.5秒) + * 模式切换动画流程: + * 1. 旧模式离开(0.3s):向下滑出并淡出 + * 2. 更新 displayedMode 为新模式 + * 3. 新模式进入(0.5s):从下方滑入并淡入 * * @param {boolean} shouldShowPort - 是否应该显示港口工具 * @param {boolean} shouldShowBranchOffice - 是否应该显示分公司工具 * @returns {object} 返回包含以下属性的对象: - * - controls: motion 动画控制器 - * - displayedMode: 延迟显示的模式('port' | 'branchOffice' | null) - * - isVisible: 元素可见性状态 + * - controls: motion 动画控制器,绑定到 motion.div 的 animate 属性 + * - displayedMode: 延迟显示的模式,用于决定渲染 port 还是 branchOffice 内容 + * - isVisible: 元素可见性状态,用于控制内容的显示/隐藏 */ export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice) { - // ==================== 状态和引用管理 ==================== + // ==================== 状态管理 ==================== + // + // 动画时长说明: + // - 进入动画:0.5s,较慢,营造舒适的进入体验 + // - 离开动画:0.3s,较快,让用户感觉到响应迅速 + // + // 缓动函数说明: + // - easeOut:进入时使用,先快后慢,更自然 + // - easeIn:离开时使用,先慢后快,快速消失 + // ==================== // motion 动画控制器,用于控制 motion.div 的动画 const controls = useAnimation(); @@ -43,16 +53,29 @@ export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice) // 延迟显示的模式状态 // 作用:在模式切换时,先显示旧模式,动画完成后再更新为新模式 - // 这样确保用户看到完整的离开动画和进入动画 + // 这样确保用户看到完整的离开动画和进入动画,实现平滑过渡 const [displayedMode, setDisplayedMode] = useState( shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null, ); // 元素可见性状态 // 注意:motion.div 始终挂载,isVisible 只控制内容的显示/隐藏 - // 这样可以保证动画控制器始终有效 + // 这样可以保证动画控制器始终有效,避免重复创建/销毁 const [isVisible, setIsVisible] = useState(false); + // ==================== 主题:监听状态变化并执行动画 ==================== + // + // useEffect 的职责: + // 1. 监听 shouldShowPort 和 shouldShowBranchOffice 的变化 + // 2. 根据不同场景自动执行相应的动画 + // + // 场景说明: + // - 首次渲染:初始化并执行进入动画 + // - 隐藏状态:执行离开动画并隐藏元素 + // - 模式切换:旧模式离开 -> 新模式进入 + // - 显示状态:从隐藏切换到可见 + // ==================== + useEffect(() => { // ==================== 计算当前应该显示的模式 ==================== // 根据 shouldShowPort 和 shouldShowBranchOffice 判断当前应该显示哪个模式 @@ -192,7 +215,6 @@ export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice) }, [shouldShowPort, shouldShowBranchOffice, controls, displayedMode, isVisible]); // ==================== 返回值 ==================== - // 返回动画控制器、延迟显示的模式和可见性状态 return { controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性 displayedMode, // 延迟显示的模式,用于决定渲染 port 还是 branchOffice 内容 diff --git a/src/pages/Container/Map/components/BottomUtils/usePortUtilsAnimation.js b/src/pages/Container/Map/components/BottomUtils/usePortUtilsAnimation.js index c20ac4b..d4d9cb2 100644 --- a/src/pages/Container/Map/components/BottomUtils/usePortUtilsAnimation.js +++ b/src/pages/Container/Map/components/BottomUtils/usePortUtilsAnimation.js @@ -2,52 +2,91 @@ import { useAnimation } from "motion/react"; import { useCallback, useEffect, useRef, useState } from "react"; /** - * PortUtils 父子项动画 Hook + * PortUtils 父子项动画 Hook(港口工具栏) * * 功能说明: - * 1. 实现父级和子级的复杂交互动画 - * 2. 支持点击父级展开/收起子级 - * 3. 初始化时不执行动画,只执行 useBottomUtilsAnimation 的容器动画 - * 4. 首次点击后执行完整的展开/收起动画序列 + * 1. 实现父级和子级的复杂交互动画序列 + * 2. 支持点击父级展开/收起子级,展现流畅的动画效果 + * 3. 初始化时不执行动画,首次点击后执行完整的展开/收起动画序列 + * 4. 使用依次动画(stagger)和并行动画,营造层次感 * - * 动画流程: + * 动画流程详解: * * 【初始显示】 * - 使用 useBottomUtilsAnimation 的主容器动画 - * - 不执行父级的依次显示动画 + * - 不执行父级的依次显示动画,保持静止 * - * 【点击父级展开子级】 - * 1. 所有父级依次下落隐藏(y: 0 → 30, opacity: 1 → 0, x: 0) - * 2. 选中的父级显示回来(y: 30 → 0, opacity: 0 → 1, x: 0) - * 3. 选中的父级移动到最左边(x: 0 → -offsetLeft) - * 4. 子级展开(opacity: 0 → 1) + * 【展开动画序列】(点击父级展开子级) + * 1. 所有父级依次下落隐藏(stagger:0.05s 间隔) + * - y: 0 → 30(向下移动) + * - opacity: 1 → 0(淡出) + * - x: 0(重置水平位置) + * - 单个动画时长:0.1s + * 2. 选中的父级单独显示回来 + * - y: 30 → 0(回到原位) + * - opacity: 0 → 1(淡入) + * - x: 0(保持水平位置) + * - 动画时长:0.1s + * 3. 选中的父级移动到最左边 + * - x: 0 → -offsetLeft(计算目标位置) + * - 缓动:easeInOut(平滑过渡) + * - 动画时长:0.1s + * 4. 子级展开 + * - opacity: 0 → 1(淡入) + * - 延迟:0.05s(等待步骤3完成) + * - 动画时长:0.1s * - * 【点击父级收起子级】 - * 1. 子级收起(opacity: 1 → 0) - * 2. 所有父级在各自当前位置下落(y: 0 → 30, opacity: 1 → 0,保持 x 位置) - * 3. 所有父级回到原位(x: 当前位置 → 0,仍在下方) - * 4. 所有父级依次从下向上显示(y: 30 → 0, opacity: 0 → 1, x: 0) + * 【收起动画序列】(点击父级收起子级) + * 1. 子级收起 + * - opacity: 1 → 0(淡出) + * - 动画时长:0.1s + * 2. 所有父级在各自当前位置下落(并行执行) + * - y: 0 → 30(向下移动) + * - opacity: 1 → 0(淡出) + * - 保持各自的 x 位置(选中的在左边,其他在原位) + * - 动画时长:0.1s + * 3. 所有父级回到原位(并行执行,仍在下方) + * - x: 当前位置 → 0(重置水平位置) + * - 动画时长:0.1s + * 4. 所有父级依次从下向上显示(stagger:0.05s 间隔) + * - y: 30 → 0(回到原位) + * - opacity: 0 → 1(淡入) + * - x: 0(保持水平位置) + * - 单个动画时长:0.1s + * + * 动画配置: + * - duration: 0.1s(单个动画的持续时间) + * - staggerDelay: 0.05s(每个父级的延迟时间,用于依次动画) * * @param {number} parentCount - 父级数量 * @param {number} currentIndex - 当前选中的父级索引(-1 表示未选中) * @param {boolean} isPortMode - 是否是港口模式 * @returns {object} 返回包含以下属性的对象: - * - parentControls: 父级动画控制器数组 - * - childControls: 子级动画控制器 - * - optionRefs: 父级元素引用数组 - * - isAnimating: 是否正在执行动画 - * - hasInteracted: 是否已经交互过 - * - shouldHideInactive: 是否应该隐藏非激活的父级 + * - parentControls: 父级动画控制器数组,每个父级独立控制 + * - childControls: 子级动画控制器,控制子级的展开/收起 + * - optionRefs: 父级元素引用数组,用于计算位置偏移量 + * - isAnimating: 是否正在执行动画,用于防止动画冲突 + * - hasInteracted: 是否已经交互过,首次点击后才执行完整动画 + * - shouldHideInactive: 是否应该隐藏非激活的父级,子级展开时隐藏其他父级 * - animationConfig: 动画配置对象(包含 duration 和 staggerDelay) */ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) { // ==================== 动画配置 ==================== const animationConfig = { duration: 0.1, // 单个动画的持续时间(秒) - staggerDelay: 0.05, // 每个父级的延迟时间(秒),用于依次动画 + staggerDelay: 0.05, // 每个父级的延迟时间(秒),用于依次动画产生波浪效果 }; - // ==================== 状态和引用管理 ==================== + // ==================== 状态管理 ==================== + // + // 动画时长说明: + // - 单个动画:0.1s,非常快,营造敏捷的响应感 + // - stagger 延迟:0.05s,相邻父级之间的时间间隔,产生波浪效果 + // + // 并行 vs 依次: + // - 并行动画:多个动画同时执行(如所有父级同时回到原位) + // - 依次动画:动画按顺序执行(如父级依次下落,每个间隔 0.05s) + // ==================== // 标记是否正在执行动画 // 作用:防止在动画执行过程中触发新的动画,避免动画冲突 @@ -58,17 +97,17 @@ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) { const [hasInteracted, setHasInteracted] = useState(false); // 标记是否应该隐藏非激活的父级 - // 作用:当子级展开时,隐藏未选中的父级,避免干扰 + // 作用:当子级展开时,隐藏未选中的父级,避免视觉干扰 // 在动画执行期间设置为 false,让所有父级参与动画 // 动画完成后设置为 true,隐藏未选中的父级 const [shouldHideInactive, setShouldHideInactive] = useState(false); // 父级元素的 ref 数组 - // 作用:获取父级 DOM 元素,用于计算位置偏移量 + // 作用:获取父级 DOM 元素,用于计算位置偏移量(offsetLeft) const optionRefs = useRef([]); // 为每个父级创建独立的动画控制器 - // 作用:每个父级可以独立控制动画 + // 作用:每个父级可以独立控制动画,实现依次动画效果 const parentControls = Array.from({ length: parentCount }, () => useAnimation()); // 子级的动画控制器 @@ -76,12 +115,28 @@ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) { const childControls = useAnimation(); // ==================== 初始化 ==================== + // 设置子级为初始状态(隐藏),确保一开始不可见 useEffect(() => { - // 设置子级为初始状态(隐藏) - // 作用:确保子级一开始不可见 childControls.set({ opacity: 0 }); }, []); + // ==================== 主题:展开/收起动画序列 ==================== + // + // 展开动画(performExpandAnimation): + // 1. 所有父级依次下落隐藏(stagger 效果) + // 2. 选中的父级单独显示回来 + // 3. 选中的父级移动到最左边 + // 4. 子级展开 + // + // 收起动画(performCollapseAnimation): + // 1. 子级收起 + // 2. 所有父级在各自当前位置下落(并行) + // 3. 所有父级回到原位(并行) + // 4. 所有父级依次从下向上显示(stagger 效果) + // + // 使用 useCallback 缓存函数,避免不必要的重建 + // ==================== + // ==================== 展开动画序列 ==================== const performExpandAnimation = useCallback(async () => { if (isAnimating) @@ -253,14 +308,13 @@ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) { }, [currentIndex, isPortMode]); // ==================== 返回值 ==================== - // 返回动画控制器和相关状态,供组件使用 return { - parentControls, // 父级动画控制器数组 - childControls, // 子级动画控制器 - optionRefs, // 父级元素引用数组 - isAnimating, // 是否正在执行动画 - hasInteracted, // 是否已经交互过 - shouldHideInactive, // 是否应该隐藏非激活的父级 + parentControls, // 父级动画控制器数组,每个父级独立控制 + childControls, // 子级动画控制器,控制子级的展开/收起 + optionRefs, // 父级元素引用数组,用于计算位置偏移量 + isAnimating, // 是否正在执行动画,用于防止动画冲突 + hasInteracted, // 是否已经交互过,首次点击后才执行完整动画 + shouldHideInactive, // 是否应该隐藏非激活的父级,子级展开时隐藏其他父级 animationConfig, // 动画配置对象(包含 duration 和 staggerDelay) }; } diff --git a/src/pages/Container/Map/components/CenterUtils/useCenterUtilsAnimation.js b/src/pages/Container/Map/components/CenterUtils/useCenterUtilsAnimation.js index a571f77..237fa7e 100644 --- a/src/pages/Container/Map/components/CenterUtils/useCenterUtilsAnimation.js +++ b/src/pages/Container/Map/components/CenterUtils/useCenterUtilsAnimation.js @@ -5,37 +5,35 @@ import { useEffect, useRef, useState } from "react"; * CenterUtils 弹跳动画 Hook * * 功能说明: - * 1. 监听组件的显示/隐藏状态变化 - * 2. 执行炫酷的弹跳动画效果(类似 animate.css 的 bounceIn/bounceOut) - * 3. 支持多种动画类型(上下弹跳、缩放弹跳) - * 4. 使用弹跳、缩放、透明度三重动画效果,让动画更加生动 - * 5. 延迟隐藏机制,确保离开动画完整播放 + * 1. 监听组件的显示/隐藏状态变化,执行炫酷的弹跳动画效果 + * 2. 支持两种动画类型:上下弹跳(up-down)和缩放弹跳(scale) + * 3. 使用位置、缩放、透明度三重动画效果,营造生动的视觉体验 + * 4. 延迟隐藏机制,确保离开动画完整播放后再移除组件 * - * 动画效果详解(上下弹跳模式 up-down): - * - 进入动画: - * - 位置:从上方 120px 处冲到下方 40px(超出目标),轻微上弹到 -10px,最后稳定到 0 - * - 缩放:从 0.9 倍缩小到 1.05 倍(轻微放大),轻微缩小到 0.98,回到正常 1.0 - * - 透明度:从透明到不透明 - * - 时长:0.7 秒 - * - 缓动:使用强弹性贝塞尔曲线 [0.34, 1.56, 0.64, 1],产生明显的过冲和回弹效果 + * 动画参数配置: + * - up-down 模式进入动画: + * - 位置关键帧:y: [-120, 40, -10, 0](从上方120px -> 冲到下方40px -> 轻微上弹 -> 稳定) + * - 缩放关键帧:scale: [0.9, 1.05, 0.98, 1](90% -> 105% -> 98% -> 100%) + * - 透明度关键帧:opacity: [0, 1, 1, 1](透明 -> 不透明) + * - 时长:0.7s,缓动:[0.34, 1.56, 0.64, 1](强弹性曲线,产生过冲和回弹) + * - 关键帧时间点:times: [0, 0.5, 0.75, 1] * - * - 离开动画: - * - 位置:从正常位置向上弹跳 50px,回落到 20px,然后向上离开到 -120px - * - 缩放:轻微放大到 1.02,然后缩小到 0.85 - * - 透明度:保持可见再淡出 - * - 时长:0.5 秒 - * - 缓动:使用超弹性曲线 [0.68, -0.6, 0.32, 1.6],产生夸张的反向弹跳 + * - up-down 模式离开动画: + * - 位置关键帧:y: [0, -50, 20, -120](正常 -> 向上弹跳50px -> 回落到20px -> 向上离开到-120px) + * - 缩放关键帧:scale: [1, 1.02, 0.95, 0.85](100% -> 102% -> 95% -> 85%) + * - 透明度关键帧:opacity: [1, 1, 0.8, 0](保持可见 -> 淡出) + * - 时长:0.5s,缓动:[0.68, -0.6, 0.32, 1.6](超弹性曲线,产生夸张反向弹跳) + * - 关键帧时间点:times: [0, 0.2, 0.5, 1] * - * 动画效果详解(缩放弹跳模式 scale): - * - 进入动画: - * - 缩放:从 0 放大到 1.15 倍(超出目标),缩小到 0.92,回到正常 1.0 - * - 时长:0.7 秒 - * - 缓动:使用强弹性贝塞尔曲线 + * - scale 模式进入动画: + * - 缩放关键帧:scale: [0, 1.15, 0.92, 1](0 -> 115% -> 92% -> 100%) + * - 时长:0.7s,缓动:[0.34, 1.56, 0.64, 1] + * - 关键帧时间点:times: [0, 0.4, 0.7, 1] * - * - 离开动画: - * - 缩放:从 1.0 放大到 1.1,缩小到 0.85,最后消失到 0 - * - 时长:0.5 秒 - * - 缓动:使用超弹性曲线 + * - scale 模式离开动画: + * - 缩放关键帧:scale: [1, 1.1, 0.85, 0](100% -> 110% -> 85% -> 0) + * - 时长:0.5s,缓动:[0.68, -0.6, 0.32, 1.6] + * - 关键帧时间点:times: [0, 0.3, 0.6, 1] * * @param {boolean} isVisible - 控制组件的显示/隐藏 * - true: 组件应该显示,执行进入动画 @@ -45,23 +43,46 @@ import { useEffect, useRef, useState } from "react"; * - 'scale':缩放弹跳动画(从中心放大进入,缩小离开) * @returns {object} 返回包含以下属性的对象: * - controls: motion 动画控制器,绑定到 motion.div 的 animate 属性 - * - isVisible: 延迟的可见性状态(用于等待离开动画完成后再隐藏组件) + * - isVisible: 延迟的可见性状态,用于条件渲染(等待离开动画完成) */ export function useCenterUtilsAnimation(isVisible, type = "up-down") { - // ==================== 状态和引用管理 ==================== + // ==================== 状态管理 ==================== + // + // 动画时长说明: + // - 进入动画:0.7s,较慢,展示完整的弹跳效果 + // - 离开动画:0.5s,较快,让用户感觉到响应迅速 + // + // 缓动函数说明: + // - 进入动画:[0.34, 1.56, 0.64, 1],强弹性曲线,产生明显的过冲和回弹 + // - 离开动画:[0.68, -0.6, 0.32, 1.6],超弹性曲线,产生夸张的反向弹跳 + // + // 三重动画效果: + // - 位置动画(y)/ 缩放动画(scale):产生弹跳的物理运动效果 + // - 透明度动画(opacity):平滑的进入和离开 + // ==================== // motion 动画控制器,用于控制 motion.div 的动画 const controls = useAnimation(); // 标记是否是首次渲染 // 作用:首次渲染时需要特殊处理,确保初始状态正确设置 - // 首次渲染不执行离开动画,直接执行进入动画 const isFirstRender = useRef(true); // 组件的实际显示状态 // 作用:延迟隐藏,确保离开动画执行完成后才将组件从 DOM 中移除 const [displayState, setDisplayState] = useState(isVisible); + // ==================== 主题:监听可见性变化并执行弹跳动画 ==================== + // + // useEffect 的职责: + // 1. 监听 isVisible 和 type 的变化 + // 2. 根据显示/隐藏状态执行相应的弹跳动画 + // + // 场景说明: + // - 显示状态:根据 type 执行 up-down 或 scale 弹跳进入动画 + // - 隐藏状态:根据 type 执行 up-down 或 scale 弹跳离开动画 + // ==================== + useEffect(() => { // ==================== 显示状态:执行进入动画 ==================== if (isVisible) { @@ -176,11 +197,9 @@ export function useCenterUtilsAnimation(isVisible, type = "up-down") { } }, [isVisible, type, controls, displayState]); - // 返回动画控制器和延迟的可见性状态 - // - controls:用于绑定到 motion.div 的 animate 属性 - // - isVisible:延迟的可见性状态,用于条件渲染(等待离开动画完成) + // ==================== 返回值 ==================== return { - controls, - isVisible: displayState, + controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性 + isVisible: displayState, // 延迟的可见性状态,用于条件渲染(等待离开动画完成) }; }