Some checks failed
CI Check / check (push) Has been cancelled
- 创建统一UI三层架构系统(基础层、颜色层、组合层) - 拆分Admin页面为独立Tab组件(MonitorTab、UsersTab、DepartmentsTab等) - 提取公共逻辑到Hooks(useToast、useDataFetch、useUserOperations等) - 提取工具函数(validators、formatters、api等) - 创建统一弹窗组件(Modal)和useModal Hook - 添加错误边界和统一状态组件 - 完善类型定义和可访问性支持 - 更新文档(优化记录、优化计划、更新日志)
130 lines
3.2 KiB
TypeScript
130 lines
3.2 KiB
TypeScript
'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>
|
|
)
|
|
}
|
|
|