142 lines
3.5 KiB
TypeScript
142 lines
3.5 KiB
TypeScript
/**
|
||
* Modal 组件
|
||
*
|
||
* 统一的弹窗组件,遵循三层架构设计
|
||
* 支持多种尺寸(small、medium、large、xlarge、fullscreen)
|
||
* 支持ESC键关闭、点击遮罩关闭、键盘导航
|
||
* 完整的ARIA标签支持,确保可访问性
|
||
*
|
||
* @created 2025-11-22
|
||
* @component
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import { useEffect, useRef, ReactNode } from 'react'
|
||
import { useThemeStyles } from '@/lib/ui-styles'
|
||
|
||
export type ModalSize = 'small' | 'medium' | 'large' | 'xlarge' | 'fullscreen'
|
||
|
||
interface ModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
title?: string
|
||
children: ReactNode
|
||
size?: ModalSize
|
||
showCloseButton?: boolean
|
||
closeOnOverlayClick?: boolean
|
||
closeOnEscape?: boolean
|
||
footer?: ReactNode
|
||
className?: string
|
||
}
|
||
|
||
export default function Modal({
|
||
isOpen,
|
||
onClose,
|
||
title,
|
||
children,
|
||
size = 'medium',
|
||
showCloseButton = true,
|
||
closeOnOverlayClick = true,
|
||
closeOnEscape = true,
|
||
footer,
|
||
className = '',
|
||
}: ModalProps) {
|
||
const styles = useThemeStyles()
|
||
const modalRef = useRef<HTMLDivElement>(null)
|
||
|
||
// ESC键关闭
|
||
useEffect(() => {
|
||
if (!isOpen || !closeOnEscape) return
|
||
|
||
const handleEscape = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') {
|
||
onClose()
|
||
}
|
||
}
|
||
|
||
document.addEventListener('keydown', handleEscape)
|
||
return () => document.removeEventListener('keydown', handleEscape)
|
||
}, [isOpen, closeOnEscape, onClose])
|
||
|
||
// 阻止body滚动
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
document.body.style.overflow = 'hidden'
|
||
} else {
|
||
document.body.style.overflow = ''
|
||
}
|
||
return () => {
|
||
document.body.style.overflow = ''
|
||
}
|
||
}, [isOpen])
|
||
|
||
if (!isOpen) return null
|
||
|
||
const handleOverlayClick = (e: React.MouseEvent) => {
|
||
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
||
onClose()
|
||
}
|
||
}
|
||
|
||
const sizeClass = styles.modalStyles.sizes[size]
|
||
|
||
return (
|
||
<div
|
||
className={`${styles.modalStyles.overlay} ${className}`}
|
||
onClick={handleOverlayClick}
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby={title ? 'modal-title' : undefined}
|
||
>
|
||
<div
|
||
ref={modalRef}
|
||
className={`${styles.modalStyles.container} ${sizeClass}`}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className={styles.modalStyles.content}>
|
||
{/* 头部 */}
|
||
{(title || showCloseButton) && (
|
||
<div className={styles.modalStyles.header}>
|
||
{title && (
|
||
<h3 id="modal-title" className={styles.modalStyles.title}>
|
||
{title}
|
||
</h3>
|
||
)}
|
||
{showCloseButton && (
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className={styles.modalStyles.closeButton}
|
||
aria-label="关闭"
|
||
>
|
||
<svg
|
||
className="w-5 h-5"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 主体 */}
|
||
<div className={styles.modalStyles.body}>{children}</div>
|
||
|
||
{/* 底部 */}
|
||
{footer && <div className={styles.modalStyles.footer}>{footer}</div>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|