- Create Tailwind CSS configuration with design tokens from UIDesignSpec - Create globals.css with CSS variables and component styles - Add React component library: - UI components: Button, Card, Tag, Input, Select, ProgressBar, Modal - Navigation: BottomNav, Sidebar, StatusBar - Layout: MobileLayout, DesktopLayout - Add constants for colors, icons, and layout - Update tasks.md with 31 UI development tasks linked to design node IDs - Configure package.json, tsconfig.json, and postcss.config.js Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
167 lines
3.8 KiB
TypeScript
167 lines
3.8 KiB
TypeScript
/**
|
|
* Modal 弹窗组件
|
|
* 设计稿参考: UIDesignSpec.md
|
|
*/
|
|
import React, { useEffect, useCallback } 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 = '',
|
|
}) => {
|
|
// Handle ESC key
|
|
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]);
|
|
|
|
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}
|
|
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;
|