claude-code-cchui/src/components/ui/Modal.tsx
gaoziman cb01e2dffb feat(UI): 添加模态框、图片灯箱和文档预览组件
Modal 组件:
- 支持 ESC 键关闭和点击遮罩关闭
- 可配置关闭按钮显示、最大宽度、全屏模式
- 添加淡入和缩放动画效果

ImageLightbox 组件:
- 支持多图片浏览和左右键导航
- 实现缩放、下载功能
- 支持触摸手势滑动切换
- 底部指示点和图片计数显示

DocumentPreview 组件:
- 支持代码文件语法高亮显示
- Markdown 文件渲染预览
- 提供复制和下载功能
2025-12-20 12:13:15 +08:00

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;
}
`;