"use client"; import clsx from "clsx"; import { useState, useLayoutEffect, useRef } from "react"; import Portal from "@/components/common/portal/Portal"; /** * 为了弹窗里的表单校验开发整个组件: * 当弹窗关闭,校验弹窗里的表单时要保证弹窗里的表单元素还存在文档流中,所以这个组件采用display block/none 来控制弹窗的显示和隐藏 */ export default function CommonModal({ isOpen, destroyOnClose = false, contentClassName, contentStyle = {}, clickOutsideClose, onClose, animationEndCallback = () => {}, header, body, footer }: { isOpen: boolean; destroyOnClose?: boolean; contentClassName?: string; contentStyle?: React.CSSProperties; clickOutsideClose?: boolean; onClose: (e: boolean) => void; animationEndCallback?: (flag: string) => void; header?: React.ReactNode; body: React.ReactNode; footer?: React.ReactNode; }) { // show - 显示 // outing - 隐藏动画中 // hidden - 隐藏 const [rootVisible, setRootVisible] = useState<'show' | 'outing' | 'hidden'>('hidden'); const [mounted, setMounted] = useState(false); const cssInit = useRef(false); const rootRef = useRef(null); const footerRef = useRef(null); const headerRef = useRef(null); const [rootPddingBottom, setRootPddingBottom] = useState(0); const [rootPddingTop, setRootPddingTop] = useState(0); const baseClassNname = "absolute box-border transition-transform duration-300 ease-out"; let applyClassName = "bottom-0 left-0 h-17/20 w-full bg-white rounded-t-xl"; if(contentClassName) { applyClassName = contentClassName; } useLayoutEffect(() => { const original = document.documentElement.style.overflow; if(isOpen){ // destroy模式重新创建DOM if(destroyOnClose){ // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); } document.documentElement.style.overflow = "hidden"; requestAnimationFrame(()=>{ setRootVisible("show"); }); }else{ document.documentElement.style.overflow = ""; if(destroyOnClose){ // 先播放关闭动画 if(mounted){ // eslint-disable-next-line react-hooks/set-state-in-effect setRootVisible("outing"); } }else{ // 保留DOM if(cssInit.current){ // eslint-disable-next-line react-hooks/set-state-in-effect setRootVisible("outing"); } } } return ()=>{ document.documentElement.style.overflow = original; }; },[isOpen, destroyOnClose, mounted]); // useLayoutEffect(() => { // const original = document.documentElement.style.overflow; // if(isOpen) { // document.documentElement.style.overflow = "hidden"; // } else { // document.documentElement.style.overflow = ""; // } // // intent: 为了实现关闭时也有动画效果,告诉eslint忽略这个错误 // if(isOpen) { // // eslint-disable-next-line react-hooks/set-state-in-effect // setRootVisible('show'); // } else { // if(cssInit.current) { // // eslint-disable-next-line react-hooks/set-state-in-effect // setRootVisible('outing'); // } // } // return () => { document.documentElement.style.overflow = original; }; // },[isOpen]); // 当弹窗完全显示后计算 footer 高度 useLayoutEffect(() => { if (rootVisible === 'show' && !cssInit.current && footerRef.current && headerRef.current) { const footerHeight = footerRef.current.getBoundingClientRect().height; const headerHeight = headerRef.current.getBoundingClientRect().height; setRootPddingBottom(footerHeight); setRootPddingTop(headerHeight); cssInit.current = true; } }, [rootVisible]); const rootClick = (e: React.MouseEvent) => { e.stopPropagation(); if(clickOutsideClose) { onClose(false); } } // 如果是关闭,动画结束之后才隐藏 const handlerAnimationEnd = (e: React.AnimationEvent) => { // 如果子元素上也有自己的动画效果,也会触发父元素的动画事件 if (e.target !== e.currentTarget) return; if(e.animationName === "fade-out") { setRootVisible('hidden'); if(destroyOnClose){ setMounted(false); cssInit.current=false; } animationEndCallback('close'); } else { animationEndCallback('show'); } } // 初始化时是隐藏状态 // fade-in的时候display block + fade-in // fade-out的时候需要先加fade-out 类名,动画效果结束之后才能display none if(destroyOnClose && !mounted){ return null; } return (
{header && header}
{body}
{footer && footer}
); }