地图添加过渡动画
parent
efc735f01d
commit
6d2e201730
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { CSSTransition } from "react-transition-group";
|
|
||||||
import bg7 from "~/assets/images/map_bi/bottom_utils/bg7.png";
|
import bg7 from "~/assets/images/map_bi/bottom_utils/bg7.png";
|
||||||
import bg8 from "~/assets/images/map_bi/bottom_utils/bg8.png";
|
import bg8 from "~/assets/images/map_bi/bottom_utils/bg8.png";
|
||||||
import titleImg from "~/assets/images/map_bi/bottom_utils/title.png";
|
import titleImg from "~/assets/images/map_bi/bottom_utils/title.png";
|
||||||
|
|
@ -9,11 +9,13 @@ import { Context } from "~/pages/Container/Map/js/context";
|
||||||
import mitt from "~/pages/Container/Map/js/mitt";
|
import mitt from "~/pages/Container/Map/js/mitt";
|
||||||
import {
|
import {
|
||||||
deletePeoplePositionPointMittKey,
|
deletePeoplePositionPointMittKey,
|
||||||
initBottomUtilsMittKey,
|
|
||||||
resetAllBottomUtilsCheckMittKey,
|
resetAllBottomUtilsCheckMittKey,
|
||||||
resetBottomCurrentIndexMittKey,
|
resetBottomCurrentIndexMittKey,
|
||||||
} from "~/pages/Container/Map/js/mittKey";
|
} from "~/pages/Container/Map/js/mittKey";
|
||||||
import { portUtilsList } from "./portUtilsList";
|
import { portUtilsList } from "./portUtilsList";
|
||||||
|
import { useBottomUtilsAnimation } from "./useBottomUtilsAnimation";
|
||||||
|
import { useBranchOfficeUtilsAnimation } from "./useBranchOfficeUtilsAnimation";
|
||||||
|
import { usePortUtilsAnimation } from "./usePortUtilsAnimation";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
function BottomUtils(props) {
|
function BottomUtils(props) {
|
||||||
|
|
@ -21,9 +23,33 @@ function BottomUtils(props) {
|
||||||
|
|
||||||
const [list, setList] = useState([]);
|
const [list, setList] = useState([]);
|
||||||
|
|
||||||
const initList = () => {
|
const {
|
||||||
setList(!currentBranchOffice ? portUtilsList : branchOfficeUtilsList);
|
controls: bottomUtilsControls,
|
||||||
};
|
displayedMode: bottomUtilsDisplayedMode,
|
||||||
|
isVisible: bottomUtilsIsVisible,
|
||||||
|
} = useBottomUtilsAnimation(
|
||||||
|
!pureMap && currentPort && !currentBranchOffice,
|
||||||
|
!pureMap && currentPort && currentBranchOffice,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
containerVariants: branchOfficeUtilsContainerVariants,
|
||||||
|
itemVariants: branchOfficeUtilsChildItemVariants,
|
||||||
|
} = useBranchOfficeUtilsAnimation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
parentControls: portUtilsParentControls,
|
||||||
|
childControls: portUtilsChildControls,
|
||||||
|
optionRefs: portUtilsOptionRefs,
|
||||||
|
isAnimating: portUtilsIsAnimating,
|
||||||
|
hasInteracted: portUtilsHasInteracted,
|
||||||
|
shouldHideInactive: portUtilsShouldHideInactive,
|
||||||
|
animationConfig: portUtilsAnimationConfig,
|
||||||
|
} = usePortUtilsAnimation(
|
||||||
|
portUtilsList.length,
|
||||||
|
bottomUtilsCurrentIndex,
|
||||||
|
bottomUtilsDisplayedMode === "port",
|
||||||
|
);
|
||||||
|
|
||||||
const resetAllCheck = () => {
|
const resetAllCheck = () => {
|
||||||
setList((prevList) => {
|
setList((prevList) => {
|
||||||
|
|
@ -35,14 +61,16 @@ function BottomUtils(props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mitt.on(initBottomUtilsMittKey, () => {
|
if (bottomUtilsDisplayedMode === "port") {
|
||||||
initList();
|
setList(portUtilsList);
|
||||||
});
|
}
|
||||||
|
else if (bottomUtilsDisplayedMode === "branchOffice") {
|
||||||
return () => {
|
setList(branchOfficeUtilsList);
|
||||||
mitt.off(initBottomUtilsMittKey);
|
}
|
||||||
};
|
else {
|
||||||
}, [currentBranchOffice]);
|
setList([]);
|
||||||
|
}
|
||||||
|
}, [bottomUtilsDisplayedMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mitt.on(resetBottomCurrentIndexMittKey, () => {
|
mitt.on(resetBottomCurrentIndexMittKey, () => {
|
||||||
|
|
@ -74,16 +102,65 @@ function BottomUtils(props) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBranchOfficeUtils = () => {
|
const renderPortUtils = () => {
|
||||||
return (
|
if (bottomUtilsDisplayedMode === "port") {
|
||||||
<div className="bottom_utils branch_office">
|
return list.map((item, index) => {
|
||||||
{list.map((item, index) => {
|
|
||||||
const isCurrentActive = bottomUtilsCurrentIndex === index;
|
const isCurrentActive = bottomUtilsCurrentIndex === index;
|
||||||
// const hasActiveChildren = bottomUtilsCurrentIndex !== -1;
|
const hasActiveChildren = bottomUtilsCurrentIndex !== -1;
|
||||||
// if (hasActiveChildren && !isCurrentActive) {
|
const isAllowClick = hasActiveChildren && !isCurrentActive && portUtilsShouldHideInactive;
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
ref={el => (portUtilsOptionRefs.current[index] = el)}
|
||||||
|
className="option"
|
||||||
|
animate={portUtilsHasInteracted ? portUtilsParentControls[index] : false}
|
||||||
|
onClick={() => !portUtilsIsAnimating && !isAllowClick && optionsClick(index)}
|
||||||
|
style={{
|
||||||
|
cursor: isAllowClick ? "default" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={isCurrentActive ? item.checkImg : item.img} alt="" className="img" />
|
||||||
|
<div className="label" style={{ backgroundImage: `url(${isCurrentActive ? titleOnImg : titleImg})` }}>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
{isCurrentActive && (
|
||||||
|
<motion.div
|
||||||
|
className="child_items"
|
||||||
|
animate={portUtilsChildControls}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
>
|
||||||
|
{item.list?.map((item1, index1) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={index1}
|
||||||
|
className="child_item"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: index1 * portUtilsAnimationConfig.staggerDelay }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
optionsItemsClick(index, index1, item, item1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={item1.check ? item1.checkImg : item1.img} alt="" className="child_img" />
|
||||||
|
<div className="child_label">{item1.label}</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBranchOfficeUtils = () => {
|
||||||
|
if (bottomUtilsDisplayedMode === "branchOffice") {
|
||||||
|
return (
|
||||||
|
list.map((item, index) => {
|
||||||
|
const isCurrentActive = bottomUtilsCurrentIndex === index;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
|
|
@ -92,92 +169,44 @@ function BottomUtils(props) {
|
||||||
onClick={() => optionsClick(index)}
|
onClick={() => optionsClick(index)}
|
||||||
>
|
>
|
||||||
<div className="label">{item.label}</div>
|
<div className="label">{item.label}</div>
|
||||||
<CSSTransition
|
<AnimatePresence>
|
||||||
timeout={500}
|
{(isCurrentActive && item.list?.length > 0) && (
|
||||||
classNames={{
|
<motion.div className="items" initial="hidden" animate="visible" exit="hidden" variants={branchOfficeUtilsContainerVariants}>
|
||||||
enter: "animate__animated animate__faster",
|
{item.list?.map((item1, index1) => {
|
||||||
enterActive: "animate__animated animate__faster animate__fadeInUp",
|
return (
|
||||||
exit: "animate__animated animate__faster",
|
<motion.div
|
||||||
exitActive: "animate__animated animate__faster animate__fadeOutDown",
|
|
||||||
}}
|
|
||||||
unmountOnExit
|
|
||||||
in={(isCurrentActive && item.list?.length > 0)}
|
|
||||||
>
|
|
||||||
<div className="items">
|
|
||||||
{item.list?.map((item1, index1) => (
|
|
||||||
<div
|
|
||||||
key={index1}
|
key={index1}
|
||||||
className={`item ${item1.check ? "active" : ""}`}
|
className={`item ${item1.check ? "active" : ""}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
optionsItemsClick(index, index1, item, item1);
|
optionsItemsClick(index, index1, item, item1);
|
||||||
}}
|
}}
|
||||||
|
variants={branchOfficeUtilsChildItemVariants}
|
||||||
>
|
>
|
||||||
<div className="child_label">{item1.label}</div>
|
<div className="child_label">{item1.label}</div>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPortUtils = () => {
|
|
||||||
return (
|
|
||||||
<div className="bottom_utils port">
|
|
||||||
{list.map((item, index) => {
|
|
||||||
const isCurrentActive = bottomUtilsCurrentIndex === index;
|
|
||||||
const hasActiveChildren = bottomUtilsCurrentIndex !== -1;
|
|
||||||
if (hasActiveChildren && !isCurrentActive) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="option" onClick={() => optionsClick(index)}>
|
|
||||||
<img src={isCurrentActive ? item.checkImg : item.img} alt="" className="img" />
|
|
||||||
<div
|
|
||||||
className="label"
|
|
||||||
style={{ backgroundImage: `url(${isCurrentActive ? titleOnImg : titleImg})` }}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</div>
|
|
||||||
{(isCurrentActive && item.list?.length > 0) && (
|
|
||||||
<div className="items">
|
|
||||||
{item.list?.map((item1, index1) => (
|
|
||||||
<div
|
|
||||||
key={index1}
|
|
||||||
className="item"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
optionsItemsClick(index, index1, item, item1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={item1.check ? item1.checkImg : item1.img} alt="" className="child_img" />
|
|
||||||
<div className="child_label">{item1.label}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const renderUtils = () => {
|
|
||||||
if (pureMap || !currentPort)
|
|
||||||
return (<div></div>);
|
|
||||||
|
|
||||||
return (!currentBranchOffice ? renderPortUtils() : renderBranchOfficeUtils());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map_content_bottom_utils_container">
|
<div className="map_content_bottom_utils_container">
|
||||||
{renderUtils()}
|
<motion.div className={bottomUtilsDisplayedMode === "port" ? "bottom_utils port" : "bottom_utils branch_office"} animate={bottomUtilsControls}>
|
||||||
|
{bottomUtilsIsVisible && (
|
||||||
|
<>
|
||||||
|
{renderPortUtils()}
|
||||||
|
{renderBranchOfficeUtils()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.child_items {
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
border-right: none;
|
border-right: none;
|
||||||
padding: 6px 50px;
|
padding: 6px 50px;
|
||||||
|
|
||||||
.item {
|
.child_item {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BottomUtils 组件动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 监听显示/隐藏状态变化
|
||||||
|
* 2. 执行从下向上进入、从上向下离开的动画效果
|
||||||
|
* 3. 支持模式切换(港口工具 ↔ 分公司工具)
|
||||||
|
* 4. 支持首次加载动画
|
||||||
|
*
|
||||||
|
* 动画效果:
|
||||||
|
* - 进入动画:从下方 100px 处滑入,同时淡入(y: 100 → 0, opacity: 0 → 1)
|
||||||
|
* - 离开动画:向下方 100px 处滑出,同时淡出(y: 0 → 100, opacity: 1 → 0)
|
||||||
|
*
|
||||||
|
* 模式切换流程:
|
||||||
|
* 1. 旧模式执行离开动画(0.3秒)
|
||||||
|
* 2. 离开动画完成后,更新 displayedMode 为新模式
|
||||||
|
* 3. 设置元素到初始位置(下方 100px)
|
||||||
|
* 4. 执行新模式的进入动画(0.5秒)
|
||||||
|
*
|
||||||
|
* @param {boolean} shouldShowPort - 是否应该显示港口工具
|
||||||
|
* @param {boolean} shouldShowBranchOffice - 是否应该显示分公司工具
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - controls: motion 动画控制器
|
||||||
|
* - displayedMode: 延迟显示的模式('port' | 'branchOffice' | null)
|
||||||
|
* - isVisible: 元素可见性状态
|
||||||
|
*/
|
||||||
|
export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice) {
|
||||||
|
// ==================== 状态和引用管理 ====================
|
||||||
|
|
||||||
|
// motion 动画控制器,用于控制 motion.div 的动画
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
// 标记是否是首次渲染
|
||||||
|
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
// 标记是否正在执行动画
|
||||||
|
// 作用:防止在动画执行过程中触发新的动画,避免动画冲突
|
||||||
|
const isAnimating = useRef(false);
|
||||||
|
|
||||||
|
// 延迟显示的模式状态
|
||||||
|
// 作用:在模式切换时,先显示旧模式,动画完成后再更新为新模式
|
||||||
|
// 这样确保用户看到完整的离开动画和进入动画
|
||||||
|
const [displayedMode, setDisplayedMode] = useState(
|
||||||
|
shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 元素可见性状态
|
||||||
|
// 注意:motion.div 始终挂载,isVisible 只控制内容的显示/隐藏
|
||||||
|
// 这样可以保证动画控制器始终有效
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ==================== 计算当前应该显示的模式 ====================
|
||||||
|
// 根据 shouldShowPort 和 shouldShowBranchOffice 判断当前应该显示哪个模式
|
||||||
|
const currentMode = shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null;
|
||||||
|
|
||||||
|
// ==================== 首次渲染 ====================
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
if (currentMode) {
|
||||||
|
// ===== 步骤 1:立即更新显示的模式 =====
|
||||||
|
// 设置 displayedMode 为当前模式,让组件知道应该渲染哪种内容
|
||||||
|
setDisplayedMode(currentMode);
|
||||||
|
|
||||||
|
// ===== 步骤 2:设置初始动画状态 =====
|
||||||
|
// 此时 motion.div 还没有挂载(因为 isVisible 还是 false)
|
||||||
|
// controls.set() 会将初始状态保存到 controls 对象中
|
||||||
|
// 当 motion.div 挂载时,会读取这个初始状态
|
||||||
|
controls.set({
|
||||||
|
y: 100, // Y 轴位置:从下方 100px 处开始
|
||||||
|
opacity: 0, // 透明度:完全透明
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 3:显示元素,触发 motion.div 挂载 =====
|
||||||
|
// 设置 isVisible 为 true,触发组件重渲染
|
||||||
|
// motion.div 会挂载到 DOM,并读取 controls 的初始状态 { y: 100, opacity: 0 }
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// ===== 步骤 4:执行进入动画 =====
|
||||||
|
// 从初始位置(y: 100, opacity: 0)动画到正常位置(y: 0, opacity: 1)
|
||||||
|
controls.start({
|
||||||
|
y: 0, // 移动到正常位置(Y 轴 0)
|
||||||
|
opacity: 1, // 变为完全不透明
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 动画时长:0.5 秒
|
||||||
|
ease: "easeOut", // 缓动函数:快速开始,缓慢结束
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// ===== 步骤 5:动画完成,标记首次渲染结束 =====
|
||||||
|
isFirstRender.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 如果首次渲染时不需要显示(currentMode 为 null)
|
||||||
|
// 直接标记首次渲染完成,不需要执行动画
|
||||||
|
isFirstRender.current = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 隐藏状态(当前没有工具应该显示)====================
|
||||||
|
if (!currentMode) {
|
||||||
|
if (isVisible && !isAnimating.current) {
|
||||||
|
// ===== 执行离开动画 =====
|
||||||
|
isAnimating.current = true;
|
||||||
|
controls.start({
|
||||||
|
y: 100, // 向下移动到 100px 处
|
||||||
|
opacity: 0, // 同时淡出
|
||||||
|
transition: {
|
||||||
|
duration: 0.3, // 动画时长:0.3 秒(比进入动画短,显得更轻快)
|
||||||
|
ease: "easeIn", // 缓动函数:缓慢开始,快速结束
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// ===== 离开动画完成后,隐藏元素 =====
|
||||||
|
setIsVisible(false); // 隐藏内容(motion.div 仍然挂载)
|
||||||
|
setDisplayedMode(null); // 清空显示的模式
|
||||||
|
isAnimating.current = false; // 解除动画锁定
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 模式切换或显示 ====================
|
||||||
|
|
||||||
|
// 检查模式是否发生了变化(港口 ↔ 分公司)
|
||||||
|
const modeChanged = displayedMode !== currentMode;
|
||||||
|
|
||||||
|
// ===== 场景 1:模式切换(港口 ↔ 分公司)=====
|
||||||
|
if (modeChanged && !isAnimating.current && isVisible) {
|
||||||
|
isAnimating.current = true;
|
||||||
|
|
||||||
|
// -------- 步骤 1:执行离开动画(旧模式离开)--------
|
||||||
|
controls.start({
|
||||||
|
y: 100, // 向下移动到 100px 处
|
||||||
|
opacity: 0, // 同时淡出
|
||||||
|
transition: {
|
||||||
|
duration: 0.3, // 动画时长:0.3 秒
|
||||||
|
ease: "easeIn", // 缓动函数
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// -------- 步骤 2:离开动画完成后,更新显示的模式 --------
|
||||||
|
setDisplayedMode(currentMode);
|
||||||
|
|
||||||
|
// -------- 步骤 3:设置元素到初始位置(下方外部)--------
|
||||||
|
controls.set({
|
||||||
|
y: 100, // 确保元素在下方 100px 处
|
||||||
|
opacity: 0, // 确保元素是透明的
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------- 步骤 4:执行进入动画(新模式进入)--------
|
||||||
|
controls.start({
|
||||||
|
y: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 动画时长:0.5 秒
|
||||||
|
ease: "easeOut", // 缓动函数
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// -------- 步骤 5:动画完成 --------
|
||||||
|
isAnimating.current = false; // 解除动画锁定
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 场景 2:元素不可见,但应该显示(例如:从隐藏状态切换到显示)=====
|
||||||
|
else if (!isVisible && currentMode) {
|
||||||
|
// -------- 步骤 1:显示元素 --------
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// -------- 步骤 2:更新显示的模式 --------
|
||||||
|
setDisplayedMode(currentMode);
|
||||||
|
|
||||||
|
// -------- 步骤 3:设置元素到初始位置 --------
|
||||||
|
controls.set({
|
||||||
|
y: 100, // 下方 100px 处
|
||||||
|
opacity: 0, // 透明
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------- 步骤 4:执行进入动画 --------
|
||||||
|
controls.start({
|
||||||
|
y: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 动画时长:0.5 秒
|
||||||
|
ease: "easeOut", // 缓动函数
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [shouldShowPort, shouldShowBranchOffice, controls, displayedMode, isVisible]);
|
||||||
|
|
||||||
|
// ==================== 返回值 ====================
|
||||||
|
// 返回动画控制器、延迟显示的模式和可见性状态
|
||||||
|
return {
|
||||||
|
controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
|
||||||
|
displayedMode, // 延迟显示的模式,用于决定渲染 port 还是 branchOffice 内容
|
||||||
|
isVisible, // 元素可见性状态,用于控制内容的显示/隐藏
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* BottomUtils 子项动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 为子项列表提供从下向上进入、从上向下离开的动画效果
|
||||||
|
* 2. 子项依次出现的波浪式动画(stagger 效果)
|
||||||
|
* 3. 离开时反向依次消失
|
||||||
|
*
|
||||||
|
* 动画效果:
|
||||||
|
* - 进入动画:每个子项从下方 50px 处滑入,同时淡入(y: 50 → 0, opacity: 0 → 1)
|
||||||
|
* - 离开动画:每个子项向下方 50px 处滑出,同时淡出(y: 0 → 50, opacity: 1 → 0)
|
||||||
|
*
|
||||||
|
* Stagger 效果:
|
||||||
|
* - 进入时(visible):子项从第一个到最后一个依次出现,每个间隔 0.05 秒
|
||||||
|
* - 离开时(hidden):子项从最后一个到第一个依次消失,每个间隔 0.03 秒(更快)
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* 1. 在父组件中使用 AnimatePresence 包裹子项容器
|
||||||
|
* 2. 在子项容器上设置 initial="hidden"、animate="visible"、exit="hidden"
|
||||||
|
* 3. 在子项容器上使用 containerVariants
|
||||||
|
* 4. 在每个子项上使用 itemVariants
|
||||||
|
*
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - containerVariants: 容器的动画变体(用于 motion.div)
|
||||||
|
* - itemVariants: 子项的动画变体(用于每个子项)
|
||||||
|
*/
|
||||||
|
export function useBranchOfficeUtilsAnimation() {
|
||||||
|
// ==================== 容器动画变体 ====================
|
||||||
|
// 作用:控制整个子项容器的动画和子项的 stagger 效果
|
||||||
|
const containerVariants = {
|
||||||
|
// 隐藏状态
|
||||||
|
// - 用途 1:作为初始状态(initial="hidden")
|
||||||
|
// - 用途 2:作为离开状态(exit="hidden")
|
||||||
|
// - 当同时定义 hidden.transition 和 visible.transition 时
|
||||||
|
// Framer Motion 会根据当前是进入还是离开自动选择对应的 transition
|
||||||
|
hidden: {
|
||||||
|
opacity: 0, // 容器整体透明
|
||||||
|
y: 200, // 容器从下方 200px 处开始/离开到下方 200px
|
||||||
|
// 离开时的 transition 配置(当 exit="hidden" 时使用)
|
||||||
|
// 进入时会使用 visible.transition,所以这里只配置离开相关的参数
|
||||||
|
transition: {
|
||||||
|
duration: 0.2, // 容器离开动画时长:0.2 秒(比进入快)
|
||||||
|
ease: "easeIn", // 容器离开缓动函数
|
||||||
|
// 子项依次离开的反向波浪效果
|
||||||
|
staggerChildren: 0.03, // 离开时间隔更短(0.03 秒),显得更轻快
|
||||||
|
staggerDirection: -1, // -1 = 反向(从最后一个到第一个)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 可见状态(进入动画)
|
||||||
|
// - 当组件挂载后立即使用(animate="visible")
|
||||||
|
visible: {
|
||||||
|
opacity: 1, // 容器整体不透明
|
||||||
|
y: 0, // 容器移动到正常位置
|
||||||
|
transition: {
|
||||||
|
duration: 0.3, // 容器动画时长:0.3 秒
|
||||||
|
ease: "easeOut", // 容器缓动函数
|
||||||
|
// 子项依次出现的波浪效果(进入时使用)
|
||||||
|
staggerChildren: 0.05, // 每个子项间隔 0.05 秒依次出现
|
||||||
|
staggerDirection: 1, // 1 = 正向(从第一个到最后一个)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 子项动画变体 ====================
|
||||||
|
// 作用:控制单个子项的进入和离开动画
|
||||||
|
const itemVariants = {
|
||||||
|
// 隐藏状态
|
||||||
|
// - 作为初始状态(initial="hidden")
|
||||||
|
// - 作为离开的目标状态(exit="hidden" 时子项会动画到此状态)
|
||||||
|
hidden: {
|
||||||
|
y: 50, // Y 轴位置:从下方 50px 处开始/离开到下方 50px
|
||||||
|
opacity: 0, // 透明度:完全透明
|
||||||
|
},
|
||||||
|
|
||||||
|
// 可见状态
|
||||||
|
// - 作为进入的目标状态(animate="visible" 时子项会动画到此状态)
|
||||||
|
visible: {
|
||||||
|
y: 0, // Y 轴位置:移动到正常位置
|
||||||
|
opacity: 1, // 透明度:变为完全不透明
|
||||||
|
transition: {
|
||||||
|
duration: 0.3, // 动画时长:0.3 秒
|
||||||
|
ease: "easeOut", // 缓动函数:快速开始,缓慢结束
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 返回值 ====================
|
||||||
|
// 返回容器和子项的动画变体
|
||||||
|
return {
|
||||||
|
containerVariants, // 容器的动画变体,用于 motion.div 的 variants 属性
|
||||||
|
itemVariants, // 子项的动画变体,用于每个子项 motion.div 的 variants 属性
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PortUtils 父子项动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 实现父级和子级的复杂交互动画
|
||||||
|
* 2. 支持点击父级展开/收起子级
|
||||||
|
* 3. 初始化时不执行动画,只执行 useBottomUtilsAnimation 的容器动画
|
||||||
|
* 4. 首次点击后执行完整的展开/收起动画序列
|
||||||
|
*
|
||||||
|
* 动画流程:
|
||||||
|
*
|
||||||
|
* 【初始显示】
|
||||||
|
* - 使用 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. 子级收起(opacity: 1 → 0)
|
||||||
|
* 2. 所有父级在各自当前位置下落(y: 0 → 30, opacity: 1 → 0,保持 x 位置)
|
||||||
|
* 3. 所有父级回到原位(x: 当前位置 → 0,仍在下方)
|
||||||
|
* 4. 所有父级依次从下向上显示(y: 30 → 0, opacity: 0 → 1, x: 0)
|
||||||
|
*
|
||||||
|
* @param {number} parentCount - 父级数量
|
||||||
|
* @param {number} currentIndex - 当前选中的父级索引(-1 表示未选中)
|
||||||
|
* @param {boolean} isPortMode - 是否是港口模式
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - parentControls: 父级动画控制器数组
|
||||||
|
* - childControls: 子级动画控制器
|
||||||
|
* - optionRefs: 父级元素引用数组
|
||||||
|
* - isAnimating: 是否正在执行动画
|
||||||
|
* - hasInteracted: 是否已经交互过
|
||||||
|
* - shouldHideInactive: 是否应该隐藏非激活的父级
|
||||||
|
* - animationConfig: 动画配置对象(包含 duration 和 staggerDelay)
|
||||||
|
*/
|
||||||
|
export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) {
|
||||||
|
// ==================== 动画配置 ====================
|
||||||
|
const animationConfig = {
|
||||||
|
duration: 0.1, // 单个动画的持续时间(秒)
|
||||||
|
staggerDelay: 0.05, // 每个父级的延迟时间(秒),用于依次动画
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 状态和引用管理 ====================
|
||||||
|
|
||||||
|
// 标记是否正在执行动画
|
||||||
|
// 作用:防止在动画执行过程中触发新的动画,避免动画冲突
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
|
// 标记是否已经交互过
|
||||||
|
// 作用:初始化时不执行动画,首次点击后设置为 true,之后才执行完整动画
|
||||||
|
const [hasInteracted, setHasInteracted] = useState(false);
|
||||||
|
|
||||||
|
// 标记是否应该隐藏非激活的父级
|
||||||
|
// 作用:当子级展开时,隐藏未选中的父级,避免干扰
|
||||||
|
// 在动画执行期间设置为 false,让所有父级参与动画
|
||||||
|
// 动画完成后设置为 true,隐藏未选中的父级
|
||||||
|
const [shouldHideInactive, setShouldHideInactive] = useState(false);
|
||||||
|
|
||||||
|
// 父级元素的 ref 数组
|
||||||
|
// 作用:获取父级 DOM 元素,用于计算位置偏移量
|
||||||
|
const optionRefs = useRef([]);
|
||||||
|
|
||||||
|
// 为每个父级创建独立的动画控制器
|
||||||
|
// 作用:每个父级可以独立控制动画
|
||||||
|
const parentControls = Array.from({ length: parentCount }, () => useAnimation());
|
||||||
|
|
||||||
|
// 子级的动画控制器
|
||||||
|
// 作用:控制子级的展开/收起动画
|
||||||
|
const childControls = useAnimation();
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
useEffect(() => {
|
||||||
|
// 设置子级为初始状态(隐藏)
|
||||||
|
// 作用:确保子级一开始不可见
|
||||||
|
childControls.set({ opacity: 0 });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ==================== 展开动画序列 ====================
|
||||||
|
const performExpandAnimation = useCallback(async () => {
|
||||||
|
if (isAnimating)
|
||||||
|
return;
|
||||||
|
setIsAnimating(true);
|
||||||
|
|
||||||
|
const { duration, staggerDelay } = animationConfig;
|
||||||
|
const selectedIndex = currentIndex;
|
||||||
|
|
||||||
|
// ===== 准备阶段 =====
|
||||||
|
// 先设置为 false,让其他父级参与下落动画
|
||||||
|
setShouldHideInactive(false);
|
||||||
|
|
||||||
|
// ===== 步骤 1:所有父级依次下落隐藏 =====
|
||||||
|
// 目的:让所有父级依次消失,为选中的父级腾出空间
|
||||||
|
const hidePromises = parentControls.map((control, index) => {
|
||||||
|
return control.start({
|
||||||
|
y: 30, // 向下移动 30px
|
||||||
|
opacity: 0, // 透明度变为 0
|
||||||
|
x: 0, // 重置 x 位置到原始位置
|
||||||
|
transition: { duration, delay: index * staggerDelay }, // 依次延迟,产生波浪效果
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(hidePromises); // 等待所有父级下落完成
|
||||||
|
|
||||||
|
// ===== 步骤 2:选中的父级显示回来 =====
|
||||||
|
// 目的:让选中的父级从下向上淡入显示
|
||||||
|
await parentControls[selectedIndex].start({
|
||||||
|
y: 0, // 回到原始 Y 位置
|
||||||
|
opacity: 1, // 透明度变为 1
|
||||||
|
x: 0, // 保持 x 为 0
|
||||||
|
transition: { duration }, // 不延迟,立即执行
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 3:选中的父级移动到最左边 =====
|
||||||
|
// 目的:让选中的父级移动到容器最左侧,为子级展开腾出空间
|
||||||
|
const selectedElement = optionRefs.current[selectedIndex];
|
||||||
|
const targetX = -selectedElement.offsetLeft; // 计算目标 x 位置(负的偏移量)
|
||||||
|
|
||||||
|
await parentControls[selectedIndex].start({
|
||||||
|
x: targetX, // 移动到最左边
|
||||||
|
transition: { duration, ease: "easeInOut" }, // 使用平滑的缓动函数
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 4:子级展开 =====
|
||||||
|
// 目的:让子级淡入显示
|
||||||
|
await childControls.start({
|
||||||
|
opacity: 1, // 透明度从 0 到 1
|
||||||
|
transition: {
|
||||||
|
duration, // 动画时长
|
||||||
|
delay: staggerDelay, // 延迟执行,等待步骤 3 完成
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 动画完成 =====
|
||||||
|
// 所有动画完成后,隐藏其他父级,避免干扰
|
||||||
|
setShouldHideInactive(true);
|
||||||
|
|
||||||
|
setIsAnimating(false);
|
||||||
|
}, [isAnimating, currentIndex, parentControls, childControls, animationConfig]);
|
||||||
|
|
||||||
|
// ==================== 首次交互监听 ====================
|
||||||
|
// 监听 hasInteracted 变化,在第一次切换后执行动画
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasInteracted && !isAnimating && currentIndex !== -1) {
|
||||||
|
// 第一次切换到 motion.div 后,延迟一帧执行动画
|
||||||
|
// 作用:确保 DOM 已更新,motion.div 已挂载
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
performExpandAnimation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [hasInteracted]);
|
||||||
|
|
||||||
|
// ==================== 收起动画序列 ====================
|
||||||
|
const performCollapseAnimation = useCallback(async () => {
|
||||||
|
if (isAnimating)
|
||||||
|
return;
|
||||||
|
setIsAnimating(true);
|
||||||
|
|
||||||
|
const { duration, staggerDelay } = animationConfig;
|
||||||
|
|
||||||
|
// ===== 准备阶段 =====
|
||||||
|
// 先设置为 false,让所有父级参与收起动画
|
||||||
|
setShouldHideInactive(false);
|
||||||
|
|
||||||
|
// ===== 步骤 1:子级收起 =====
|
||||||
|
// 目的:先隐藏子级,避免视觉干扰
|
||||||
|
await childControls.start({
|
||||||
|
opacity: 0, // 透明度从 1 到 0
|
||||||
|
transition: { duration }, // 立即执行
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 2:所有父级在各自当前位置下落 =====
|
||||||
|
// 目的:让所有父级从当前位置下落,保持各自的 x 位置
|
||||||
|
const hidePromises = parentControls.map((control) => {
|
||||||
|
return control.start({
|
||||||
|
y: 30, // 向下移动 30px
|
||||||
|
opacity: 0, // 透明度变为 0
|
||||||
|
// 不设置 x,保持各自的当前位置(选中的父级在左边,其他在原位)
|
||||||
|
transition: { duration },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(hidePromises); // 等待所有父级下落完成
|
||||||
|
|
||||||
|
// ===== 步骤 3:所有父级回到原位 =====
|
||||||
|
// 目的:将所有父级的 x 位置重置为 0(仍在下方)
|
||||||
|
await Promise.all(
|
||||||
|
parentControls.map((control) => {
|
||||||
|
return control.start({
|
||||||
|
x: 0, // 重置 x 位置到原始位置
|
||||||
|
transition: { duration }, // 平滑过渡
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== 步骤 4:所有父级依次从下向上显示 =====
|
||||||
|
// 目的:让所有父级依次从下向上淡入,恢复到初始状态
|
||||||
|
const showPromises = parentControls.map((control, index) => {
|
||||||
|
return control.start({
|
||||||
|
y: 0, // 回到原始 Y 位置
|
||||||
|
opacity: 1, // 透明度变为 1
|
||||||
|
transition: { duration, delay: index * staggerDelay }, // 依次延迟,产生波浪效果
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(showPromises); // 等待所有父级显示完成
|
||||||
|
|
||||||
|
// ===== 动画完成 =====
|
||||||
|
// 收起完成,恢复隐藏状态
|
||||||
|
setShouldHideInactive(true);
|
||||||
|
|
||||||
|
setIsAnimating(false);
|
||||||
|
}, [isAnimating, parentControls, childControls, animationConfig]);
|
||||||
|
|
||||||
|
// ==================== 监听 currentIndex 变化,触发相应的动画 ====================
|
||||||
|
useEffect(() => {
|
||||||
|
// ===== 模式切换时重置交互状态 =====
|
||||||
|
// 作用:当 isPortMode 变化时,重新设置成未交互过
|
||||||
|
// 这样下次点击时不会触发动画,直到再次点击
|
||||||
|
if (!isPortMode) {
|
||||||
|
setHasInteracted(false);
|
||||||
|
}
|
||||||
|
if (!isPortMode || isAnimating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// ===== 判断当前是否有激活的父级 =====
|
||||||
|
const hasActiveParent = currentIndex !== -1;
|
||||||
|
|
||||||
|
// ===== 场景 1:有激活的父级,且是首次交互 =====
|
||||||
|
if (hasActiveParent && !hasInteracted) {
|
||||||
|
// 第一次点击:只设置 hasInteracted,不执行动画
|
||||||
|
// 动画会在 hasInteracted 变化后的 useEffect 中执行
|
||||||
|
// 作用:延迟动画执行,确保 DOM 已更新
|
||||||
|
setHasInteracted(true);
|
||||||
|
}
|
||||||
|
// ===== 场景 2:有激活的父级,且已经交互过 =====
|
||||||
|
else if (hasActiveParent && hasInteracted) {
|
||||||
|
// 已经交互过,直接执行展开动画
|
||||||
|
performExpandAnimation();
|
||||||
|
}
|
||||||
|
// ===== 场景 3:没有激活的父级,且已经交互过 =====
|
||||||
|
else if (!hasActiveParent && hasInteracted) {
|
||||||
|
// 只有在已经交互过的情况下,才执行收起动画
|
||||||
|
// 作用:避免初始化时执行收起动画
|
||||||
|
performCollapseAnimation();
|
||||||
|
}
|
||||||
|
}, [currentIndex, isPortMode]);
|
||||||
|
|
||||||
|
// ==================== 返回值 ====================
|
||||||
|
// 返回动画控制器和相关状态,供组件使用
|
||||||
|
return {
|
||||||
|
parentControls, // 父级动画控制器数组
|
||||||
|
childControls, // 子级动画控制器
|
||||||
|
optionRefs, // 父级元素引用数组
|
||||||
|
isAnimating, // 是否正在执行动画
|
||||||
|
hasInteracted, // 是否已经交互过
|
||||||
|
shouldHideInactive, // 是否应该隐藏非激活的父级
|
||||||
|
animationConfig, // 动画配置对象(包含 duration 和 staggerDelay)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { motion } from "motion/react";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { CSSTransition } from "react-transition-group";
|
|
||||||
import statisticsTitleImg from "~/assets/images/map_bi/center_utils/statistics_title.png";
|
import statisticsTitleImg from "~/assets/images/map_bi/center_utils/statistics_title.png";
|
||||||
import guangImg from "~/assets/images/map_bi/center_utils/tabguang.png";
|
import guangImg from "~/assets/images/map_bi/center_utils/tabguang.png";
|
||||||
import tabLeftImg from "~/assets/images/map_bi/center_utils/tableft.png";
|
import tabLeftImg from "~/assets/images/map_bi/center_utils/tableft.png";
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
resetAllBottomUtilsCheckMittKey,
|
resetAllBottomUtilsCheckMittKey,
|
||||||
resetBottomCurrentIndexMittKey,
|
resetBottomCurrentIndexMittKey,
|
||||||
} from "~/pages/Container/Map/js/mittKey";
|
} from "~/pages/Container/Map/js/mittKey";
|
||||||
|
import { useCenterUtilsAnimation } from "./useCenterUtilsAnimation";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
function CenterUtils(props) {
|
function CenterUtils(props) {
|
||||||
|
|
@ -27,6 +28,21 @@ function CenterUtils(props) {
|
||||||
];
|
];
|
||||||
const [activeIndex, setActiveIndex] = useState(1);
|
const [activeIndex, setActiveIndex] = useState(1);
|
||||||
|
|
||||||
|
const { controls: containerControls, isVisible: isContainerVisible } = useCenterUtilsAnimation(
|
||||||
|
(currentPort === "00003" && !currentBranchOffice && !pureMap),
|
||||||
|
"up-down",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { controls: westControls, isVisible: isWestVisible } = useCenterUtilsAnimation(
|
||||||
|
((currentPort === "00003" && !currentBranchOffice && !pureMap) && (activeIndex === 0 || activeIndex === 1)),
|
||||||
|
"scale",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { controls: eastControls, isVisible: isEastVisible } = useCenterUtilsAnimation(
|
||||||
|
((currentPort === "00003" && !currentBranchOffice && !pureMap) && (activeIndex === 2 || activeIndex === 1)),
|
||||||
|
"scale",
|
||||||
|
);
|
||||||
|
|
||||||
const onChangeArea = (index) => {
|
const onChangeArea = (index) => {
|
||||||
if (index === activeIndex)
|
if (index === activeIndex)
|
||||||
return;
|
return;
|
||||||
|
|
@ -41,18 +57,9 @@ function CenterUtils(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map_content_center_options_container">
|
<div className="map_content_center_options_container">
|
||||||
<CSSTransition
|
<motion.div animate={containerControls} className="center_utils">
|
||||||
timeout={1000}
|
{isContainerVisible && (
|
||||||
classNames={{
|
<>
|
||||||
enter: "animate__animated",
|
|
||||||
enterActive: "animate__animated animate__bounceInDown",
|
|
||||||
exit: "animate__animated",
|
|
||||||
exitActive: "animate__animated animate__bounceOutUp",
|
|
||||||
}}
|
|
||||||
unmountOnExit
|
|
||||||
in={(currentPort === "00003" && !currentBranchOffice && !pureMap)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="center_options">
|
<div className="center_options">
|
||||||
<div
|
<div
|
||||||
className="guang"
|
className="guang"
|
||||||
|
|
@ -74,17 +81,8 @@ function CenterUtils(props) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="statistics">
|
<div className="statistics">
|
||||||
<CSSTransition
|
<motion.div animate={westControls}>
|
||||||
timeout={1000}
|
{isWestVisible && (
|
||||||
classNames={{
|
|
||||||
enter: "animate__animated",
|
|
||||||
enterActive: "animate__animated animate__bounceIn",
|
|
||||||
exit: "animate__animated",
|
|
||||||
exitActive: "animate__animated animate__bounceOut",
|
|
||||||
}}
|
|
||||||
unmountOnExit
|
|
||||||
in={(activeIndex === 0 || activeIndex === 1)}
|
|
||||||
>
|
|
||||||
<div className="statistic">
|
<div className="statistic">
|
||||||
<div className="title" style={{ backgroundImage: `url(${statisticsTitleImg})` }}>西港区</div>
|
<div className="title" style={{ backgroundImage: `url(${statisticsTitleImg})` }}>西港区</div>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
|
|
@ -92,18 +90,10 @@ function CenterUtils(props) {
|
||||||
<div className="value">车辆:33辆</div>
|
<div className="value">车辆:33辆</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
)}
|
||||||
<CSSTransition
|
</motion.div>
|
||||||
timeout={1000}
|
<motion.div animate={eastControls}>
|
||||||
classNames={{
|
{isEastVisible && (
|
||||||
enter: "animate__animated",
|
|
||||||
enterActive: "animate__animated animate__bounceIn",
|
|
||||||
exit: "animate__animated",
|
|
||||||
exitActive: "animate__animated animate__bounceOut",
|
|
||||||
}}
|
|
||||||
unmountOnExit
|
|
||||||
in={(activeIndex === 2 || activeIndex === 1)}
|
|
||||||
>
|
|
||||||
<div className="statistic">
|
<div className="statistic">
|
||||||
<div className="title" style={{ backgroundImage: `url(${statisticsTitleImg})` }}>东港区</div>
|
<div className="title" style={{ backgroundImage: `url(${statisticsTitleImg})` }}>东港区</div>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
|
|
@ -111,10 +101,12 @@ function CenterUtils(props) {
|
||||||
<div className="value">车辆:33辆</div>
|
<div className="value">车辆:33辆</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
)}
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</CSSTransition>
|
)}
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
.map_content_center_options_container {
|
.map_content_center_options_container {
|
||||||
|
.center_utils {
|
||||||
|
|
||||||
.center_options {
|
.center_options {
|
||||||
width: 408px;
|
width: 408px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -75,3 +77,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CenterUtils 弹跳动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 监听组件的显示/隐藏状态变化
|
||||||
|
* 2. 执行炫酷的弹跳动画效果(类似 animate.css 的 bounceIn/bounceOut)
|
||||||
|
* 3. 支持多种动画类型(上下弹跳、缩放弹跳)
|
||||||
|
* 4. 使用弹跳、缩放、透明度三重动画效果,让动画更加生动
|
||||||
|
* 5. 延迟隐藏机制,确保离开动画完整播放
|
||||||
|
*
|
||||||
|
* 动画效果详解(上下弹跳模式 up-down):
|
||||||
|
* - 进入动画:
|
||||||
|
* - 位置:从上方 120px 处冲到下方 40px(超出目标),轻微上弹到 -10px,最后稳定到 0
|
||||||
|
* - 缩放:从 0.9 倍缩小到 1.05 倍(轻微放大),轻微缩小到 0.98,回到正常 1.0
|
||||||
|
* - 透明度:从透明到不透明
|
||||||
|
* - 时长:0.7 秒
|
||||||
|
* - 缓动:使用强弹性贝塞尔曲线 [0.34, 1.56, 0.64, 1],产生明显的过冲和回弹效果
|
||||||
|
*
|
||||||
|
* - 离开动画:
|
||||||
|
* - 位置:从正常位置向上弹跳 50px,回落到 20px,然后向上离开到 -120px
|
||||||
|
* - 缩放:轻微放大到 1.02,然后缩小到 0.85
|
||||||
|
* - 透明度:保持可见再淡出
|
||||||
|
* - 时长:0.5 秒
|
||||||
|
* - 缓动:使用超弹性曲线 [0.68, -0.6, 0.32, 1.6],产生夸张的反向弹跳
|
||||||
|
*
|
||||||
|
* 动画效果详解(缩放弹跳模式 scale):
|
||||||
|
* - 进入动画:
|
||||||
|
* - 缩放:从 0 放大到 1.15 倍(超出目标),缩小到 0.92,回到正常 1.0
|
||||||
|
* - 时长:0.7 秒
|
||||||
|
* - 缓动:使用强弹性贝塞尔曲线
|
||||||
|
*
|
||||||
|
* - 离开动画:
|
||||||
|
* - 缩放:从 1.0 放大到 1.1,缩小到 0.85,最后消失到 0
|
||||||
|
* - 时长:0.5 秒
|
||||||
|
* - 缓动:使用超弹性曲线
|
||||||
|
*
|
||||||
|
* @param {boolean} isVisible - 控制组件的显示/隐藏
|
||||||
|
* - true: 组件应该显示,执行进入动画
|
||||||
|
* - false: 组件应该隐藏,执行离开动画
|
||||||
|
* @param {string} type - 动画类型
|
||||||
|
* - 'up-down':上下弹跳动画(默认值,从上方进入,向上方离开)
|
||||||
|
* - 'scale':缩放弹跳动画(从中心放大进入,缩小离开)
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - controls: motion 动画控制器,绑定到 motion.div 的 animate 属性
|
||||||
|
* - isVisible: 延迟的可见性状态(用于等待离开动画完成后再隐藏组件)
|
||||||
|
*/
|
||||||
|
export function useCenterUtilsAnimation(isVisible, type = "up-down") {
|
||||||
|
// ==================== 状态和引用管理 ====================
|
||||||
|
|
||||||
|
// motion 动画控制器,用于控制 motion.div 的动画
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
// 标记是否是首次渲染
|
||||||
|
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
||||||
|
// 首次渲染不执行离开动画,直接执行进入动画
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
// 组件的实际显示状态
|
||||||
|
// 作用:延迟隐藏,确保离开动画执行完成后才将组件从 DOM 中移除
|
||||||
|
const [displayState, setDisplayState] = useState(isVisible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ==================== 显示状态:执行进入动画 ====================
|
||||||
|
if (isVisible) {
|
||||||
|
// 步骤 1:确保组件处于显示状态
|
||||||
|
if (!displayState) {
|
||||||
|
setDisplayState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤 2:根据动画类型执行进入动画
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// 类型 1:上下弹跳动画(炫酷 bounceInDown 效果)
|
||||||
|
if (type === "up-down") {
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
// === 首次渲染:炫酷弹跳进入 ===
|
||||||
|
controls.start({
|
||||||
|
y: [-120, 40, -10, 0], // 四个关键帧:上方120px -> 超出下方40px(大弹跳)-> 轻微上弹 -> 稳定
|
||||||
|
opacity: [0, 1, 1, 1], // 快速淡入
|
||||||
|
scale: [0.9, 1.05, 0.98, 1], // 缩放:缩小 -> 轻微放大 -> 轻微缩小 -> 正常
|
||||||
|
transition: {
|
||||||
|
duration: 0.7, // 总动画时长:0.7 秒
|
||||||
|
times: [0, 0.5, 0.75, 1], // 时间点分布:快速下落,多次回弹
|
||||||
|
ease: [0.34, 1.56, 0.64, 1], // 强弹性贝塞尔曲线,产生明显的回弹效果
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
isFirstRender.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// === 非首次渲染:炫酷弹跳进入 ===
|
||||||
|
controls.start({
|
||||||
|
y: [-120, 40, -10, 0],
|
||||||
|
opacity: [0, 1, 1, 1],
|
||||||
|
scale: [0.9, 1.05, 0.98, 1],
|
||||||
|
transition: {
|
||||||
|
duration: 0.7,
|
||||||
|
times: [0, 0.5, 0.75, 1],
|
||||||
|
ease: [0.34, 1.56, 0.64, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// 类型 2:缩放弹跳动画(炫酷 bounceIn 效果)
|
||||||
|
else if (type === "scale") {
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
// === 首次渲染:炫酷缩放弹跳 ===
|
||||||
|
controls.start({
|
||||||
|
scale: [0, 1.15, 0.92, 1], // 四个关键帧:0 -> 超出115% -> 缩小到92% -> 正常
|
||||||
|
opacity: [0, 1, 1, 1],
|
||||||
|
transition: {
|
||||||
|
duration: 0.7,
|
||||||
|
times: [0, 0.4, 0.7, 1], // 时间点分布
|
||||||
|
ease: [0.34, 1.56, 0.64, 1], // 强弹性曲线
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
isFirstRender.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// === 非首次渲染:炫酷缩放弹跳 ===
|
||||||
|
controls.start({
|
||||||
|
scale: [0, 1.15, 0.92, 1],
|
||||||
|
opacity: [0, 1, 1, 1],
|
||||||
|
transition: {
|
||||||
|
duration: 0.7,
|
||||||
|
times: [0, 0.4, 0.7, 1],
|
||||||
|
ease: [0.34, 1.56, 0.64, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 隐藏状态:执行离开动画 ====================
|
||||||
|
else {
|
||||||
|
// 步骤 1:根据动画类型执行离开动画
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// 类型 1:上下弹跳动画(炫酷 bounceOutUp 效果)
|
||||||
|
if (type === "up-down") {
|
||||||
|
controls.start({
|
||||||
|
y: [0, -50, 20, -120], // 四个关键帧:正常 -> 超出上方50px -> 回落 -> 向上离开
|
||||||
|
opacity: [1, 1, 0.8, 0], // 保持可见再淡出
|
||||||
|
scale: [1, 1.02, 0.95, 0.85], // 缩放:轻微放大 -> 轻微缩小
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 总动画时长:0.5 秒
|
||||||
|
times: [0, 0.2, 0.5, 1], // 时间点分布:快速弹起,缓慢落下
|
||||||
|
ease: [0.68, -0.6, 0.32, 1.6], // 超弹性曲线,产生夸张的回弹效果
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
setDisplayState(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// 类型 2:缩放弹跳动画(炫酷 bounceOut 效果)
|
||||||
|
else if (type === "scale") {
|
||||||
|
controls.start({
|
||||||
|
scale: [1, 1.1, 0.85, 0], // 四个关键帧:正常 -> 放大到110% -> 缩小到85% -> 消失
|
||||||
|
opacity: [1, 1, 0.6, 0],
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
times: [0, 0.3, 0.6, 1],
|
||||||
|
ease: [0.68, -0.6, 0.32, 1.6], // 超弹性曲线
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
setDisplayState(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isVisible, type, controls, displayState]);
|
||||||
|
|
||||||
|
// 返回动画控制器和延迟的可见性状态
|
||||||
|
// - controls:用于绑定到 motion.div 的 animate 属性
|
||||||
|
// - isVisible:延迟的可见性状态,用于条件渲染(等待离开动画完成)
|
||||||
|
return {
|
||||||
|
controls,
|
||||||
|
isVisible: displayState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { motion } from "motion/react";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import collapseMenuImg1 from "~/assets/images/map_bi/content/collapse_menu1.png";
|
import collapseMenuImg1 from "~/assets/images/map_bi/content/collapse_menu1.png";
|
||||||
import collapseMenuImg2 from "~/assets/images/map_bi/content/collapse_menu2.png";
|
import collapseMenuImg2 from "~/assets/images/map_bi/content/collapse_menu2.png";
|
||||||
|
|
@ -25,16 +26,42 @@ import PortWeiXian from "~/pages/Container/Map/components/Content/port/WeiXian";
|
||||||
import PortXiaoFang from "~/pages/Container/Map/components/Content/port/XiaoFang";
|
import PortXiaoFang from "~/pages/Container/Map/components/Content/port/XiaoFang";
|
||||||
import PortZhongDian from "~/pages/Container/Map/components/Content/port/ZhongDian";
|
import PortZhongDian from "~/pages/Container/Map/components/Content/port/ZhongDian";
|
||||||
import { Context } from "~/pages/Container/Map/js/context";
|
import { Context } from "~/pages/Container/Map/js/context";
|
||||||
|
import { useContentAnimation } from "./useContentAnimation";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
function Content() {
|
function Content() {
|
||||||
const { currentPort, currentBranchOffice, pureMap, bottomUtilsCurrentIndex } = useContext(Context);
|
const { currentPort, currentBranchOffice, pureMap, bottomUtilsCurrentIndex } = useContext(Context);
|
||||||
|
|
||||||
const [collapseLeft, setCollapseLeft] = useState(false);
|
const [collapseLeft, setCollapseLeft] = useState(false);
|
||||||
const [collapseRight, setCollapseRight] = useState(false);
|
const [collapseRight, setCollapseRight] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
controls: leftControls,
|
||||||
|
displayedContent: leftDisplayedContent,
|
||||||
|
isVisible: isLeftVisible,
|
||||||
|
handleCollapse: handleLeftCollapse,
|
||||||
|
} = useContentAnimation(
|
||||||
|
{ currentPort, currentBranchOffice, bottomUtilsCurrentIndex },
|
||||||
|
collapseLeft,
|
||||||
|
"left",
|
||||||
|
pureMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
controls: rightControls,
|
||||||
|
displayedContent: rightDisplayedContent,
|
||||||
|
isVisible: isRightVisible,
|
||||||
|
handleCollapse: handleRightCollapse,
|
||||||
|
} = useContentAnimation(
|
||||||
|
{ currentBranchOffice, bottomUtilsCurrentIndex },
|
||||||
|
collapseRight,
|
||||||
|
"right",
|
||||||
|
pureMap,
|
||||||
|
);
|
||||||
|
|
||||||
const renderPortContent = () => {
|
const renderPortContent = () => {
|
||||||
const bottomUtilsCurrentType = bottomUtilsCurrentIndex !== -1 ? portUtilsList[bottomUtilsCurrentIndex].type : "";
|
const bottomUtilsCurrentType = (leftDisplayedContent.bottomUtilsCurrentIndex !== -1 && portUtilsList[leftDisplayedContent.bottomUtilsCurrentIndex])
|
||||||
|
? portUtilsList[leftDisplayedContent.bottomUtilsCurrentIndex].type
|
||||||
|
: "";
|
||||||
if (bottomUtilsCurrentType === "" || bottomUtilsCurrentType === "camera")
|
if (bottomUtilsCurrentType === "" || bottomUtilsCurrentType === "camera")
|
||||||
return <PortIndex />;
|
return <PortIndex />;
|
||||||
if (bottomUtilsCurrentType === "door")
|
if (bottomUtilsCurrentType === "door")
|
||||||
|
|
@ -54,7 +81,9 @@ function Content() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBranchOfficeContentLeft = () => {
|
const renderBranchOfficeContentLeft = () => {
|
||||||
const bottomUtilsCurrentType = bottomUtilsCurrentIndex !== -1 ? branchOfficeUtilsList[bottomUtilsCurrentIndex].type : "";
|
const bottomUtilsCurrentType = (leftDisplayedContent.bottomUtilsCurrentIndex !== -1 && branchOfficeUtilsList[leftDisplayedContent.bottomUtilsCurrentIndex])
|
||||||
|
? branchOfficeUtilsList[leftDisplayedContent.bottomUtilsCurrentIndex].type
|
||||||
|
: "";
|
||||||
if (bottomUtilsCurrentType === "")
|
if (bottomUtilsCurrentType === "")
|
||||||
return <BranchOfficeIndexLeft />;
|
return <BranchOfficeIndexLeft />;
|
||||||
if (bottomUtilsCurrentType === "danger")
|
if (bottomUtilsCurrentType === "danger")
|
||||||
|
|
@ -76,34 +105,35 @@ function Content() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBranchOfficeContentRight = () => {
|
const renderBranchOfficeContentRight = () => {
|
||||||
const bottomUtilsCurrentType = bottomUtilsCurrentIndex !== -1 ? branchOfficeUtilsList[bottomUtilsCurrentIndex].type : "";
|
const bottomUtilsCurrentType = (rightDisplayedContent.bottomUtilsCurrentIndex !== -1 && branchOfficeUtilsList[rightDisplayedContent.bottomUtilsCurrentIndex])
|
||||||
|
? branchOfficeUtilsList[rightDisplayedContent.bottomUtilsCurrentIndex].type
|
||||||
|
: "";
|
||||||
if (bottomUtilsCurrentType === "")
|
if (bottomUtilsCurrentType === "")
|
||||||
return <BranchOfficeIndexRight />;
|
return <BranchOfficeIndexRight />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (pureMap)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!collapseLeft && (
|
{isLeftVisible && (
|
||||||
<div
|
<motion.div
|
||||||
className={`map_content_container__content ${(!currentPort || (currentPort === "00003" && !currentBranchOffice)) ? "port" : "branch_office"}`}
|
animate={leftControls}
|
||||||
|
className={`map_content_container__content ${(!leftDisplayedContent.currentPort || (leftDisplayedContent.currentPort === "00003" && !leftDisplayedContent.currentBranchOffice)) ? "port" : "branch_office"}`}
|
||||||
style={{ left: 35 }}
|
style={{ left: 35 }}
|
||||||
>
|
>
|
||||||
{!currentPort && <IndexInfo />}
|
{!leftDisplayedContent.currentPort && <IndexInfo />}
|
||||||
{(currentPort === "00003" && !currentBranchOffice) && renderPortContent()}
|
{(leftDisplayedContent.currentPort === "00003" && !leftDisplayedContent.currentBranchOffice) && renderPortContent()}
|
||||||
{currentBranchOffice && renderBranchOfficeContentLeft()}
|
{leftDisplayedContent.currentBranchOffice && renderBranchOfficeContentLeft()}
|
||||||
</div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{!collapseRight && (
|
{isRightVisible && (
|
||||||
<div
|
<motion.div
|
||||||
className={`map_content_container__content ${(!currentPort || (currentPort === "00003" && !currentBranchOffice)) ? "port" : "branch_office"}`}
|
animate={rightControls}
|
||||||
|
className={`map_content_container__content ${(!leftDisplayedContent.currentPort || (leftDisplayedContent.currentPort === "00003" && !leftDisplayedContent.currentBranchOffice)) ? "port" : "branch_office"}`}
|
||||||
style={{ right: 35 }}
|
style={{ right: 35 }}
|
||||||
>
|
>
|
||||||
{currentBranchOffice && renderBranchOfficeContentRight()}
|
{rightDisplayedContent.currentBranchOffice && renderBranchOfficeContentRight()}
|
||||||
</div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -118,9 +148,7 @@ function Content() {
|
||||||
<div
|
<div
|
||||||
className={`collapse_menu port left ${collapseLeft ? "active" : ""}`}
|
className={`collapse_menu port left ${collapseLeft ? "active" : ""}`}
|
||||||
style={{ backgroundImage: `url(${collapseMenuBg1})` }}
|
style={{ backgroundImage: `url(${collapseMenuBg1})` }}
|
||||||
onClick={() => {
|
onClick={() => handleLeftCollapse(setCollapseLeft)}
|
||||||
setCollapseLeft(!collapseLeft);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<img src={collapseMenuImg1} alt="" />
|
<img src={collapseMenuImg1} alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,9 +164,7 @@ function Content() {
|
||||||
<div
|
<div
|
||||||
className={`collapse_menu branch_office left ${collapseLeft ? "active" : ""}`}
|
className={`collapse_menu branch_office left ${collapseLeft ? "active" : ""}`}
|
||||||
style={{ backgroundImage: `url(${collapseMenuBg2})` }}
|
style={{ backgroundImage: `url(${collapseMenuBg2})` }}
|
||||||
onClick={() => {
|
onClick={() => handleLeftCollapse(setCollapseLeft)}
|
||||||
setCollapseLeft(!collapseLeft);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<img src={collapseMenuImg2} alt="" />
|
<img src={collapseMenuImg2} alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -147,9 +173,7 @@ function Content() {
|
||||||
<div
|
<div
|
||||||
className={`collapse_menu branch_office right ${collapseRight ? "active" : ""}`}
|
className={`collapse_menu branch_office right ${collapseRight ? "active" : ""}`}
|
||||||
style={{ backgroundImage: `url(${collapseMenuBg2})` }}
|
style={{ backgroundImage: `url(${collapseMenuBg2})` }}
|
||||||
onClick={() => {
|
onClick={() => handleRightCollapse(setCollapseRight)}
|
||||||
setCollapseRight(!collapseRight);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<img src={collapseMenuImg2} alt="" />
|
<img src={collapseMenuImg2} alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,13 @@
|
||||||
&.port {
|
&.port {
|
||||||
top: 75px;
|
top: 75px;
|
||||||
max-height: calc(100vh - 75px);
|
max-height: calc(100vh - 75px);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.branch_office {
|
&.branch_office {
|
||||||
top: 70px;
|
top: 70px;
|
||||||
max-height: calc(100vh - 70px);
|
max-height: calc(100vh - 70px);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内容切换动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 管理内容面板的显示/隐藏状态
|
||||||
|
* 2. 处理内容切换时的动画效果(离开 + 进入)
|
||||||
|
* 3. 处理折叠/展开时的动画效果
|
||||||
|
* 4. 处理纯地图模式下的动画效果
|
||||||
|
*
|
||||||
|
* 优先级:pureMap > 折叠状态 > 内容变化
|
||||||
|
*
|
||||||
|
* @param {object} currentContent - 当前要显示的内容状态(包含 currentPort、currentBranchOffice、bottomUtilsCurrentIndex 等)
|
||||||
|
* @param {boolean} isCollapsed - 当前是否处于折叠状态
|
||||||
|
* @param {string} side - 'left' 或 'right',控制动画方向
|
||||||
|
* @param {boolean} isPureMap - 是否为纯地图模式
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - controls: motion 动画控制器
|
||||||
|
* - displayedContent: 延迟显示的内容状态(用于动画过渡)
|
||||||
|
* - isVisible: 元素可见性状态
|
||||||
|
* - handleCollapse: 处理折叠/展开的函数
|
||||||
|
*/
|
||||||
|
export function useContentAnimation(currentContent, isCollapsed, side = "left", isPureMap = false) {
|
||||||
|
// ==================== 状态管理 ====================
|
||||||
|
|
||||||
|
// motion 动画控制器,用于控制元素的动画
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
// 标记是否是首次渲染(首次渲染不执行离开动画,直接进入)
|
||||||
|
const isLeftFirstRender = useRef(true);
|
||||||
|
|
||||||
|
// 标记当前是否正在执行动画(防止动画冲突)
|
||||||
|
const isAnimating = useRef(false);
|
||||||
|
|
||||||
|
// 延迟显示的内容状态
|
||||||
|
// 作用:在内容切换时,先显示旧内容,动画完成后再更新为新内容
|
||||||
|
const [displayedContent, setDisplayedContent] = useState(currentContent);
|
||||||
|
|
||||||
|
// 元素可见性状态
|
||||||
|
// false: 元素被隐藏(折叠或纯地图模式)
|
||||||
|
// true: 元素可见(非折叠、非纯地图模式)
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
// ==================== 辅助函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算动画的初始位置
|
||||||
|
* @returns {number} 初始 X 轴位移值(负数表示在左侧,正数表示在右侧)
|
||||||
|
*/
|
||||||
|
const getInitialX = () => side === "left" ? -300 : 300;
|
||||||
|
|
||||||
|
// ==================== 折叠/展开处理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理折叠按钮点击事件
|
||||||
|
* @param {Function} setCollapseState - setState 函数,用于更新折叠状态
|
||||||
|
*/
|
||||||
|
const handleCollapse = (setCollapseState) => {
|
||||||
|
if (isCollapsed) {
|
||||||
|
// ==================== 展开逻辑 ====================
|
||||||
|
// 1. 先同步显示内容(确保显示最新的内容)
|
||||||
|
setDisplayedContent(currentContent);
|
||||||
|
|
||||||
|
// 2. 显示元素
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// 3. 更新折叠状态
|
||||||
|
setCollapseState(false);
|
||||||
|
|
||||||
|
// 4. 执行进入动画
|
||||||
|
// 使用 requestAnimationFrame 确保 DOM 更新后再设置初始状态
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// 设置元素到初始隐藏位置(侧边外部)
|
||||||
|
controls.set({
|
||||||
|
x: getInitialX(),
|
||||||
|
opacity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 然后触发进入动画(从侧边滑入)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// ==================== 折叠逻辑 ====================
|
||||||
|
// 执行离开动画(滑到侧边并淡出)
|
||||||
|
controls.start({
|
||||||
|
x: getInitialX(), // 移动到侧边外部
|
||||||
|
opacity: 0, // 淡出
|
||||||
|
transition: { duration: 0.3, ease: "easeIn" },
|
||||||
|
}).then(() => {
|
||||||
|
// 动画完成后:
|
||||||
|
// 1. 隐藏元素
|
||||||
|
setIsVisible(false);
|
||||||
|
|
||||||
|
// 2. 更新折叠状态为 true
|
||||||
|
setCollapseState(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 主题:监听状态变化并执行动画 ====================
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ==================== 场景 1:纯地图模式(最高优先级)====================
|
||||||
|
if (isPureMap) {
|
||||||
|
// 如果元素当前可见,执行离开动画并隐藏
|
||||||
|
if (isVisible) {
|
||||||
|
controls.start({
|
||||||
|
x: getInitialX(), // 滑到侧边
|
||||||
|
opacity: 0, // 淡出
|
||||||
|
transition: { duration: 0.3, ease: "easeIn" },
|
||||||
|
}).then(() => {
|
||||||
|
// 动画完成后隐藏元素
|
||||||
|
setIsVisible(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 纯地图模式下,不执行任何后续逻辑
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 场景 2:折叠状态 ====================
|
||||||
|
if (isCollapsed) {
|
||||||
|
// 检查内容是否发生变化
|
||||||
|
const contentChanged = Object.keys(currentContent).some(
|
||||||
|
key => displayedContent[key] !== currentContent[key],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 静默更新内容(不执行动画,保持数据同步)
|
||||||
|
if (contentChanged) {
|
||||||
|
setDisplayedContent(currentContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保元素保持隐藏(防止异常情况导致的意外显示)
|
||||||
|
if (isVisible) {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折叠状态下,不执行任何动画逻辑
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 场景 3:正常模式(非折叠、非纯地图)====================
|
||||||
|
|
||||||
|
// 检查内容是否真的变化了
|
||||||
|
const contentChanged = Object.keys(currentContent).some(
|
||||||
|
key => displayedContent[key] !== currentContent[key],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----- 情况 A:首次渲染 -----
|
||||||
|
if (isLeftFirstRender.current) {
|
||||||
|
// 1. 设置元素到初始隐藏位置(侧边外部)
|
||||||
|
controls.set({
|
||||||
|
x: getInitialX(),
|
||||||
|
opacity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 立即更新显示的内容
|
||||||
|
setDisplayedContent(currentContent);
|
||||||
|
|
||||||
|
// 3. 执行滑入动画(从侧边滑入到正常位置)
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 标记首次渲染完成
|
||||||
|
isLeftFirstRender.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- 情况 B:内容变化(用户切换工具)-----
|
||||||
|
else if (contentChanged && !isAnimating.current) {
|
||||||
|
// 标记开始执行动画(防止动画冲突)
|
||||||
|
isAnimating.current = true;
|
||||||
|
|
||||||
|
// 1. 先执行离开动画(旧内容离开)
|
||||||
|
controls.start({
|
||||||
|
x: getInitialX(), // 滑到侧边
|
||||||
|
opacity: 0, // 淡出
|
||||||
|
transition: { duration: 0.3, ease: "easeIn" },
|
||||||
|
}).then(() => {
|
||||||
|
// 离开动画完成后:
|
||||||
|
|
||||||
|
// 2. 更新显示的内容为新内容
|
||||||
|
setDisplayedContent(currentContent);
|
||||||
|
|
||||||
|
// 3. 标记动画完成
|
||||||
|
isAnimating.current = false;
|
||||||
|
|
||||||
|
// 4. 执行进入动画(新内容进入)
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 从侧边滑入到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- 情况 C:从隐藏状态切换回来(退出纯地图模式或展开)-----
|
||||||
|
else if (!isVisible) {
|
||||||
|
// 1. 显示元素
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// 2. 更新显示的内容
|
||||||
|
setDisplayedContent(currentContent);
|
||||||
|
|
||||||
|
// 3. 执行进入动画
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// 设置元素到初始隐藏位置
|
||||||
|
controls.set({
|
||||||
|
x: getInitialX(),
|
||||||
|
opacity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 然后触发进入动画
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 滑入到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentContent, isCollapsed, isPureMap, controls, displayedContent, side, isVisible]);
|
||||||
|
|
||||||
|
// ==================== 返回值 ====================
|
||||||
|
|
||||||
|
return {
|
||||||
|
controls, // motion 动画控制器
|
||||||
|
displayedContent, // 延迟显示的内容(用于动画过渡)
|
||||||
|
isVisible, // 元素可见性状态
|
||||||
|
handleCollapse, // 折叠/展开处理函数
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { motion } from "motion/react";
|
||||||
import { CSSTransition } from "react-transition-group";
|
import { useContext } from "react";
|
||||||
import backImg1 from "~/assets/images/map_bi/back1.png";
|
import backImg1 from "~/assets/images/map_bi/back1.png";
|
||||||
import backImg2 from "~/assets/images/map_bi/back2.png";
|
import backImg2 from "~/assets/images/map_bi/back2.png";
|
||||||
import guangImg from "~/assets/images/map_bi/guang.png";
|
import guangImg from "~/assets/images/map_bi/guang.png";
|
||||||
|
|
@ -16,27 +16,13 @@ import {
|
||||||
resetAllBottomUtilsCheckMittKey,
|
resetAllBottomUtilsCheckMittKey,
|
||||||
resetBottomCurrentIndexMittKey,
|
resetBottomCurrentIndexMittKey,
|
||||||
} from "~/pages/Container/Map/js/mittKey";
|
} from "~/pages/Container/Map/js/mittKey";
|
||||||
|
import { useHeaderAnimation } from "./useHeaderAnimation";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
const animationTime = 1000;
|
|
||||||
function Header(props) {
|
function Header(props) {
|
||||||
const { currentPort, currentBranchOffice, mapMethods, area } = useContext(Context);
|
const { currentPort, currentBranchOffice, mapMethods, area } = useContext(Context);
|
||||||
|
|
||||||
const [animationShow, setAnimationShow] = useState(false);
|
const { controls, displayedTitle } = useHeaderAnimation(props.headerTitle);
|
||||||
const [displayTitle, setDisplayTitle] = useState(props.headerTitle);
|
|
||||||
const timer = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAnimationShow(false);
|
|
||||||
timer.current = setTimeout(() => {
|
|
||||||
setDisplayTitle(props.headerTitle);
|
|
||||||
setAnimationShow(true);
|
|
||||||
}, animationTime);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
timer.current && clearTimeout(timer.current);
|
|
||||||
};
|
|
||||||
}, [props.headerTitle]);
|
|
||||||
|
|
||||||
const onBack = () => {
|
const onBack = () => {
|
||||||
sessionStorage.removeItem("mapCurrentBranchOfficeId");
|
sessionStorage.removeItem("mapCurrentBranchOfficeId");
|
||||||
|
|
@ -56,6 +42,8 @@ function Header(props) {
|
||||||
mapMethods.current.removeBranchOfficePoint();
|
mapMethods.current.removeBranchOfficePoint();
|
||||||
mapMethods.current.removeMarkPoint();
|
mapMethods.current.removeMarkPoint();
|
||||||
mapMethods.current.returnPreviousCenterPoint();
|
mapMethods.current.returnPreviousCenterPoint();
|
||||||
|
mapMethods.current.removeFourColorDiagram();
|
||||||
|
mapMethods.current.removeWall();
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
mapMethods.current.addBranchOfficePoint(area);
|
mapMethods.current.addBranchOfficePoint(area);
|
||||||
// }, 2000);
|
// }, 2000);
|
||||||
|
|
@ -77,34 +65,23 @@ function Header(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map_content_header_container">
|
<div className="map_content_header_container">
|
||||||
<CSSTransition
|
<motion.header
|
||||||
timeout={animationTime}
|
animate={controls}
|
||||||
classNames={{
|
className={`${displayedTitle === "秦港股份安全监管平台" ? "port" : "branch_office"}`}
|
||||||
enter: "animate__animated",
|
style={{ backgroundImage: `url(${displayedTitle === "秦港股份安全监管平台" ? topImg1 : topImg2})` }}
|
||||||
enterActive: "animate__animated animate__fadeInDown",
|
|
||||||
exit: "animate__animated",
|
|
||||||
exitActive: "animate__animated animate__fadeOutUp",
|
|
||||||
}}
|
|
||||||
unmountOnExit
|
|
||||||
in={animationShow}
|
|
||||||
>
|
>
|
||||||
<header
|
{(currentPort && displayedTitle === "秦港股份安全监管平台") && (
|
||||||
className={`${displayTitle === "秦港股份安全监管平台" ? "port" : "branch_office"}`}
|
|
||||||
style={{ backgroundImage: `url(${displayTitle === "秦港股份安全监管平台" ? topImg1 : topImg2})` }}
|
|
||||||
>
|
|
||||||
{(currentPort && displayTitle === "秦港股份安全监管平台") && (
|
|
||||||
<div style={{ backgroundImage: `url(${backImg1})` }} className="back" onClick={onBack} />
|
<div style={{ backgroundImage: `url(${backImg1})` }} className="back" onClick={onBack} />
|
||||||
)}
|
)}
|
||||||
{displayTitle !== "秦港股份安全监管平台" && (
|
{displayedTitle !== "秦港股份安全监管平台" && (
|
||||||
<div className="back" onClick={onBack}>
|
<div className="back" onClick={onBack}>
|
||||||
<img src={backImg2} alt="" />
|
<img src={backImg2} alt="" />
|
||||||
<div>返回</div>
|
<div>返回</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="title">{displayTitle}</div>
|
<div className="title">{displayedTitle}</div>
|
||||||
<div style={{ backgroundImage: `url(${guangImg})` }} className="guang" />
|
<div style={{ backgroundImage: `url(${guangImg})` }} className="guang" />
|
||||||
</header>
|
</motion.header>
|
||||||
</CSSTransition>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
&.port {
|
&.port {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header 组件动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 监听标题内容变化
|
||||||
|
* 2. 内容变化时执行上下方向的动画效果(向上离开 + 向下进入)
|
||||||
|
* 3. 支持首次加载动画
|
||||||
|
*
|
||||||
|
* 动画效果:
|
||||||
|
* - 进入动画:从上方 100px 处滑入,同时淡入(y: -100 → 0, opacity: 0 → 1)
|
||||||
|
* - 离开动画:向上方 100px 处滑出,同时淡出(y: 0 → -100, opacity: 1 → 0)
|
||||||
|
*
|
||||||
|
* @param {string} currentTitle - 当前的标题内容
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - controls: motion 动画控制器
|
||||||
|
* - displayedTitle: 延迟显示的标题(用于动画过渡)
|
||||||
|
*/
|
||||||
|
export function useHeaderAnimation(currentTitle) {
|
||||||
|
// ==================== 状态和引用管理 ====================
|
||||||
|
|
||||||
|
// motion 动画控制器,用于控制 motion.header 的动画
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
// 标记是否是首次渲染
|
||||||
|
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
// 延迟显示的标题状态
|
||||||
|
// 作用:在标题切换时,先显示旧标题,动画完成后再更新为新标题
|
||||||
|
// 这样确保离开动画显示的是旧标题,进入动画显示的是新标题
|
||||||
|
const [displayedTitle, setDisplayedTitle] = useState(currentTitle);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ==================== 检查标题是否发生变化 ====================
|
||||||
|
const titleChanged = displayedTitle !== currentTitle;
|
||||||
|
|
||||||
|
// ==================== 首次渲染 ====================
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
// ===== 步骤 1:设置元素到初始隐藏位置(上方外部)=====
|
||||||
|
// - y: -100:元素位于视图上方 100px 处(隐藏)
|
||||||
|
// - opacity: 0:完全透明
|
||||||
|
// - controls.set() 是立即设置,不会触发动画过渡
|
||||||
|
controls.set({
|
||||||
|
y: -100, // Y 轴位置:从上方 100px 处开始
|
||||||
|
opacity: 0, // 透明度:完全透明
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 2:立即更新显示的标题 =====
|
||||||
|
// 因为首次渲染不需要保留旧标题,直接设置为传入的标题
|
||||||
|
setDisplayedTitle(currentTitle);
|
||||||
|
|
||||||
|
// ===== 步骤 3:执行滑入动画(从上方滑入到正常位置)=====
|
||||||
|
// - y: 0:从上方移动到正常位置
|
||||||
|
// - opacity: 1:从透明变为不透明
|
||||||
|
// - duration: 0.5s:动画时长 0.5 秒
|
||||||
|
// - ease: "easeOut":快速开始,缓慢结束的缓动函数
|
||||||
|
controls.start({
|
||||||
|
y: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 4:标记首次渲染完成 =====
|
||||||
|
isFirstRender.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 标题变化 ====================
|
||||||
|
else if (titleChanged) {
|
||||||
|
// ===== 步骤 1:先执行离开动画(向上离开)=====
|
||||||
|
// 作用:显示旧标题向上滑出
|
||||||
|
// - y: -100:向上移动到视图外部
|
||||||
|
// - opacity: 0:同时淡出
|
||||||
|
// - duration: 0.3s:较短的离开动画时长,显得更轻快
|
||||||
|
// - ease: "easeIn":缓慢开始,快速结束的缓动函数
|
||||||
|
controls.start({
|
||||||
|
y: -100, // 向上移动到外部
|
||||||
|
opacity: 0, // 淡出
|
||||||
|
transition: { duration: 0.3, ease: "easeIn" },
|
||||||
|
}).then(() => {
|
||||||
|
// ===== 步骤 2:离开动画完成后,更新显示的标题为新标题 =====
|
||||||
|
// 此时旧标题已经完全离开视图,更新标题用户看不见
|
||||||
|
setDisplayedTitle(currentTitle);
|
||||||
|
|
||||||
|
// ===== 步骤 3:执行进入动画(从上方进入)=====
|
||||||
|
// 作用:显示新标题从上方滑入
|
||||||
|
// - y: 0:从上方移动到正常位置
|
||||||
|
// - opacity: 1:从透明变为不透明
|
||||||
|
// - duration: 0.5s:进入动画时长
|
||||||
|
// - ease: "easeOut":快速开始,缓慢结束的缓动函数
|
||||||
|
controls.start({
|
||||||
|
y: 0, // 从上方移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentTitle, controls, displayedTitle]);
|
||||||
|
|
||||||
|
// ==================== 返回值 ====================
|
||||||
|
// 返回动画控制器和延迟显示的标题
|
||||||
|
return {
|
||||||
|
controls, // motion 动画控制器,绑定到 motion.header 的 animate 属性
|
||||||
|
displayedTitle, // 延迟显示的标题,用于在动画过渡期间保持视觉连续性
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import backImg from "~/assets/images/map_bi/right_utils/branch_office/ico1.png";
|
|
||||||
import fullImg from "~/assets/images/map_bi/right_utils/branch_office/ico2.png";
|
|
||||||
import img2Img from "~/assets/images/map_bi/right_utils/branch_office/ico3.png";
|
|
||||||
import img4Img from "~/assets/images/map_bi/right_utils/branch_office/ico4.png";
|
|
||||||
import delImg from "~/assets/images/map_bi/right_utils/branch_office/ico5.png";
|
|
||||||
import mapImg from "~/assets/images/map_bi/right_utils/branch_office/ico6.png";
|
|
||||||
import qixiangImg from "~/assets/images/map_bi/right_utils/branch_office/ico7.png";
|
|
||||||
import sisetuImg from "~/assets/images/map_bi/right_utils/branch_office/ico8.png";
|
|
||||||
import bianjieImg from "~/assets/images/map_bi/right_utils/branch_office/ico9.png";
|
|
||||||
|
|
||||||
export const branchOfficeUtilsList = [
|
|
||||||
{
|
|
||||||
img: backImg,
|
|
||||||
label: "返回主系统",
|
|
||||||
type: "back",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: fullImg,
|
|
||||||
check: false,
|
|
||||||
label: "全屏",
|
|
||||||
type: "full",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: img2Img,
|
|
||||||
label: "返回中心点",
|
|
||||||
type: "return",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: img4Img,
|
|
||||||
check: false,
|
|
||||||
label: "切换视角",
|
|
||||||
type: "scene",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: delImg,
|
|
||||||
label: "删除标记点",
|
|
||||||
type: "del",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: mapImg,
|
|
||||||
check: false,
|
|
||||||
label: "纯净地图",
|
|
||||||
type: "pureMap",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: qixiangImg,
|
|
||||||
label: "气象监测",
|
|
||||||
type: "weather",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: sisetuImg,
|
|
||||||
check: false,
|
|
||||||
label: "四色图",
|
|
||||||
type: "fourColor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: bianjieImg,
|
|
||||||
check: false,
|
|
||||||
label: "边界",
|
|
||||||
type: "wall",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
|
import { motion } from "motion/react";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { CSSTransition } from "react-transition-group";
|
|
||||||
import tooltipImg2 from "~/assets/images/map_bi/right_utils/branch_office/bg11.png";
|
import tooltipImg2 from "~/assets/images/map_bi/right_utils/branch_office/bg11.png";
|
||||||
import buttonBg from "~/assets/images/map_bi/right_utils/branch_office/button.png";
|
import buttonBg from "~/assets/images/map_bi/right_utils/branch_office/button.png";
|
||||||
import tooltipImg1 from "~/assets/images/map_bi/right_utils/port/tooltip.png";
|
import tooltipImg1 from "~/assets/images/map_bi/right_utils/port/tooltip.png";
|
||||||
|
|
@ -11,9 +11,10 @@ import {
|
||||||
clickBackMittKey,
|
clickBackMittKey,
|
||||||
clickMarkPointMittKey,
|
clickMarkPointMittKey,
|
||||||
deletePeoplePositionPointMittKey,
|
deletePeoplePositionPointMittKey,
|
||||||
initRightUtilsMittKey,
|
|
||||||
resetAllBottomUtilsCheckMittKey,
|
resetAllBottomUtilsCheckMittKey,
|
||||||
} from "~/pages/Container/Map/js/mittKey";
|
} from "~/pages/Container/Map/js/mittKey";
|
||||||
|
import { useChildMenuAnimation } from "./useChildMenuAnimation";
|
||||||
|
import { useRightUtilsAnimation } from "./useRightUtilsAnimation";
|
||||||
import { utilsList } from "./utilsList";
|
import { utilsList } from "./utilsList";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
|
|
@ -21,18 +22,25 @@ function RightUtils(props) {
|
||||||
const { currentPort, mapMethods, pureMap, currentBranchOffice, bottomUtilsCurrentIndex } = useContext(Context);
|
const { currentPort, mapMethods, pureMap, currentBranchOffice, bottomUtilsCurrentIndex } = useContext(Context);
|
||||||
|
|
||||||
const [list, setList] = useState(utilsList);
|
const [list, setList] = useState(utilsList);
|
||||||
const [isShowChildLevel, setIsShowChildLevel] = useState(false);
|
const [isShowChildLevel, setIsShowChildLevel] = useState(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
controls: rightUtilsControls,
|
||||||
|
displayedMode: rightUtilsDisplayedMode,
|
||||||
|
isVisible: rightUtilsIsVisible,
|
||||||
|
} = useRightUtilsAnimation(
|
||||||
|
!currentBranchOffice,
|
||||||
|
currentBranchOffice && bottomUtilsCurrentIndex !== -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { controls: childMenuControls } = useChildMenuAnimation(
|
||||||
|
currentBranchOffice && bottomUtilsCurrentIndex !== -1 && isShowChildLevel,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mitt.on(initRightUtilsMittKey, () => {
|
if (bottomUtilsCurrentIndex === -1)
|
||||||
mapMethods.current?.removeFourColorDiagram();
|
setIsShowChildLevel(true);
|
||||||
mapMethods.current?.removeWall();
|
}, [bottomUtilsCurrentIndex]);
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mitt.off(initRightUtilsMittKey);
|
|
||||||
};
|
|
||||||
}, [currentBranchOffice]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mitt.on(clickBackMittKey, () => {
|
mitt.on(clickBackMittKey, () => {
|
||||||
|
|
@ -112,10 +120,10 @@ function RightUtils(props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPortUtils = () => {
|
const renderPortUtils = () => {
|
||||||
|
if (rightUtilsDisplayedMode === "port") {
|
||||||
return (
|
return (
|
||||||
<div className="right_utils port">
|
<>
|
||||||
{
|
{list.map((item, index) => (
|
||||||
list.map((item, index) => (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="option"
|
className="option"
|
||||||
|
|
@ -124,29 +132,19 @@ function RightUtils(props) {
|
||||||
<div style={{ backgroundImage: `url(${tooltipImg1})` }} className="tooltip">{item.label}</div>
|
<div style={{ backgroundImage: `url(${tooltipImg1})` }} className="tooltip">{item.label}</div>
|
||||||
<img src={item.check ? item.checkImgPort : item.imgPort} alt="" />
|
<img src={item.check ? item.checkImgPort : item.imgPort} alt="" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
}
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBranchOfficeUtils = () => {
|
const renderBranchOfficeUtils = () => {
|
||||||
|
if (rightUtilsDisplayedMode === "branchOffice") {
|
||||||
return (
|
return (
|
||||||
<div className="right_utils branch_office">
|
<>
|
||||||
<CSSTransition
|
<motion.div className="options" animate={childMenuControls}>
|
||||||
timeout={1000}
|
{list.map((item, index) => (
|
||||||
classNames={{
|
|
||||||
enter: "animate__animated animate__faster",
|
|
||||||
enterActive: "animate__animated animate__faster animate__bounceInUp",
|
|
||||||
exit: "animate__animated animate__faster",
|
|
||||||
exitActive: "animate__animated animate__faster animate__bounceOutDown",
|
|
||||||
}}
|
|
||||||
unmountOnExit
|
|
||||||
in={(isShowChildLevel && bottomUtilsCurrentIndex !== -1)}
|
|
||||||
>
|
|
||||||
<div className="options">
|
|
||||||
{
|
|
||||||
list.map((item, index) => (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`option ${item.check ? "active" : ""}`}
|
className={`option ${item.check ? "active" : ""}`}
|
||||||
|
|
@ -155,25 +153,33 @@ function RightUtils(props) {
|
||||||
<div style={{ backgroundImage: `url(${tooltipImg2})` }} className="tooltip">{item.label}</div>
|
<div style={{ backgroundImage: `url(${tooltipImg2})` }} className="tooltip">{item.label}</div>
|
||||||
<img src={item.imgBranchOffice} alt="" />
|
<img src={item.imgBranchOffice} alt="" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
}
|
</motion.div>
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
{(bottomUtilsCurrentIndex !== -1) && (
|
|
||||||
<div
|
<div
|
||||||
className={`bg ${isShowChildLevel ? "active" : ""}`}
|
className={`bg ${isShowChildLevel ? "active" : ""}`}
|
||||||
onClick={() => setIsShowChildLevel(!isShowChildLevel)}
|
onClick={() => setIsShowChildLevel(!isShowChildLevel)}
|
||||||
>
|
>
|
||||||
<img src={buttonBg} alt="" />
|
<img src={buttonBg} alt="" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map_content_right_utils_container">
|
<div className="map_content_right_utils_container">
|
||||||
{!currentBranchOffice ? renderPortUtils() : renderBranchOfficeUtils()}
|
<motion.div
|
||||||
|
className={rightUtilsDisplayedMode === "port" ? "right_utils port" : "right_utils branch_office"}
|
||||||
|
animate={rightUtilsControls}
|
||||||
|
>
|
||||||
|
{rightUtilsIsVisible && (
|
||||||
|
<>
|
||||||
|
{renderPortUtils()}
|
||||||
|
{renderBranchOfficeUtils()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid rgba(0, 126, 255, 0.58);
|
border: 1px solid rgba(0, 126, 255, 0.58);
|
||||||
border-radius: 50px 50px 0 0;
|
border-radius: 50px 50px 0 0;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import backImg from "~/assets/images/map_bi/right_utils/port/back.png";
|
|
||||||
import bianjieImg from "~/assets/images/map_bi/right_utils/port/bianjie.png";
|
|
||||||
import bianjieOnImg from "~/assets/images/map_bi/right_utils/port/bianjie_on.png";
|
|
||||||
import delImg from "~/assets/images/map_bi/right_utils/port/del.png";
|
|
||||||
import fullImg from "~/assets/images/map_bi/right_utils/port/full.png";
|
|
||||||
import fullOnImg from "~/assets/images/map_bi/right_utils/port/full_on.png";
|
|
||||||
import img2Img from "~/assets/images/map_bi/right_utils/port/img2.png";
|
|
||||||
import img4Img from "~/assets/images/map_bi/right_utils/port/img4.png";
|
|
||||||
import img4OnImg from "~/assets/images/map_bi/right_utils/port/img4_on.png";
|
|
||||||
import mapImg from "~/assets/images/map_bi/right_utils/port/map.png";
|
|
||||||
import mapOnImg from "~/assets/images/map_bi/right_utils/port/map_on.png";
|
|
||||||
import qixiangImg from "~/assets/images/map_bi/right_utils/port/qixiang.png";
|
|
||||||
import sisetuImg from "~/assets/images/map_bi/right_utils/port/sisetu.png";
|
|
||||||
import sisetuOnImg from "~/assets/images/map_bi/right_utils/port/sisetu_on.png";
|
|
||||||
|
|
||||||
export const portUtilsList = [
|
|
||||||
{
|
|
||||||
img: backImg,
|
|
||||||
label: "返回主系统",
|
|
||||||
type: "back",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: fullImg,
|
|
||||||
checkImg: fullOnImg,
|
|
||||||
check: false,
|
|
||||||
label: "全屏",
|
|
||||||
type: "full",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: img2Img,
|
|
||||||
label: "返回中心点",
|
|
||||||
type: "return",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: img4Img,
|
|
||||||
checkImg: img4OnImg,
|
|
||||||
check: false,
|
|
||||||
label: "切换视角",
|
|
||||||
type: "scene",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: delImg,
|
|
||||||
label: "删除标记点",
|
|
||||||
type: "del",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: mapImg,
|
|
||||||
checkImg: mapOnImg,
|
|
||||||
check: false,
|
|
||||||
label: "纯净地图",
|
|
||||||
type: "pureMap",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: qixiangImg,
|
|
||||||
label: "气象监测",
|
|
||||||
type: "weather",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: sisetuImg,
|
|
||||||
checkImg: sisetuOnImg,
|
|
||||||
check: false,
|
|
||||||
label: "四色图",
|
|
||||||
type: "fourColor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: bianjieImg,
|
|
||||||
checkImg: bianjieOnImg,
|
|
||||||
check: false,
|
|
||||||
label: "边界",
|
|
||||||
type: "wall",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RightUtils 子菜单动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 监听子菜单的展开/收起状态变化
|
||||||
|
* 2. 执行炫酷的弹跳动画效果(从下向上进入,从上向下离开)
|
||||||
|
* 3. 支持首次加载动画,初始化时就会显示并执行进入动画
|
||||||
|
* 4. 使用弹跳、缩放、透明度三重动画效果,让动画更加生动
|
||||||
|
*
|
||||||
|
* 动画效果详解:
|
||||||
|
* - 展开动画(进入):
|
||||||
|
* - 位置:从下方 80px 处冲到上方 15px(超出目标),轻微回落到 5px,最后稳定到 0
|
||||||
|
* - 缩放:从 0.9 倍缩小到 1.05 倍(轻微放大),轻微缩小到 0.98,回到正常 1.0
|
||||||
|
* - 透明度:从透明到不透明
|
||||||
|
* - 时长:0.7 秒
|
||||||
|
* - 缓动:使用强弹性贝塞尔曲线 [0.34, 1.56, 0.64, 1],产生明显的过冲和回弹效果
|
||||||
|
*
|
||||||
|
* - 收起动画(离开):
|
||||||
|
* - 位置:从正常位置向上弹跳 20px,回落到 10px,然后向下离开到 100px
|
||||||
|
* - 缩放:轻微放大到 1.02,然后缩小到 0.85
|
||||||
|
* - 透明度:保持可见再淡出
|
||||||
|
* - 时长:0.5 秒
|
||||||
|
* - 缓动:使用超弹性曲线 [0.68, -0.6, 0.32, 1.6],产生夸张的反向弹跳
|
||||||
|
*
|
||||||
|
* @param {boolean} isVisible - 子菜单是否可见
|
||||||
|
* - true: 子菜单应该展开,执行展开动画
|
||||||
|
* - false: 子菜单应该收起,执行收起动画
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - controls: motion 动画控制器,绑定到 motion.div 的 animate 属性
|
||||||
|
*/
|
||||||
|
export function useChildMenuAnimation(isVisible) {
|
||||||
|
// ==================== 状态和引用管理 ====================
|
||||||
|
|
||||||
|
// motion 动画控制器,用于控制 motion.div 的动画
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
// 标记子菜单是否已经执行过展开动画
|
||||||
|
// 作用:防止重复执行展开动画,只在首次显示时执行
|
||||||
|
const isExpanded = useRef(false);
|
||||||
|
|
||||||
|
// 标记是否是首次渲染
|
||||||
|
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ==================== 首次渲染 ====================
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
if (isVisible) {
|
||||||
|
// ===== 执行展开动画:从下向上炫酷弹跳进入 =====
|
||||||
|
controls.start({
|
||||||
|
// Y 轴位置关键帧(四个关键帧实现弹跳效果):
|
||||||
|
// ① 80:从下方 80px 处开始
|
||||||
|
// ② -15:快速冲到上方 15px(超出目标位置,产生过冲效果)
|
||||||
|
// ③ 5:轻微回落到下方 5px
|
||||||
|
// ④ 0:最终稳定到正常位置
|
||||||
|
y: [80, -15, 5, 0],
|
||||||
|
|
||||||
|
// 透明度关键帧:
|
||||||
|
// ① 0:初始完全透明
|
||||||
|
// ② 1:快速变为不透明
|
||||||
|
// ③ 1:保持不透明
|
||||||
|
// ④ 1:保持不透明
|
||||||
|
opacity: [0, 1, 1, 1],
|
||||||
|
|
||||||
|
// 缩放关键帧(配合位置变化,增强弹跳感):
|
||||||
|
// ① 0.9:从 90% 大小开始(轻微缩小)
|
||||||
|
// ② 1.05:放大到 105%(轻微放大,配合位置过冲)
|
||||||
|
// ③ 0.98:缩小到 98%(轻微缩小,配合位置回落)
|
||||||
|
// ④ 1:回到正常大小 100%
|
||||||
|
scale: [0.9, 1.05, 0.98, 1],
|
||||||
|
|
||||||
|
transition: {
|
||||||
|
duration: 0.7, // 总动画时长:0.7 秒
|
||||||
|
// 关键帧时间点分布:
|
||||||
|
// ① 0:0% 时(0s)在 y=80 处
|
||||||
|
// ② 0.5:50% 时(0.35s)在 y=-15 处(过冲峰值)
|
||||||
|
// ③ 0.75:75% 时(0.525s)在 y=5 处(回落)
|
||||||
|
// ④ 1:100% 时(0.7s)在 y=0 处(稳定)
|
||||||
|
times: [0, 0.5, 0.75, 1],
|
||||||
|
|
||||||
|
// 强弹性贝塞尔曲线 [0.34, 1.56, 0.64, 1]:
|
||||||
|
// - 第一个值 0.34:控制起始加速度(较小,平稳开始)
|
||||||
|
// - 第二个值 1.56:控制起始减速度(大于 1,产生过冲)
|
||||||
|
// - 第三个值 0.64:控制结束加速度(小于 1,平滑过渡)
|
||||||
|
// - 第四个值 1:控制结束减速度(正常结束)
|
||||||
|
// 这个曲线会产生明显的过冲和回弹效果
|
||||||
|
ease: [0.34, 1.56, 0.64, 1],
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// ===== 动画完成,标记状态 =====
|
||||||
|
isExpanded.current = true; // 标记已展开
|
||||||
|
isFirstRender.current = false; // 标记首次渲染完成
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 如果首次渲染时不需要显示,直接标记首次渲染完成
|
||||||
|
isFirstRender.current = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 展开动画(非首次渲染)====================
|
||||||
|
if (isVisible && !isExpanded.current) {
|
||||||
|
// ===== 从下向上炫酷弹跳进入 =====
|
||||||
|
controls.start({
|
||||||
|
y: [80, -15, 5, 0], // 位置:下方80px -> 上方15px(过冲)-> 下方5px(回落)-> 正常
|
||||||
|
opacity: [0, 1, 1, 1], // 透明度:透明 -> 不透明 -> 不透明 -> 不透明
|
||||||
|
scale: [0.9, 1.05, 0.98, 1], // 缩放:90% -> 105% -> 98% -> 100%
|
||||||
|
transition: {
|
||||||
|
duration: 0.7, // 总动画时长:0.7 秒
|
||||||
|
times: [0, 0.5, 0.75, 1], // 时间点分布
|
||||||
|
ease: [0.34, 1.56, 0.64, 1], // 强弹性贝塞尔曲线
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
isExpanded.current = true; // 标记已展开
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 收起动画 ====================
|
||||||
|
else if (!isVisible && isExpanded.current) {
|
||||||
|
// ===== 从上向下炫酷弹跳离开 =====
|
||||||
|
controls.start({
|
||||||
|
// Y 轴位置关键帧:
|
||||||
|
// ① 0:从正常位置开始
|
||||||
|
// ② -20:向上弹跳到上方 20px(反向过冲)
|
||||||
|
// ③ 10:回落到下方 10px
|
||||||
|
// ④ 100:向下离开到 100px 处
|
||||||
|
y: [0, -20, 10, 100],
|
||||||
|
|
||||||
|
// 透明度关键帧:
|
||||||
|
// ① 1:初始不透明
|
||||||
|
// ② 1:弹跳过程中保持不透明
|
||||||
|
// ③ 0.8:开始淡出
|
||||||
|
// ④ 0:完全透明
|
||||||
|
opacity: [1, 1, 0.8, 0],
|
||||||
|
|
||||||
|
// 缩放关键帧:
|
||||||
|
// ① 1:从正常大小开始
|
||||||
|
// ② 1.02:轻微放大到 102%(配合向上弹跳)
|
||||||
|
// ③ 0.95:缩小到 95%(配合回落)
|
||||||
|
// ④ 0.85:继续缩小到 85%(配合向下离开)
|
||||||
|
scale: [1, 1.02, 0.95, 0.85],
|
||||||
|
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 总动画时长:0.5 秒(比进入动画短,显得更轻快)
|
||||||
|
// 关键帧时间点分布:
|
||||||
|
// ① 0:0% 时(0s)在正常位置 y=0
|
||||||
|
// ② 0.2:20% 时(0.1s)在 y=-20 处(快速弹起)
|
||||||
|
// ③ 0.5:50% 时(0.25s)在 y=10 处(回落)
|
||||||
|
// ④ 1:100% 时(0.5s)在 y=100 处(完全离开)
|
||||||
|
times: [0, 0.2, 0.5, 1],
|
||||||
|
|
||||||
|
// 超弹性曲线 [0.68, -0.6, 0.32, 1.6]:
|
||||||
|
// - 第一个值 0.68:较大的起始加速度
|
||||||
|
// - 第二个值 -0.6:负值表示反向过冲(产生夸张的向上弹跳)
|
||||||
|
// - 第三个值 0.32:较小的结束加速度
|
||||||
|
// - 第四个值 1.6:大于 1 的结束减速度(产生回弹效果)
|
||||||
|
// 这个曲线会产生夸张的反向弹跳效果
|
||||||
|
ease: [0.68, -0.6, 0.32, 1.6],
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
isExpanded.current = false; // 标记已收起
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isVisible, controls]);
|
||||||
|
|
||||||
|
// 返回动画控制器
|
||||||
|
return {
|
||||||
|
controls,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { useAnimation } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RightUtils 组件动画 Hook
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 监听显示/隐藏状态变化
|
||||||
|
* 2. 执行从右侧进入、从右侧离开的动画效果
|
||||||
|
* 3. 支持模式切换(港口工具 ↔ 分公司工具)
|
||||||
|
* 4. 支持首次加载动画
|
||||||
|
* 5. 初始化时就会显示并执行进入动画
|
||||||
|
*
|
||||||
|
* 动画效果:
|
||||||
|
* - 进入动画:从右侧 100px 处滑入,同时淡入(x: 100 → 0, opacity: 0 → 1)
|
||||||
|
* - 离开动画:向右侧 100px 处滑出,同时淡出(x: 0 → 100, opacity: 1 → 0)
|
||||||
|
*
|
||||||
|
* 模式切换流程:
|
||||||
|
* 1. 旧模式执行离开动画(0.3秒)
|
||||||
|
* 2. 离开动画完成后,更新 displayedMode 为新模式
|
||||||
|
* 3. 设置元素到初始位置(右侧 100px)
|
||||||
|
* 4. 执行新模式的进入动画(0.5秒)
|
||||||
|
*
|
||||||
|
* @param {boolean} shouldShowPort - 是否应该显示港口工具
|
||||||
|
* @param {boolean} shouldShowBranchOffice - 是否应该显示分公司工具
|
||||||
|
* @returns {object} 返回包含以下属性的对象:
|
||||||
|
* - controls: motion 动画控制器
|
||||||
|
* - displayedMode: 延迟显示的模式('port' | 'branchOffice' | null)
|
||||||
|
* - isVisible: 元素可见性状态
|
||||||
|
*/
|
||||||
|
export function useRightUtilsAnimation(shouldShowPort, shouldShowBranchOffice) {
|
||||||
|
// ==================== 状态和引用管理 ====================
|
||||||
|
|
||||||
|
// motion 动画控制器,用于控制 motion.div 的动画
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
// 标记是否是首次渲染
|
||||||
|
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
// 标记是否正在执行动画
|
||||||
|
// 作用:防止在动画执行过程中触发新的动画,避免动画冲突
|
||||||
|
const isAnimating = useRef(false);
|
||||||
|
|
||||||
|
// 延迟显示的模式状态
|
||||||
|
// 作用:在模式切换时,先显示旧模式,动画完成后再更新为新模式
|
||||||
|
// 这样确保用户看到完整的离开动画和进入动画
|
||||||
|
const [displayedMode, setDisplayedMode] = useState(
|
||||||
|
shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 元素可见性状态
|
||||||
|
// 注意:motion.div 始终挂载,isVisible 只控制内容的显示/隐藏
|
||||||
|
// 这样可以保证动画控制器始终有效
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ==================== 计算当前应该显示的模式 ====================
|
||||||
|
// 根据 shouldShowPort 和 shouldShowBranchOffice 判断当前应该显示哪个模式
|
||||||
|
const currentMode = shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null;
|
||||||
|
|
||||||
|
// ==================== 首次渲染 ====================
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
if (currentMode) {
|
||||||
|
// ===== 步骤 1:立即更新显示的模式 =====
|
||||||
|
// 设置 displayedMode 为当前模式,让组件知道应该渲染哪种内容
|
||||||
|
setDisplayedMode(currentMode);
|
||||||
|
|
||||||
|
// ===== 步骤 2:设置初始动画状态 =====
|
||||||
|
// 此时 motion.div 还没有挂载(因为 isVisible 还是 false)
|
||||||
|
// controls.set() 会将初始状态保存到 controls 对象中
|
||||||
|
// 当 motion.div 挂载时,会读取这个初始状态
|
||||||
|
controls.set({
|
||||||
|
x: 100, // X 轴位置:从右侧 100px 处开始
|
||||||
|
opacity: 0, // 透明度:完全透明
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 步骤 3:显示元素,触发 motion.div 挂载 =====
|
||||||
|
// 设置 isVisible 为 true,触发组件重渲染
|
||||||
|
// motion.div 会挂载到 DOM,并读取 controls 的初始状态 { x: 100, opacity: 0 }
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// ===== 步骤 4:执行进入动画 =====
|
||||||
|
// 从初始位置(x: 100, opacity: 0)动画到正常位置(x: 0, opacity: 1)
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 移动到正常位置(X 轴 0)
|
||||||
|
opacity: 1, // 变为完全不透明
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 动画时长:0.5 秒
|
||||||
|
ease: "easeOut", // 缓动函数:快速开始,缓慢结束
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// ===== 步骤 5:动画完成,标记首次渲染结束 =====
|
||||||
|
isFirstRender.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 如果首次渲染时不需要显示(currentMode 为 null)
|
||||||
|
// 直接标记首次渲染完成,不需要执行动画
|
||||||
|
isFirstRender.current = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 隐藏状态(当前没有工具应该显示)====================
|
||||||
|
if (!currentMode) {
|
||||||
|
if (isVisible && !isAnimating.current) {
|
||||||
|
// ===== 执行离开动画 =====
|
||||||
|
isAnimating.current = true;
|
||||||
|
controls.start({
|
||||||
|
x: 100, // 向右移动到 100px 处
|
||||||
|
opacity: 0, // 同时淡出
|
||||||
|
transition: {
|
||||||
|
duration: 0.3, // 动画时长:0.3 秒(比进入动画短,显得更轻快)
|
||||||
|
ease: "easeIn", // 缓动函数:缓慢开始,快速结束
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// ===== 离开动画完成后,隐藏元素 =====
|
||||||
|
setIsVisible(false); // 隐藏内容(motion.div 仍然挂载)
|
||||||
|
setDisplayedMode(null); // 清空显示的模式
|
||||||
|
isAnimating.current = false; // 解除动画锁定
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 模式切换或显示 ====================
|
||||||
|
|
||||||
|
// 检查模式是否发生了变化(港口 ↔ 分公司)
|
||||||
|
const modeChanged = displayedMode !== currentMode;
|
||||||
|
|
||||||
|
// ===== 场景 1:模式切换(港口 ↔ 分公司)=====
|
||||||
|
if (modeChanged && !isAnimating.current && isVisible) {
|
||||||
|
isAnimating.current = true;
|
||||||
|
|
||||||
|
// -------- 步骤 1:执行离开动画(旧模式离开)--------
|
||||||
|
controls.start({
|
||||||
|
x: 100, // 向右移动到 100px 处
|
||||||
|
opacity: 0, // 同时淡出
|
||||||
|
transition: {
|
||||||
|
duration: 0.3, // 动画时长:0.3 秒
|
||||||
|
ease: "easeIn", // 缓动函数
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// -------- 步骤 2:离开动画完成后,更新显示的模式 --------
|
||||||
|
setDisplayedMode(currentMode);
|
||||||
|
|
||||||
|
// -------- 步骤 3:设置元素到初始位置(右侧外部)--------
|
||||||
|
controls.set({
|
||||||
|
x: 100, // 确保元素在右侧 100px 处
|
||||||
|
opacity: 0, // 确保元素是透明的
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------- 步骤 4:执行进入动画(新模式进入)--------
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 动画时长:0.5 秒
|
||||||
|
ease: "easeOut", // 缓动函数
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
// -------- 步骤 5:动画完成 --------
|
||||||
|
isAnimating.current = false; // 解除动画锁定
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 场景 2:元素不可见,但应该显示(例如:从隐藏状态切换到显示)=====
|
||||||
|
else if (!isVisible && currentMode) {
|
||||||
|
// -------- 步骤 1:显示元素 --------
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
// -------- 步骤 2:更新显示的模式 --------
|
||||||
|
setDisplayedMode(currentMode);
|
||||||
|
|
||||||
|
// -------- 步骤 3:设置元素到初始位置 --------
|
||||||
|
controls.set({
|
||||||
|
x: 100, // 右侧 100px 处
|
||||||
|
opacity: 0, // 透明
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------- 步骤 4:执行进入动画 --------
|
||||||
|
controls.start({
|
||||||
|
x: 0, // 移动到正常位置
|
||||||
|
opacity: 1, // 淡入
|
||||||
|
transition: {
|
||||||
|
duration: 0.5, // 动画时长:0.5 秒
|
||||||
|
ease: "easeOut", // 缓动函数
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [shouldShowPort, shouldShowBranchOffice, controls, displayedMode, isVisible]);
|
||||||
|
|
||||||
|
// ==================== 返回值 ====================
|
||||||
|
// 返回动画控制器、延迟显示的模式和可见性状态
|
||||||
|
return {
|
||||||
|
controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
|
||||||
|
displayedMode, // 延迟显示的模式,用于决定渲染 port 还是 branchOffice 内容
|
||||||
|
isVisible, // 元素可见性状态,用于控制内容的显示/隐藏
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useFullscreen, useMount } from "ahooks";
|
import { useFullscreen, useMount } from "ahooks";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import autoFit from "autofit.js";
|
import autoFit from "autofit.js";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
||||||
import BottomUtils from "./components/BottomUtils";
|
import BottomUtils from "./components/BottomUtils";
|
||||||
import CenterUtils from "./components/CenterUtils";
|
import CenterUtils from "./components/CenterUtils";
|
||||||
|
|
@ -15,8 +15,6 @@ import {
|
||||||
changeCoverMaskVisibleMittKey,
|
changeCoverMaskVisibleMittKey,
|
||||||
clickBranchOfficePointMittKey,
|
clickBranchOfficePointMittKey,
|
||||||
clickPortPointMittKey,
|
clickPortPointMittKey,
|
||||||
initBottomUtilsMittKey,
|
|
||||||
initRightUtilsMittKey,
|
|
||||||
} from "./js/mittKey";
|
} from "./js/mittKey";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
|
|
@ -51,8 +49,10 @@ function Map() {
|
||||||
initMap.externalEntryPort(query);
|
initMap.externalEntryPort(query);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
setTimeout(() => {
|
||||||
mapMethodsInstance.flyTo();
|
mapMethodsInstance.flyTo();
|
||||||
mapMethodsInstance.addPortPoint();
|
mapMethodsInstance.addPortPoint();
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
mitt.on(clickPortPointMittKey, (data) => {
|
mitt.on(clickPortPointMittKey, (data) => {
|
||||||
|
|
@ -84,11 +84,6 @@ function Map() {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mitt.emit(initBottomUtilsMittKey);
|
|
||||||
mitt.emit(initRightUtilsMittKey);
|
|
||||||
}, [currentBranchOffice]);
|
|
||||||
|
|
||||||
const providerValues = useMemo(
|
const providerValues = useMemo(
|
||||||
() => ({ viewer, mapMethods, currentPort, currentBranchOffice, area, bottomUtilsCurrentIndex, pureMap }),
|
() => ({ viewer, mapMethods, currentPort, currentBranchOffice, area, bottomUtilsCurrentIndex, pureMap }),
|
||||||
[viewer, mapMethods, currentPort, currentBranchOffice, area, bottomUtilsCurrentIndex, pureMap],
|
[viewer, mapMethods, currentPort, currentBranchOffice, area, bottomUtilsCurrentIndex, pureMap],
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ export const clickBackMittKey = "clickBack";
|
||||||
export const resetBottomCurrentIndexMittKey = "resetBottomCurrentIndex";
|
export const resetBottomCurrentIndexMittKey = "resetBottomCurrentIndex";
|
||||||
// 重置所有底部工具栏选中状态
|
// 重置所有底部工具栏选中状态
|
||||||
export const resetAllBottomUtilsCheckMittKey = "resetAllBottomUtilsCheck";
|
export const resetAllBottomUtilsCheckMittKey = "resetAllBottomUtilsCheck";
|
||||||
// 初始化底部工具栏
|
|
||||||
export const initBottomUtilsMittKey = "initBottomUtils";
|
|
||||||
// 改变覆盖蒙版显隐
|
// 改变覆盖蒙版显隐
|
||||||
export const changeCoverMaskVisibleMittKey = "changeCoverMaskVisible";
|
export const changeCoverMaskVisibleMittKey = "changeCoverMaskVisible";
|
||||||
// 改变人员轨迹选择窗口显隐
|
// 改变人员轨迹选择窗口显隐
|
||||||
|
|
@ -20,5 +18,3 @@ export const changePeopleTrajectorySelectVisibleMittKey = "changePeopleTrajector
|
||||||
export const providePeoplePositionListMittKey = "providePeoplePositionList";
|
export const providePeoplePositionListMittKey = "providePeoplePositionList";
|
||||||
// 删除人员轨迹点位
|
// 删除人员轨迹点位
|
||||||
export const deletePeoplePositionPointMittKey = "deletePeoplePositionPoint";
|
export const deletePeoplePositionPointMittKey = "deletePeoplePositionPoint";
|
||||||
// 初始化右侧工具栏
|
|
||||||
export const initRightUtilsMittKey = "initRightUtils";
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ export default class PointClickEvent {
|
||||||
mitt.emit(clickPortPointMittKey, data);
|
mitt.emit(clickPortPointMittKey, data);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
this.#mapMethods.removeFourColorDiagram();
|
||||||
|
this.#mapMethods.removeWall();
|
||||||
this.#mapMethods.flyTo({ longitude: data.position.x, latitude: data.position.y, height: 2000 });
|
this.#mapMethods.flyTo({ longitude: data.position.x, latitude: data.position.y, height: 2000 });
|
||||||
this.#mapMethods.addBranchOfficePoint("", {
|
this.#mapMethods.addBranchOfficePoint("", {
|
||||||
corpName: data.name,
|
corpName: data.name,
|
||||||
|
|
@ -115,6 +117,8 @@ export default class PointClickEvent {
|
||||||
this.#mapMethods.removeBranchOfficePoint();
|
this.#mapMethods.removeBranchOfficePoint();
|
||||||
this.#mapMethods.removeMarkPoint();
|
this.#mapMethods.removeMarkPoint();
|
||||||
this.#mapMethods.addBranchOfficePoint("", data);
|
this.#mapMethods.addBranchOfficePoint("", data);
|
||||||
|
this.#mapMethods.removeFourColorDiagram();
|
||||||
|
this.#mapMethods.removeWall();
|
||||||
this.#mapMethods.flyTo({ longitude: data.longitude, latitude: data.latitude, height: 2000 });
|
this.#mapMethods.flyTo({ longitude: data.longitude, latitude: data.latitude, height: 2000 });
|
||||||
mitt.emit(deletePeoplePositionPointMittKey);
|
mitt.emit(deletePeoplePositionPointMittKey);
|
||||||
mitt.emit(clickBranchOfficePointMittKey, data);
|
mitt.emit(clickBranchOfficePointMittKey, data);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue