flnxi-Wu/components/ui/Modal.tsx
2025-11-22 17:56:24 +08:00

142 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
)
}