72 lines
1.8 KiB
TypeScript
72 lines
1.8 KiB
TypeScript
// app/components/ui/Modal.tsx
|
|
import React, { useEffect } from 'react';
|
|
|
|
interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: React.ReactNode;
|
|
children: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
width?: number | string;
|
|
}
|
|
|
|
export function Modal({ isOpen, onClose, title, children, footer, width = 500 }: ModalProps) {
|
|
// 点击ESC键关闭模态框
|
|
useEffect(() => {
|
|
const handleEsc = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && isOpen) {
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleEsc);
|
|
return () => window.removeEventListener('keydown', handleEsc);
|
|
}, [isOpen, onClose]);
|
|
|
|
// 禁用背景滚动
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = '';
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = '';
|
|
};
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="modal-backdrop"
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
<div
|
|
className="modal-content"
|
|
style={{ maxWidth: typeof width === 'number' ? `${width}px` : width }}
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<div className="modal-header">
|
|
<h3 className="text-lg font-medium">{title}</h3>
|
|
<button
|
|
className="text-gray-400 hover:text-gray-500"
|
|
onClick={onClose}
|
|
aria-label="关闭"
|
|
>
|
|
<i className="ri-close-line"></i>
|
|
</button>
|
|
</div>
|
|
<div className="modal-body py-4">
|
|
{children}
|
|
</div>
|
|
{footer && (
|
|
<div className="modal-footer flex justify-end space-x-2 pt-4 border-t border-gray-100">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |