feat: 增加个人信息drawer
This commit is contained in:
parent
85652b1c89
commit
e274c1425c
|
|
@ -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<HTMLDivElement>(null);
|
||||
const maskRef = useRef<HTMLDivElement>(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(
|
||||
<div
|
||||
ref={drawerRef}
|
||||
style={getPositionStyles()}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// 抽屉内容
|
||||
const drawerContent = (
|
||||
<div
|
||||
ref={drawerRef}
|
||||
style={getPositionStyles()}
|
||||
|
|
@ -189,4 +239,19 @@ export default function Drawer({
|
|||
{children}
|
||||
</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}
|
||||
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="flex justify-between">
|
||||
<span onClick={() => setIsOpen(false)}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue