Files
ragflow/web/src/components/ui/modal.tsx
chanx e101c35c0b Fix:Add a component of calendar (#8923)
### What problem does this PR solve?

fix:Add a component of calendar
[#3221](https://github.com/infiniflow/ragflow/issues/3221)

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-07-21 10:26:41 +08:00

218 lines
6.0 KiB
TypeScript

// 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;
showfooter?: boolean;
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,
showfooter = true,
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(() => {
if (showfooter === false) {
return <></>;
}
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,
showfooter,
]);
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 focus-visible:!outline-none`}
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>
)}
{/* title */}
{!title && (
<DialogPrimitive.Title className="text-lg font-medium text-foreground"></DialogPrimitive.Title>
)}
{/* content */}
<div className="p-6 overflow-y-auto max-h-[80vh] focus-visible:!outline-none">
{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>
);
}
*/