主要更新: - 更新代理商端文档,明确项目由品牌方分配流程 - 新增Brief配置详情页(已配置)设计稿 - 完善工作台紧急待办中品牌新任务功能 - 整理Pencil设计文件中代理商端页面顺序 - 新增后端FastAPI框架及核心API - 新增前端Next.js页面和组件库 - 添加.gitignore排除构建和缓存文件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
174 lines
4.0 KiB
TypeScript
174 lines
4.0 KiB
TypeScript
/**
|
|
* Modal 弹窗组件
|
|
* 设计稿参考: UIDesignSpec.md
|
|
*/
|
|
'use client';
|
|
import React, { useEffect, useCallback, useRef } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { Button } from './Button';
|
|
|
|
export interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title?: string;
|
|
children: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
closeOnOverlay?: boolean;
|
|
closeOnEsc?: boolean;
|
|
showCloseButton?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const sizeStyles = {
|
|
sm: 'max-w-sm',
|
|
md: 'max-w-md',
|
|
lg: 'max-w-lg',
|
|
xl: 'max-w-xl',
|
|
full: 'max-w-[90vw] max-h-[90vh]',
|
|
};
|
|
|
|
export const Modal: React.FC<ModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
title,
|
|
children,
|
|
footer,
|
|
size = 'md',
|
|
closeOnOverlay = true,
|
|
closeOnEsc = true,
|
|
showCloseButton = true,
|
|
className = '',
|
|
}) => {
|
|
const previousOverflowRef = useRef<string>('');
|
|
|
|
// Handle ESC key
|
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
if (closeOnEsc && e.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
}, [closeOnEsc, onClose]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
previousOverflowRef.current = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
document.body.style.overflow = previousOverflowRef.current || '';
|
|
};
|
|
}, [isOpen, handleKeyDown]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-modal flex items-center justify-center">
|
|
{/* Overlay */}
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-xs"
|
|
onClick={closeOnOverlay ? onClose : undefined}
|
|
/>
|
|
|
|
{/* Modal Content */}
|
|
<div
|
|
className={`
|
|
relative w-full mx-4
|
|
bg-bg-card rounded-card
|
|
shadow-elevated
|
|
animate-scale-in
|
|
${sizeStyles[size]}
|
|
${className}
|
|
`}
|
|
>
|
|
{/* Header */}
|
|
{(title || showCloseButton) && (
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-border-subtle">
|
|
{title && (
|
|
<h2 className="text-section-title text-text-primary font-semibold">
|
|
{title}
|
|
</h2>
|
|
)}
|
|
{showCloseButton && (
|
|
<button
|
|
onClick={onClose}
|
|
type="button"
|
|
aria-label="关闭"
|
|
className="p-1 text-text-tertiary hover:text-text-primary transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Body */}
|
|
<div className="px-5 py-4 max-h-[60vh] overflow-y-auto">
|
|
{children}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{footer && (
|
|
<div className="px-5 py-4 border-t border-border-subtle flex justify-end gap-3">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 确认弹窗
|
|
export interface ConfirmModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onConfirm: () => void;
|
|
title: string;
|
|
message: string;
|
|
confirmText?: string;
|
|
cancelText?: string;
|
|
variant?: 'default' | 'danger';
|
|
loading?: boolean;
|
|
}
|
|
|
|
export const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onConfirm,
|
|
title,
|
|
message,
|
|
confirmText = '确认',
|
|
cancelText = '取消',
|
|
variant = 'default',
|
|
loading = false,
|
|
}) => {
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title={title}
|
|
size="sm"
|
|
footer={
|
|
<>
|
|
<Button variant="secondary" onClick={onClose} disabled={loading}>
|
|
{cancelText}
|
|
</Button>
|
|
<Button
|
|
variant={variant === 'danger' ? 'danger' : 'primary'}
|
|
onClick={onConfirm}
|
|
loading={loading}
|
|
>
|
|
{confirmText}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<p className="text-body text-text-secondary">{message}</p>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default Modal;
|