feat: 增加个人信息drawer

This commit is contained in:
liuyonghe0111 2025-11-12 15:51:27 +08:00
parent 85652b1c89
commit e274c1425c
2 changed files with 89 additions and 18 deletions

View File

@ -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)',
// 遮罩使用传入的 zIndexdrawer 作为子元素会自然在遮罩之上
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;
}

View File

@ -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)}>