flnxi-Wu/components/ui/Modal.tsx
WuYongJie e4dcb6260a
Some checks failed
CI Check / check (push) Has been cancelled
feat: Admin页面重构 & UI三层架构系统
- 创建统一UI三层架构系统(基础层、颜色层、组合层)
- 拆分Admin页面为独立Tab组件(MonitorTab、UsersTab、DepartmentsTab等)
- 提取公共逻辑到Hooks(useToast、useDataFetch、useUserOperations等)
- 提取工具函数(validators、formatters、api等)
- 创建统一弹窗组件(Modal)和useModal Hook
- 添加错误边界和统一状态组件
- 完善类型定义和可访问性支持
- 更新文档(优化记录、优化计划、更新日志)
2025-11-22 15:17:10 +08:00

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