Modal 组件: - 支持 ESC 键关闭和点击遮罩关闭 - 可配置关闭按钮显示、最大宽度、全屏模式 - 添加淡入和缩放动画效果 ImageLightbox 组件: - 支持多图片浏览和左右键导航 - 实现缩放、下载功能 - 支持触摸手势滑动切换 - 底部指示点和图片计数显示 DocumentPreview 组件: - 支持代码文件语法高亮显示 - Markdown 文件渲染预览 - 提供复制和下载功能
147 lines
3.3 KiB
TypeScript
147 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useCallback, useRef } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
children: React.ReactNode;
|
|
/** 是否显示关闭按钮 */
|
|
showCloseButton?: boolean;
|
|
/** 点击遮罩是否关闭 */
|
|
closeOnBackdrop?: boolean;
|
|
/** 按 ESC 是否关闭 */
|
|
closeOnEsc?: boolean;
|
|
/** 自定义类名 */
|
|
className?: string;
|
|
/** 遮罩类名 */
|
|
backdropClassName?: string;
|
|
/** 内容区域最大宽度 */
|
|
maxWidth?: string;
|
|
/** 是否全屏 */
|
|
fullScreen?: boolean;
|
|
}
|
|
|
|
export function Modal({
|
|
isOpen,
|
|
onClose,
|
|
children,
|
|
showCloseButton = true,
|
|
closeOnBackdrop = true,
|
|
closeOnEsc = true,
|
|
className,
|
|
backdropClassName,
|
|
maxWidth = '800px',
|
|
fullScreen = false,
|
|
}: ModalProps) {
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
|
|
// ESC 关闭
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (closeOnEsc && e.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
},
|
|
[closeOnEsc, onClose]
|
|
);
|
|
|
|
// 绑定键盘事件
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
// 禁止背景滚动
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
document.body.style.overflow = '';
|
|
};
|
|
}, [isOpen, handleKeyDown]);
|
|
|
|
// 点击遮罩关闭
|
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
if (closeOnBackdrop && e.target === e.currentTarget) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'fixed inset-0 z-50 flex items-center justify-center',
|
|
'bg-black/60 backdrop-blur-sm',
|
|
'animate-fade-in-fast',
|
|
backdropClassName
|
|
)}
|
|
onClick={handleBackdropClick}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
{/* 内容区域 */}
|
|
<div
|
|
ref={contentRef}
|
|
className={cn(
|
|
'relative bg-[var(--color-bg-primary)] rounded-xl shadow-2xl',
|
|
'animate-scale-in-fast',
|
|
fullScreen ? 'w-full h-full rounded-none' : 'max-h-[90vh]',
|
|
className
|
|
)}
|
|
style={!fullScreen ? { maxWidth } : undefined}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* 关闭按钮 */}
|
|
{showCloseButton && (
|
|
<button
|
|
onClick={onClose}
|
|
className={cn(
|
|
'absolute top-3 right-3 z-10',
|
|
'w-8 h-8 flex items-center justify-center rounded-full',
|
|
'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]',
|
|
'hover:bg-[var(--color-bg-hover)] transition-colors duration-150'
|
|
)}
|
|
aria-label="关闭"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
)}
|
|
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 添加动画样式到 globals.css
|
|
// 这里导出需要添加的 CSS
|
|
export const modalAnimationStyles = `
|
|
@keyframes fadeInFast {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes scaleInFast {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
.animate-fade-in-fast {
|
|
animation: fadeInFast 0.15s ease-out;
|
|
}
|
|
|
|
.animate-scale-in-fast {
|
|
animation: scaleInFast 0.15s ease-out;
|
|
}
|
|
`;
|