在之前的弹窗的设计中,有两处地方现在做一点小小的优化,就是把_Draggable.jsx
中的 onPointerEnter 事件 用 useLayoutEffect
来规换,效果更佳,同样的,在_ModelContainer.jsx
中也是一样。如下所示:
_Draggable.jsx
/** @jsxImportSource @emotion/react */
import { css, keyframes } from '@emotion/react'
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import { useOutsideClick } from './_useOutsideClick';
import { useWindowSize } from './_useWindowSize';
import { minHeight, minWidth } from './_ModelConfigure';
//弹窗的动画
const attentionKeyframes = keyframes`
from,to {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
`;
//弹窗的开始时动画
const anim = css`
animation: ${attentionKeyframes} 400ms ease;
`;
//弹窗的结束时动画
const stopAnim = css`
animation: null;
`;
const draggableHandler = ".model-handler"; // 拖动句柄的类名
/**
* 拖动组件,使被包裹的组件可以拖动,支持拖动句柄
* @param {是否启用拖动句柄 } enableHandler
* @param {拖动句柄的类名} draggableHandler
* @param {外部点击事件} onOutsideClick
*/
export default function Draggable({
children, // 子组件
enableDragging = true,
enableHandler = false, // 是否启用拖动句柄
stateMode
}) {
const [attentionStyle, setAttentionStyle] = useState(anim); // 弹窗动画,当点击外部时,弹窗会有一个动画效果
const [isDragging, setIsDragging] = useState(false); // 是否正在拖动
const [canDrag, setCanDrag] = useState(true); // 是否可以触发拖动操作,改变鼠标样式
const normalPos = useRef({ x: 0, y: 0 }); // 正常模式下弹窗的位置(translate的值)
const minPos = useRef({ x: 0, y: 0 }); // 最小化时的位置
const maxPos = { x: 0, y: 0 }; // 最大化时的位置,因为最大化时弹窗的位置是固定的,所以不需要ref
// 当所有模式下的位置变化都是通过position来反映到UI上的,所以position是唯一的位置状态
const [position, setPosition] = useState({x: 0, y: 0}); // 弹窗的位置(translate的值)
// 当鼠标按下时,记录鼠标的位置并以当前位置为基准进行拖动(相对位置),与position的差值为偏移量,position为上一次的偏移量。
// 因为采用的是translate的方式进行拖动,这种方式下,是以组件第一次渲染的位置为基准参考点(也就是相对0,0的位置)进行拖动的.
// 正常模式下的偏移量
const normalOffsetX = useRef(0); // x轴偏移量
const normalOffsetY = useRef(0); // y轴偏移量
// 最小化时的偏移量
const minOffsetX = useRef(0); // x轴偏移量
const minOffsetY = useRef(0); // y轴偏移量
const initedRect = useRef(0); // 初始化后的弹窗大小
const wrapperRef = useRef(null);
const windowSize = useWindowSize();
// 当点击外部时,弹窗会有一个注目动画效果
useOutsideClick(wrapperRef, () => {
setAttentionStyle(anim);
});
// 弹窗注目动画的监听
useEffect(function () {
// 弹窗动画监听事件
const listener = (e) => {
if (e.type === "animationend") {
setAttentionStyle(stopAnim);
}
};
if (wrapperRef.current !== null) {
wrapperRef.current.addEventListener("animationend", listener, true);
}
return () => {
if (wrapperRef.current !== null) {
wrapperRef.current.removeEventListener("animationend", listener);
}
};
}, []);
// document的鼠标移动事件和鼠标抬起事件监听
useEffect(() => {
// 鼠标移动事件
const handleMouseMove = (e) => {
if (isDragging) {
switch (stateMode) {
case 0:
const xt = e.clientX - minOffsetX.current;
const yt = e.clientY - minOffsetY.current;
const xtMinTop = -((windowSize.height - minHeight) / 2 - 10);
const xtMaxTop = (windowSize.height - minHeight) / 2 - 10;
const xtMinLeft = -((windowSize.width - minWidth) / 2 - 10);
const xtMaxLeft = (windowSize.width - minWidth) / 2 - 10;
const xm = xt < xtMinLeft ? xtMinLeft : xt > xtMaxLeft ? xtMaxLeft : xt;
const ym = yt < xtMinTop ? xtMinTop : yt > xtMaxTop ? xtMaxTop : yt;
minPos.current = { x: xm, y: ym};
setPosition({ ...minPos.current });
break;
case 2:
break;
default:
const xTmp = e.clientX - normalOffsetX.current;
const yTmp = e.clientY - normalOffsetY.current;
const minLetf = -(windowSize.width - initedRect.current.width) / 2;
const minTop = -(windowSize.height - initedRect.current.height) / 2;
const maxLeft = (windowSize.width - initedRect.current.width) / 2;
const maxTop = (windowSize.height - initedRect.current.height) / 2;
const x = xTmp < minLetf ? minLetf : xTmp > maxLeft ? maxLeft : xTmp;
const y = yTmp < minTop ? minTop : yTmp > maxTop ? maxTop : yTmp;
normalPos.current = { x, y };
setPosition({ ...normalPos.current });
break;
}
}
};
// 鼠标抬起事件
const handleMouseUp = (e) => {
if (e.button !== 0) return;
setIsDragging(false);
};
// 在相关的事件委托到document上
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
} else {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
// 组件卸载时移除事件
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging]);
// 弹窗位置的监听, 每当弹窗状态改变时,都会重新设置弹窗的位置, 将相应状态下的最后位置设置为当前位置
// 但最小化状态下的位置有所不同,因为最小化状态下的初始位置为左下角,每次从其它状态切换到最小化状态时都要进行相同的设置。
useEffect(() => {
switch (stateMode) {
case 0:
const initX = -((windowSize.width - minWidth - 20) / 2);
const initY = windowSize.height / 2 - minHeight + 10;
setPosition({ x: initX, y: initY });
minPos.current = { x: initX, y: initY };
break;
case 2:
setPosition({...maxPos.current});
break;
default:
setPosition({ ...normalPos.current });
break;
}
}, [stateMode]);
// ref对象的鼠标移动事件,用于判断是否在拖动句柄上
const onMouseMove = (e) => {
if (!enableDragging) {
setCanDrag(false);
return;
}
if (enableHandler) {
const clickedElement = e.target;
// 检查鼠标点击的 DOM 元素是否包含特定类名
if (clickedElement.classList.contains(draggableHandler)) {
setCanDrag(true);
} else {
setCanDrag(false);
}
}
}
// ref对象的鼠标按下事件,用于触发拖动操作,
// 如果启用了拖动句柄,那么只有在拖动句柄上按下鼠标才会触发拖动操作,
// 否则直接按下鼠标就会触发拖动操作
const handleMouseDown = (e) => {
if (!enableDragging) return;
switch (stateMode) {
case 0:
if (enableHandler) {
// 判断是否在拖动句柄上
const curElement = e.target;
// 检查鼠标点击的 DOM 元素是否包含特定类名
if (curElement.classList.contains(draggableHandler)) {
if (e.button !== 0) return;
setIsDragging(true);
minOffsetX.current = e.clientX - minPos.current.x;
minOffsetY.current = e.clientY - minPos.current.y;
} else {
setCanDrag(false);
}
} else {
if (e.button !== 0) return;
setIsDragging(true);
minOffsetX.current = e.clientX - minPos.current.x;
minOffsetY.current = e.clientY - minPos.current.y;
}
return;
case 2:
return;
default:
if (enableHandler) {
// 判断是否在拖动句柄上
const curElement = e.target;
// 检查鼠标点击的 DOM 元素是否包含特定类名
if (curElement.classList.contains(draggableHandler)) {
if (e.button !== 0) return;
setIsDragging(true);
normalOffsetX.current = e.clientX - normalPos.current.x;
normalOffsetY.current = e.clientY - normalPos.current.y;
} else {
setCanDrag(false);
}
} else {
if (e.button !== 0) return;
setIsDragging(true);
normalOffsetX.current = e.clientX - normalPos.current.x;
normalOffsetY.current = e.clientY - normalPos.current.y;
}
return;
}
};
// 初始化时获取弹窗的大小,此方法替代了onPointerEnter事件,效果要好一些
useLayoutEffect(() => {
if (wrapperRef.current) {
const rect = wrapperRef.current.getBoundingClientRect();
initedRect.current = {
width: rect.width,
height: rect.height,
};
}
}, []);
return (
<Box
ref={wrapperRef}
sx={{
transform: `translate(${position.x}px, ${position.y}px)`,
cursor: canDrag ? isDragging ? "grabbing" : "grab" : "default",
transition: isDragging ? null : `transform 200ms ease-in-out`,
}}
onMouseDown={handleMouseDown}
onMouseMove={onMouseMove}
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
// onPointerEnter={() => {
// if (initedRect.current === 0 && wrapperRef.current !== null) {
// const rect = wrapperRef.current.getBoundingClientRect();
// initedRect.current = {
// width: rect.width,
// height: rect.height,
// };
// }
// }}
>
<Box
sx={{
transform: `${isDragging ? "scale(1.03)" : "scale(1)"}`,
transition: `transform 200ms ease-in-out`,
}}
css={attentionStyle}
>
{
children
}
</Box>
</Box>
);
}
在 _ModelContainer.jsx
中做如下更改:
_ModelContainer.jsx
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import { useLayoutEffect, useRef, useState } from 'react';
import { Paper } from '@mui/material';
import { useModelState } from './useModel';
import { infoLevel } from './_ModelConfigure';
// 计算不同状态下的高度
const calHeight = (sizeMode, normalHeight) => {
switch (sizeMode) {
case 0:
return '45px';
case 1:
return normalHeight > 0 ? normalHeight + 'px' : 'auto';
case 2:
return '100vh';
default:
return 'auto';
}
}
// 最大化时的固定样式
const maxSizeCss = css`
width: 100vw;
height: 100vh;
top: 0;
left: 0;
`;
/**
* 弹窗容器
* @param {*} param0
* @returns
*/
const ModelContainer = ({ children }) => {
const modelState = useModelState();
const {
stateMode, // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化
level, // 弹窗的类型(主要是颜色类型),选项有:default, error, warning, success, info
isDark, //是否是暗黑模式
} = modelState;
const [nomalSize, setNormalSize] = useState({ width: 0, height: 0 });
const containerRef = useRef(null);
useLayoutEffect(() => {
if (containerRef.current !== null) {
const rect = containerRef.current.getBoundingClientRect();
setNormalSize({
width: rect.width,
height: rect.height,
});
}
}, []);
return (
<Paper
ref={containerRef}
css={css`
border: 1px solid #A0A0A0;
border-radius: 5px;
width: ${ stateMode === 2 ? '100vw' : stateMode === 0 ? '300px' : '576px' };
height: ${ calHeight(stateMode, nomalSize.height) };
overflow: hidden;
max-width: 100%;
max-height: 100vh;
display: flex;
flex-direction: column;
background-color: ${isDark ? '#333' : infoLevel[level] ? infoLevel[level].color : "white" };
${stateMode === 2 ? maxSizeCss : null};
transition: all 0.3s;
`}
>
{
children
}
</Paper>
);
};
export default ModelContainer;
以上两点做个补充。