"use client"; import { useEffect, type ReactNode } from "react"; import { X } from "lucide-react"; export type ModalProps = { open: boolean; title: ReactNode; onClose: () => void; children: ReactNode; /** 底部操作区(取消/主按钮等) */ footer?: ReactNode; /** 标题旁辅助说明 */ description?: ReactNode; /** 面板最大宽度 class,默认 max-w-md */ maxWidthClass?: string; /** 面板额外 class(如 max-h / flex) */ panelClassName?: string; /** 点遮罩关闭;表单/确认默认 false 防误关 */ dismissOnOverlay?: boolean; /** busy 时禁用关闭与 ESC */ busy?: boolean; /** 叠层高度,确认框用 1100,普通 1000 */ zIndexClass?: string; /** 无障碍:dialog | alertdialog */ role?: "dialog" | "alertdialog"; ariaLabel?: string; /** 自定义标题区(如带图标);提供时覆盖默认 title 行,仍渲染关闭钮除非 hideClose */ header?: ReactNode; hideClose?: boolean; /** 是否响应 ESC;封禁告知等不可关场景设 false */ closeOnEscape?: boolean; /** 内容区额外 class */ bodyClassName?: string; }; /** * 全站弹层壳:统一遮罩、面板、标题关闭钮与 ESC。 * 主 CTA 请用 btn-primary;危险操作用 btn-danger;取消用 btn-line。 */ export default function Modal({ open, title, onClose, children, footer, description, maxWidthClass = "max-w-md", panelClassName = "", dismissOnOverlay = false, busy = false, zIndexClass = "z-[1000]", role = "dialog", ariaLabel, header, hideClose = false, closeOnEscape = true, bodyClassName = "", }: ModalProps) { useEffect(() => { if (!open || !closeOnEscape) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !busy) onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, busy, onClose, closeOnEscape]); if (!open) return null; const label = ariaLabel || (typeof title === "string" ? title : undefined); return (
{ if (dismissOnOverlay && !busy) onClose(); }} role={role} aria-modal="true" aria-label={label} >
e.stopPropagation()} > {header ? (
{header}
{!hideClose && ( )}
) : (

{title}

{description ? (
{description}
) : null}
{!hideClose && }
)}
{children}
{footer ? (
{footer}
) : null}
); } function CloseButton({ onClick, disabled, }: { onClick: () => void; disabled?: boolean; }) { return ( ); }