From e274c1425cca07274fe57a0b1a5a78117fbc9796 Mon Sep 17 00:00:00 2001 From: liuyonghe0111 <1763195287@qq.com> Date: Wed, 12 Nov 2025 15:51:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AFdrawer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/drawer.tsx | 99 ++++++++++++++++---- src/layouts/MainLayout/components/Avatar.tsx | 8 +- 2 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index f909aa8..81da9b6 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -12,6 +12,8 @@ type DrawerProps = { width?: number; destroyOnClose?: boolean; zIndex?: number | string; + mask?: boolean; + onClose?: () => void; }; // 一个抽屉组件,支持打开关闭动画和自定义位置、宽度、z-index **无样式** @@ -24,18 +26,23 @@ export default function Drawer({ width = 400, destroyOnClose = false, zIndex, + mask = false, + onClose, }: DrawerProps) { // shouldRender 控制是否渲染 DOM(用于 destroyOnClose) // 初始值应该根据 open 的初始值来设置,避免初始 open=true 时组件不渲染 const [shouldRender, setShouldRender] = useState(open); const mounted = useMounted(); const drawerRef = useRef(null); + const maskRef = useRef(null); const previousState = useRef<{ styles: React.CSSProperties; open: boolean; + mask: boolean; }>({ styles: {}, open: false, + mask: false, }); // 计算隐藏时的 transform 值,需要在 useEffect 之前定义 @@ -91,6 +98,29 @@ export default function Drawer({ } }, [open, shouldRender, mounted, hiddenTransform]); + // 处理遮罩动画 + useEffect(() => { + if (!mask || !shouldRender || !mounted || !maskRef.current) return; + + if (open) { + // 显示遮罩,淡入动画 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (maskRef.current) { + maskRef.current.style.opacity = '1'; + } + }); + }); + } else { + // 隐藏遮罩,淡出动画 + requestAnimationFrame(() => { + if (maskRef.current) { + maskRef.current.style.opacity = '0'; + } + }); + } + }, [mask, open, shouldRender, mounted]); + // 处理动画结束后的销毁 const handleTransitionEnd = useCallback(() => { // 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM @@ -99,6 +129,13 @@ export default function Drawer({ } }, [open, destroyOnClose, setShouldRender]); + // 处理遮罩点击 + const handleMaskClick = useCallback(() => { + if (onClose) { + onClose(); + } + }, [onClose]); + const positionStyles = useMemo(() => { switch (position) { case 'left': @@ -137,7 +174,10 @@ export default function Drawer({ // 根据位置计算样式 const getPositionStyles = (): React.CSSProperties => { // 如果状态未改变,直接返回缓存的样式 - if (previousState.current.open === open) { + if ( + previousState.current.open === open && + previousState.current.mask === mask + ) { return previousState.current.styles; } @@ -146,7 +186,9 @@ export default function Drawer({ const baseStyles: React.CSSProperties = { ...positionStyles, position: positionType, - zIndex, + // 如果有遮罩,drawer 作为遮罩的子元素,不需要设置 zIndex + // 如果没有遮罩,drawer 需要自己的 zIndex + zIndex: mask ? undefined : zIndex, boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', transition: 'transform 0.3s ease-in-out', // 初始状态总是先设置为隐藏,然后通过 useEffect 中的动画来显示 @@ -159,27 +201,35 @@ export default function Drawer({ previousState.current = { styles: baseStyles, open, + mask, }; return baseStyles; }; + // 获取遮罩样式 + const getMaskStyles = (): React.CSSProperties => { + const positionType = inBody ? 'fixed' : 'absolute'; + + return { + position: positionType, + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.45)', + // 遮罩使用传入的 zIndex,drawer 作为子元素会自然在遮罩之上 + zIndex, + opacity: 0, + transition: 'opacity 0.3s ease-in-out', + // 当抽屉关闭时,禁止遮罩的点击事件,让点击可以穿透到下层元素 + pointerEvents: open ? 'auto' : 'none', + }; + }; + if (!mounted || !shouldRender) return null; - if (inBody) { - return createPortal( -
e.stopPropagation()} - > - {children} -
, - document.body - ); - } - - return ( + // 抽屉内容 + const drawerContent = (
); + + // 如果有遮罩,将抽屉内容包裹在遮罩层中 + const content = mask ? ( +
+ {drawerContent} +
+ ) : ( + drawerContent + ); + + if (inBody) { + return createPortal(content, document.body); + } + + return content; } diff --git a/src/layouts/MainLayout/components/Avatar.tsx b/src/layouts/MainLayout/components/Avatar.tsx index 7a58d42..b3d41e3 100644 --- a/src/layouts/MainLayout/components/Avatar.tsx +++ b/src/layouts/MainLayout/components/Avatar.tsx @@ -15,7 +15,13 @@ export default function Avatar() { height={36} alt="avatar" /> - + setIsOpen(false)} + mask + width={450} + inBody + open={isOpen} + >
setIsOpen(false)}>