Your Name f166c04422 Add frontend component library and UI development tasks
- 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>
2026-02-03 17:44:22 +08:00

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;