| 
                        123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 | 
                        - // src/components/ui/modal.tsx
 - import * as DialogPrimitive from '@radix-ui/react-dialog';
 - import { Loader, X } from 'lucide-react';
 - import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
 - import { useTranslation } from 'react-i18next';
 - 
 - interface ModalProps {
 -   open: boolean;
 -   onOpenChange?: (open: boolean) => void;
 -   title?: ReactNode;
 -   children: ReactNode;
 -   footer?: ReactNode;
 -   className?: string;
 -   size?: 'small' | 'default' | 'large';
 -   closable?: boolean;
 -   closeIcon?: ReactNode;
 -   maskClosable?: boolean;
 -   destroyOnClose?: boolean;
 -   full?: boolean;
 -   confirmLoading?: boolean;
 -   cancelText?: ReactNode | string;
 -   okText?: ReactNode | string;
 -   onOk?: () => void;
 -   onCancel?: () => void;
 - }
 - 
 - export const Modal: FC<ModalProps> = ({
 -   open,
 -   onOpenChange,
 -   title,
 -   children,
 -   footer,
 -   className = '',
 -   size = 'default',
 -   closable = true,
 -   closeIcon = <X className="w-4 h-4" />,
 -   maskClosable = true,
 -   destroyOnClose = false,
 -   full = false,
 -   onOk,
 -   onCancel,
 -   confirmLoading,
 -   cancelText,
 -   okText,
 - }) => {
 -   const sizeClasses = {
 -     small: 'max-w-md',
 -     default: 'max-w-2xl',
 -     large: 'max-w-4xl',
 -   };
 - 
 -   const { t } = useTranslation();
 -   // Handle ESC key close
 -   useEffect(() => {
 -     const handleKeyDown = (e: KeyboardEvent) => {
 -       if (e.key === 'Escape' && maskClosable) {
 -         onOpenChange?.(false);
 -       }
 -     };
 -     window.addEventListener('keydown', handleKeyDown);
 -     return () => window.removeEventListener('keydown', handleKeyDown);
 -   }, [maskClosable, onOpenChange]);
 - 
 -   const handleCancel = useCallback(() => {
 -     onOpenChange?.(false);
 -     onCancel?.();
 -   }, [onOpenChange, onCancel]);
 - 
 -   const handleOk = useCallback(() => {
 -     onOpenChange?.(true);
 -     onOk?.();
 -   }, [onOpenChange, onOk]);
 -   const handleChange = (open: boolean) => {
 -     onOpenChange?.(open);
 -     if (open) {
 -       handleOk();
 -     }
 -     if (!open) {
 -       handleCancel();
 -     }
 -   };
 -   const footEl = useMemo(() => {
 -     let footerTemp;
 -     if (footer) {
 -       footerTemp = footer;
 -     } else {
 -       footerTemp = (
 -         <div className="flex justify-end gap-2">
 -           <button
 -             type="button"
 -             onClick={() => handleCancel()}
 -             className="px-2 py-1 border border-input rounded-md hover:bg-muted"
 -           >
 -             {cancelText ?? t('modal.cancelText')}
 -           </button>
 -           <button
 -             type="button"
 -             disabled={confirmLoading}
 -             onClick={() => handleOk()}
 -             className="px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
 -           >
 -             {confirmLoading && (
 -               <Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
 -             )}
 -             {okText ?? t('modal.okText')}
 -           </button>
 -         </div>
 -       );
 -     }
 -     return (
 -       <div className="flex items-center justify-end border-t border-border px-6 py-4">
 -         {footerTemp}
 -       </div>
 -     );
 -   }, [footer, cancelText, t, confirmLoading, okText, handleCancel, handleOk]);
 -   return (
 -     <DialogPrimitive.Root open={open} onOpenChange={handleChange}>
 -       <DialogPrimitive.Portal>
 -         <DialogPrimitive.Overlay
 -           className="fixed inset-0 z-50 bg-colors-background-neutral-weak/50 backdrop-blur-sm flex items-center justify-center p-4"
 -           onClick={() => maskClosable && onOpenChange?.(false)}
 -         >
 -           <DialogPrimitive.Content
 -             className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg transition-all`}
 -             onClick={(e) => e.stopPropagation()}
 -           >
 -             {/* title */}
 -             {title && (
 -               <div className="flex items-center justify-between border-b border-border px-6 py-4">
 -                 <DialogPrimitive.Title className="text-lg font-medium text-foreground">
 -                   {title}
 -                 </DialogPrimitive.Title>
 -                 {closable && (
 -                   <DialogPrimitive.Close asChild>
 -                     <button
 -                       type="button"
 -                       className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted"
 -                     >
 -                       {closeIcon}
 -                     </button>
 -                   </DialogPrimitive.Close>
 -                 )}
 -               </div>
 -             )}
 - 
 -             {/* content */}
 -             <div className="p-6 overflow-y-auto max-h-[80vh]">
 -               {destroyOnClose && !open ? null : children}
 -             </div>
 - 
 -             {/* footer */}
 -             {footEl}
 -           </DialogPrimitive.Content>
 -         </DialogPrimitive.Overlay>
 -       </DialogPrimitive.Portal>
 -     </DialogPrimitive.Root>
 -   );
 - };
 - 
 - // example usage
 - /*
 - import { Modal } from '@/components/ui/modal';
 - 
 - function Demo() {
 -   const [open, setOpen] = useState(false);
 - 
 -   return (
 -     <div>
 -       <button onClick={() => setOpen(true)}>open modal</button>
 -       
 -       <Modal
 -         open={open}
 -         onOpenChange={setOpen}
 -         title="title"
 -         footer={
 -           <div className="flex gap-2">
 -             <button onClick={() => setOpen(false)} className="px-4 py-2 border rounded-md">
 -               cancel
 -             </button>
 -             <button onClick={() => setOpen(false)} className="px-4 py-2 bg-primary text-white rounded-md">
 -               ok
 -             </button>
 -           </div>
 -         }
 -       >
 -         <div className="py-4">弹窗内容区域</div>
 -       </Modal>
 -       <Modal
 -         title={'modal-title'}
 -         onOk={handleOk}
 -         confirmLoading={loading}
 -         destroyOnClose
 -       >
 -         <div className="py-4">弹窗内容区域</div>
 -       </Modal>
 -     </div>
 -   );
 - }
 - */
 
 
  |