| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
- "use client";
- import { useEffect, useState } from "react";
- import { createPortal } from "react-dom";
- import { cn } from "@/lib/utils";
- export function ModalShell({
- open,
- children,
- className,
- zIndexClassName = "z-[60]",
- onBackdropClick,
- /** 内容偏高时从顶部留白展示,避免卡片内滚动 */
- alignTop = false,
- }: {
- open: boolean;
- children: React.ReactNode;
- className?: string;
- zIndexClassName?: string;
- /** 点击遮罩时触发(子内容已阻止冒泡) */
- onBackdropClick?: () => void;
- alignTop?: boolean;
- }) {
- const [mounted, setMounted] = useState(false);
- useEffect(() => {
- /* eslint-disable-next-line react-hooks/set-state-in-effect -- document.body portal only after mount; avoids SSR mismatch */
- setMounted(true);
- }, []);
- if (!mounted) {
- return null;
- }
- return createPortal(
- <div
- role="presentation"
- className={cn(
- "fixed inset-0 flex justify-center px-4 transition-all duration-300",
- alignTop ? "items-start overflow-y-auto py-6 sm:py-10" : "items-center",
- zIndexClassName,
- open
- ? "pointer-events-auto bg-slate-900/55 opacity-100 backdrop-blur-[2px]"
- : "pointer-events-none bg-transparent opacity-0",
- )}
- onClick={
- onBackdropClick && open
- ? () => {
- onBackdropClick();
- }
- : undefined
- }
- >
- <div
- className={cn(
- "mx-auto w-full transition-all duration-300",
- open ? "translate-y-0 scale-100 opacity-100" : "translate-y-3 scale-[0.98] opacity-0",
- className,
- )}
- onClick={(e) => e.stopPropagation()}
- >
- {children}
- </div>
- </div>,
- document.body,
- );
- }
|