Spaces:
Sleeping
Sleeping
import React, { useEffect, useRef } from 'react'; | |
import ReactDOM from 'react-dom'; | |
interface ModalProps { | |
isOpen: boolean; | |
onClose: () => void; | |
title?: string; | |
children: React.ReactNode; | |
footer?: React.ReactNode; | |
size?: 'sm' | 'md' | 'lg'; | |
closeOnClickOutside?: boolean; | |
} | |
const Modal: React.FC<ModalProps> = ({ | |
isOpen, | |
onClose, | |
title, | |
children, | |
footer, | |
size = 'md', | |
closeOnClickOutside = true | |
}) => { | |
const modalRef = useRef<HTMLDivElement>(null); | |
// 处理ESC按键关闭模态框 | |
useEffect(() => { | |
const handleEscKey = (event: KeyboardEvent) => { | |
if (event.key === 'Escape' && isOpen) { | |
onClose(); | |
} | |
}; | |
window.addEventListener('keydown', handleEscKey); | |
return () => { | |
window.removeEventListener('keydown', handleEscKey); | |
}; | |
}, [isOpen, onClose]); | |
// 阻止点击模态框内部时传播到外部背景 | |
const handleModalClick = (e: React.MouseEvent) => { | |
e.stopPropagation(); | |
}; | |
// 处理点击背景关闭模态框 | |
const handleBackdropClick = (e: React.MouseEvent) => { | |
if (closeOnClickOutside && modalRef.current && !modalRef.current.contains(e.target as Node)) { | |
onClose(); | |
} | |
}; | |
// 防止模态框打开时页面滚动 | |
useEffect(() => { | |
if (isOpen) { | |
document.body.style.overflow = 'hidden'; | |
} else { | |
document.body.style.overflow = ''; | |
} | |
return () => { | |
document.body.style.overflow = ''; | |
}; | |
}, [isOpen]); | |
// 获取模态框大小样式 | |
const getSizeClass = () => { | |
switch (size) { | |
case 'sm': | |
return 'max-w-md'; | |
case 'md': | |
return 'max-w-lg'; | |
case 'lg': | |
return 'max-w-2xl'; | |
default: | |
return 'max-w-lg'; | |
} | |
}; | |
if (!isOpen) return null; | |
// 使用React Portal将模态框渲染到DOM树的最顶层 | |
return ReactDOM.createPortal( | |
<div | |
className={`ios-modal-backdrop open fixed inset-0 z-50 overflow-auto bg-black bg-opacity-40 flex items-center justify-center p-4`} | |
onClick={handleBackdropClick} | |
> | |
<div | |
ref={modalRef} | |
className={`ios-modal ${getSizeClass()} bg-white rounded-xl shadow-xl transform transition-all`} | |
onClick={handleModalClick} | |
> | |
{title && ( | |
<div className="ios-modal-header px-6 py-4 border-b border-gray-200"> | |
<h3 className="ios-modal-title text-lg font-semibold text-center">{title}</h3> | |
</div> | |
)} | |
<div className="ios-modal-body p-6 max-h-[70vh] overflow-auto"> | |
{children} | |
</div> | |
{footer && ( | |
<div className="ios-modal-footer border-t border-gray-200"> | |
{footer} | |
</div> | |
)} | |
</div> | |
</div>, | |
document.body | |
); | |
}; | |
// 预定义的模态框按钮布局 | |
export const ModalFooter: React.FC<{ | |
children: React.ReactNode; | |
className?: string; | |
}> = ({ children, className = '' }) => { | |
return ( | |
<div className={`flex border-t border-gray-200 ${className}`}> | |
{children} | |
</div> | |
); | |
}; | |
// 预定义的模态框按钮 | |
export const ModalButton: React.FC<{ | |
children: React.ReactNode; | |
onClick: () => void; | |
variant?: 'primary' | 'secondary' | 'danger'; | |
className?: string; | |
}> = ({ | |
children, | |
onClick, | |
variant = 'secondary', | |
className = '' | |
}) => { | |
const getVariantClass = () => { | |
switch (variant) { | |
case 'primary': | |
return 'text-blue-600 hover:bg-blue-50'; | |
case 'secondary': | |
return 'text-gray-600 hover:bg-gray-50'; | |
case 'danger': | |
return 'text-red-600 hover:bg-red-50'; | |
default: | |
return 'text-gray-600 hover:bg-gray-50'; | |
} | |
}; | |
return ( | |
<button | |
className={` | |
flex-1 py-3 px-5 text-center font-medium text-sm transition-colors duration-200 | |
${getVariantClass()} | |
${className} | |
`} | |
onClick={onClick} | |
> | |
{children} | |
</button> | |
); | |
}; | |
export default Modal; |