feat: 增加个人信息drawer
This commit is contained in:
parent
85652b1c89
commit
e274c1425c
|
|
@ -12,6 +12,8 @@ type DrawerProps = {
|
||||||
width?: number;
|
width?: number;
|
||||||
destroyOnClose?: boolean;
|
destroyOnClose?: boolean;
|
||||||
zIndex?: number | string;
|
zIndex?: number | string;
|
||||||
|
mask?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 一个抽屉组件,支持打开关闭动画和自定义位置、宽度、z-index **无样式**
|
// 一个抽屉组件,支持打开关闭动画和自定义位置、宽度、z-index **无样式**
|
||||||
|
|
@ -24,18 +26,23 @@ export default function Drawer({
|
||||||
width = 400,
|
width = 400,
|
||||||
destroyOnClose = false,
|
destroyOnClose = false,
|
||||||
zIndex,
|
zIndex,
|
||||||
|
mask = false,
|
||||||
|
onClose,
|
||||||
}: DrawerProps) {
|
}: DrawerProps) {
|
||||||
// shouldRender 控制是否渲染 DOM(用于 destroyOnClose)
|
// shouldRender 控制是否渲染 DOM(用于 destroyOnClose)
|
||||||
// 初始值应该根据 open 的初始值来设置,避免初始 open=true 时组件不渲染
|
// 初始值应该根据 open 的初始值来设置,避免初始 open=true 时组件不渲染
|
||||||
const [shouldRender, setShouldRender] = useState(open);
|
const [shouldRender, setShouldRender] = useState(open);
|
||||||
const mounted = useMounted();
|
const mounted = useMounted();
|
||||||
const drawerRef = useRef<HTMLDivElement>(null);
|
const drawerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const maskRef = useRef<HTMLDivElement>(null);
|
||||||
const previousState = useRef<{
|
const previousState = useRef<{
|
||||||
styles: React.CSSProperties;
|
styles: React.CSSProperties;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
mask: boolean;
|
||||||
}>({
|
}>({
|
||||||
styles: {},
|
styles: {},
|
||||||
open: false,
|
open: false,
|
||||||
|
mask: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算隐藏时的 transform 值,需要在 useEffect 之前定义
|
// 计算隐藏时的 transform 值,需要在 useEffect 之前定义
|
||||||
|
|
@ -91,6 +98,29 @@ export default function Drawer({
|
||||||
}
|
}
|
||||||
}, [open, shouldRender, mounted, hiddenTransform]);
|
}, [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(() => {
|
const handleTransitionEnd = useCallback(() => {
|
||||||
// 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM
|
// 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM
|
||||||
|
|
@ -99,6 +129,13 @@ export default function Drawer({
|
||||||
}
|
}
|
||||||
}, [open, destroyOnClose, setShouldRender]);
|
}, [open, destroyOnClose, setShouldRender]);
|
||||||
|
|
||||||
|
// 处理遮罩点击
|
||||||
|
const handleMaskClick = useCallback(() => {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
const positionStyles = useMemo(() => {
|
const positionStyles = useMemo(() => {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'left':
|
case 'left':
|
||||||
|
|
@ -137,7 +174,10 @@ export default function Drawer({
|
||||||
// 根据位置计算样式
|
// 根据位置计算样式
|
||||||
const getPositionStyles = (): React.CSSProperties => {
|
const getPositionStyles = (): React.CSSProperties => {
|
||||||
// 如果状态未改变,直接返回缓存的样式
|
// 如果状态未改变,直接返回缓存的样式
|
||||||
if (previousState.current.open === open) {
|
if (
|
||||||
|
previousState.current.open === open &&
|
||||||
|
previousState.current.mask === mask
|
||||||
|
) {
|
||||||
return previousState.current.styles;
|
return previousState.current.styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +186,9 @@ export default function Drawer({
|
||||||
const baseStyles: React.CSSProperties = {
|
const baseStyles: React.CSSProperties = {
|
||||||
...positionStyles,
|
...positionStyles,
|
||||||
position: positionType,
|
position: positionType,
|
||||||
zIndex,
|
// 如果有遮罩,drawer 作为遮罩的子元素,不需要设置 zIndex
|
||||||
|
// 如果没有遮罩,drawer 需要自己的 zIndex
|
||||||
|
zIndex: mask ? undefined : zIndex,
|
||||||
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
|
||||||
transition: 'transform 0.3s ease-in-out',
|
transition: 'transform 0.3s ease-in-out',
|
||||||
// 初始状态总是先设置为隐藏,然后通过 useEffect 中的动画来显示
|
// 初始状态总是先设置为隐藏,然后通过 useEffect 中的动画来显示
|
||||||
|
|
@ -159,27 +201,35 @@ export default function Drawer({
|
||||||
previousState.current = {
|
previousState.current = {
|
||||||
styles: baseStyles,
|
styles: baseStyles,
|
||||||
open,
|
open,
|
||||||
|
mask,
|
||||||
};
|
};
|
||||||
return baseStyles;
|
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 (!mounted || !shouldRender) return null;
|
||||||
|
|
||||||
if (inBody) {
|
// 抽屉内容
|
||||||
return createPortal(
|
const drawerContent = (
|
||||||
<div
|
|
||||||
ref={drawerRef}
|
|
||||||
style={getPositionStyles()}
|
|
||||||
onTransitionEnd={handleTransitionEnd}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
ref={drawerRef}
|
ref={drawerRef}
|
||||||
style={getPositionStyles()}
|
style={getPositionStyles()}
|
||||||
|
|
@ -189,4 +239,19 @@ export default function Drawer({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 如果有遮罩,将抽屉内容包裹在遮罩层中
|
||||||
|
const content = mask ? (
|
||||||
|
<div ref={maskRef} style={getMaskStyles()} onClick={handleMaskClick}>
|
||||||
|
{drawerContent}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
drawerContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inBody) {
|
||||||
|
return createPortal(content, document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@ export default function Avatar() {
|
||||||
height={36}
|
height={36}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
<Drawer width={450} inBody open={isOpen}>
|
<Drawer
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
mask
|
||||||
|
width={450}
|
||||||
|
inBody
|
||||||
|
open={isOpen}
|
||||||
|
>
|
||||||
<div className="h-full w-full bg-[rgba(26,23,34,1)] px-10 pt-7">
|
<div className="h-full w-full bg-[rgba(26,23,34,1)] px-10 pt-7">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span onClick={() => setIsOpen(false)}>
|
<span onClick={() => setIsOpen(false)}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue