mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-21 15:11:08 +08:00
Refactor: Remove ant design component (#13143)
### What problem does this PR solve? _Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._ ### Type of change - [x] Refactoring
This commit is contained in:
470
web/src/components/ui/date-picker.tsx
Normal file
470
web/src/components/ui/date-picker.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
import { Calendar } from '@/components/originui/calendar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Locale } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Calendar as CalendarIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
ChangeEvent,
|
||||
forwardRef,
|
||||
InputHTMLAttributes,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from './button';
|
||||
import { TimePicker } from './time-picker';
|
||||
|
||||
type PickerType = 'date' | 'month' | 'year';
|
||||
|
||||
interface DatePickerProps extends Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'value' | 'onChange'
|
||||
> {
|
||||
value?: Date | number;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
showTimeSelect?: boolean;
|
||||
dateFormat?: string;
|
||||
timeFormat?: string;
|
||||
showTimeSelectOnly?: boolean;
|
||||
locale?: Locale;
|
||||
openChange?: (open: boolean) => void;
|
||||
picker?: PickerType;
|
||||
allowInput?: boolean;
|
||||
minYear?: number;
|
||||
maxYear?: number;
|
||||
}
|
||||
|
||||
const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
dateFormat = 'DD/MM/YYYY',
|
||||
timeFormat = 'HH:mm:ss',
|
||||
showTimeSelect = false,
|
||||
showTimeSelectOnly = false,
|
||||
openChange,
|
||||
picker = 'date',
|
||||
allowInput = false,
|
||||
minYear = 1900,
|
||||
maxYear = 2100,
|
||||
placeholder,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [currentYear, setCurrentYear] = useState(() => {
|
||||
const year = value
|
||||
? value instanceof Date
|
||||
? value.getFullYear()
|
||||
: value
|
||||
: new Date().getFullYear();
|
||||
return year;
|
||||
});
|
||||
const [currentMonth, setCurrentMonth] = useState(() => {
|
||||
if (value instanceof Date) {
|
||||
return value.getMonth();
|
||||
}
|
||||
return new Date().getMonth();
|
||||
});
|
||||
|
||||
const selectedDate = useMemo(() => {
|
||||
if (!value) return undefined;
|
||||
if (value instanceof Date) return value;
|
||||
return new Date(value, 0, 1);
|
||||
}, [value]);
|
||||
|
||||
const displayFormat = useMemo(() => {
|
||||
if (picker === 'year') return 'YYYY';
|
||||
if (picker === 'month') return 'MM/YYYY';
|
||||
if (showTimeSelect) return `${dateFormat} ${timeFormat}`;
|
||||
if (showTimeSelectOnly) return timeFormat;
|
||||
return dateFormat;
|
||||
}, [picker, dateFormat, timeFormat, showTimeSelect, showTimeSelectOnly]);
|
||||
|
||||
const formattedValue = useMemo(() => {
|
||||
if (selectedDate && !isNaN(selectedDate.getTime())) {
|
||||
return dayjs(selectedDate).format(displayFormat);
|
||||
}
|
||||
return inputValue || '';
|
||||
}, [selectedDate, displayFormat, inputValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate && !isNaN(selectedDate.getTime())) {
|
||||
setInputValue(dayjs(selectedDate).format(displayFormat));
|
||||
}
|
||||
}, [selectedDate, displayFormat]);
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (date && selectedDate) {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
date.setHours(valueDate.hour());
|
||||
date.setMinutes(valueDate.minute());
|
||||
date.setSeconds(valueDate.second());
|
||||
}
|
||||
onChange?.(date);
|
||||
};
|
||||
|
||||
const handleTimeSelect = (date: Date | undefined) => {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
if (selectedDate) {
|
||||
date?.setFullYear(valueDate.year());
|
||||
date?.setMonth(valueDate.month());
|
||||
date?.setDate(valueDate.date());
|
||||
}
|
||||
if (date) {
|
||||
onChange?.(date);
|
||||
} else {
|
||||
valueDate?.hour(0);
|
||||
valueDate?.minute(0);
|
||||
valueDate?.second(0);
|
||||
onChange?.(valueDate.toDate());
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
|
||||
if (allowInput) {
|
||||
const parsed = dayjs(newValue, displayFormat, true);
|
||||
if (parsed.isValid()) {
|
||||
onChange?.(parsed.toDate());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
openChange?.(open);
|
||||
};
|
||||
|
||||
const handleYearSelect = (year: number) => {
|
||||
if (picker === 'year') {
|
||||
onChange?.(new Date(year, 0, 1));
|
||||
setOpen(false);
|
||||
} else {
|
||||
setCurrentYear(year);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthSelect = (month: number) => {
|
||||
if (picker === 'month') {
|
||||
onChange?.(new Date(currentYear, month, 1));
|
||||
setOpen(false);
|
||||
} else {
|
||||
setCurrentMonth(month);
|
||||
}
|
||||
};
|
||||
|
||||
const years = useMemo(() => {
|
||||
const result: number[] = [];
|
||||
const startYear = Math.floor(currentYear / 10) * 10 - 1;
|
||||
for (let i = startYear; i <= startYear + 12; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
}, [currentYear]);
|
||||
|
||||
const months = useMemo(() => {
|
||||
return [
|
||||
{ value: 0, label: t('common.january', 'Jan') },
|
||||
{ value: 1, label: t('common.february', 'Feb') },
|
||||
{ value: 2, label: t('common.march', 'Mar') },
|
||||
{ value: 3, label: t('common.april', 'Apr') },
|
||||
{ value: 4, label: t('common.may', 'May') },
|
||||
{ value: 5, label: t('common.june', 'Jun') },
|
||||
{ value: 6, label: t('common.july', 'Jul') },
|
||||
{ value: 7, label: t('common.august', 'Aug') },
|
||||
{ value: 8, label: t('common.september', 'Sep') },
|
||||
{ value: 9, label: t('common.october', 'Oct') },
|
||||
{ value: 10, label: t('common.november', 'Nov') },
|
||||
{ value: 11, label: t('common.december', 'Dec') },
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
const [view, setView] = useState<'date' | 'month' | 'year'>(
|
||||
picker === 'year' ? 'year' : picker === 'month' ? 'month' : 'date',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setView(
|
||||
picker === 'year' ? 'year' : picker === 'month' ? 'month' : 'date',
|
||||
);
|
||||
}, [picker]);
|
||||
|
||||
const handlePrev = () => {
|
||||
if (view === 'year') {
|
||||
setCurrentYear((prev) => prev - 10);
|
||||
} else if (view === 'month') {
|
||||
setCurrentYear((prev) => prev - 1);
|
||||
} else {
|
||||
const newMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
||||
const newYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
||||
setCurrentMonth(newMonth);
|
||||
setCurrentYear(newYear);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (view === 'year') {
|
||||
setCurrentYear((prev) => prev + 10);
|
||||
} else if (view === 'month') {
|
||||
setCurrentYear((prev) => prev + 1);
|
||||
} else {
|
||||
const newMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
||||
const newYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
||||
setCurrentMonth(newMonth);
|
||||
setCurrentYear(newYear);
|
||||
}
|
||||
};
|
||||
|
||||
const renderYearPicker = () => (
|
||||
<div className="p-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<span className="text-sm font-medium">
|
||||
{years[1]} - {years[10]}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{years.map((year) => {
|
||||
const isSelected = year === (selectedDate?.getFullYear() || 0);
|
||||
const isCurrentDecade = year >= years[1] && year <= years[10];
|
||||
const isDisabled = year < minYear || year > maxYear;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={year}
|
||||
variant={isSelected ? 'default' : 'ghost'}
|
||||
size="lg"
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
'text-sm ',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isCurrentDecade && 'text-muted-foreground opacity-50',
|
||||
!isSelected && isCurrentDecade && 'hover:bg-accent',
|
||||
)}
|
||||
onClick={() => handleYearSelect(year)}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMonthPicker = () => (
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<span
|
||||
className="text-sm font-medium cursor-pointer hover:text-primary"
|
||||
onClick={() => picker !== 'month' && setView('year')}
|
||||
>
|
||||
{currentYear}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{months.map((month) => {
|
||||
const isSelected =
|
||||
month.value === (selectedDate?.getMonth() ?? -1) &&
|
||||
currentYear === (selectedDate?.getFullYear() ?? 0);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={month.value}
|
||||
variant={isSelected ? 'default' : 'ghost'}
|
||||
size="lg"
|
||||
className={cn(
|
||||
'text-sm',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
)}
|
||||
onClick={() => handleMonthSelect(month.value)}
|
||||
>
|
||||
{month.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderDatePicker = () => (
|
||||
<>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={handleDateSelect}
|
||||
month={new Date(currentYear, currentMonth)}
|
||||
onMonthChange={(date) => {
|
||||
setCurrentYear(date.getFullYear());
|
||||
setCurrentMonth(date.getMonth());
|
||||
}}
|
||||
onDayClick={() => {
|
||||
if (!showTimeSelect) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
month_caption:
|
||||
'relative mx-10 mb-1 flex h-9 items-center justify-center z-20 [&>span]:cursor-pointer',
|
||||
}}
|
||||
components={{
|
||||
Chevron: (props) => {
|
||||
if (props.orientation === 'left') {
|
||||
return (
|
||||
<ChevronLeftIcon
|
||||
size={16}
|
||||
{...props}
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePrev();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
size={16}
|
||||
{...props}
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleNext();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/* <div
|
||||
className="text-center py-1 text-sm cursor-pointer hover:text-primary"
|
||||
onClick={() => setView('month')}
|
||||
>
|
||||
{dayjs(new Date(currentYear, currentMonth)).format('MMMM YYYY')}
|
||||
</div> */}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderPicker = () => {
|
||||
if (view === 'year') return renderYearPicker();
|
||||
if (view === 'month') return renderMonthPicker();
|
||||
return renderDatePicker();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={ref}
|
||||
value={formattedValue}
|
||||
onChange={handleInputChange}
|
||||
readOnly={!allowInput}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'bg-bg-card hover:text-text-primary border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]',
|
||||
allowInput ? 'cursor-text' : 'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<CalendarIcon
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors pointer-events-none"
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-2" align="start">
|
||||
{renderPicker()}
|
||||
{showTimeSelect && picker === 'date' && (
|
||||
<TimePicker
|
||||
value={selectedDate}
|
||||
onChange={(value: Date | undefined) => {
|
||||
handleTimeSelect(value);
|
||||
}}
|
||||
showNow
|
||||
/>
|
||||
)}
|
||||
<div className="w-full flex justify-end mt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="text-sm mr-2"
|
||||
onClick={() => {
|
||||
onChange?.(undefined);
|
||||
setInputValue('');
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.clear', 'Clear')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="text-sm text-text-primary-inverse"
|
||||
onClick={() => {
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.confirm', 'OK')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DatePicker.displayName = 'DatePicker';
|
||||
|
||||
export { DatePicker };
|
||||
@@ -1,175 +0,0 @@
|
||||
import { Calendar } from '@/components/originui/calendar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Locale } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import { Calendar as CalendarIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from './button';
|
||||
import { TimePicker } from './time-picker';
|
||||
// import TimePicker from 'react-time-picker';
|
||||
interface DateInputProps extends Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'value' | 'onChange'
|
||||
> {
|
||||
value?: Date;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
showTimeSelect?: boolean;
|
||||
dateFormat?: string;
|
||||
timeFormat?: string;
|
||||
showTimeSelectOnly?: boolean;
|
||||
locale?: Locale; // Support for internationalization
|
||||
openChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
dateFormat = 'DD/MM/YYYY',
|
||||
timeFormat = 'HH:mm:ss',
|
||||
showTimeSelect = false,
|
||||
showTimeSelectOnly = false,
|
||||
openChange,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(
|
||||
value,
|
||||
);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
date?.setHours(valueDate.hour());
|
||||
date?.setMinutes(valueDate.minute());
|
||||
date?.setSeconds(valueDate.second());
|
||||
}
|
||||
setSelectedDate(date);
|
||||
// onChange?.(date);
|
||||
};
|
||||
|
||||
const handleTimeSelect = (date: Date | undefined) => {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
if (selectedDate) {
|
||||
date?.setFullYear(valueDate.year());
|
||||
date?.setMonth(valueDate.month());
|
||||
date?.setDate(valueDate.date());
|
||||
}
|
||||
if (date) {
|
||||
// onChange?.(date);
|
||||
setSelectedDate(date);
|
||||
} else {
|
||||
valueDate?.hour(0);
|
||||
valueDate?.minute(0);
|
||||
valueDate?.second(0);
|
||||
// onChange?.(valueDate.toDate());
|
||||
setSelectedDate(valueDate.toDate());
|
||||
}
|
||||
};
|
||||
|
||||
// Determine display format based on the type of date picker
|
||||
let displayFormat = dateFormat;
|
||||
if (showTimeSelect) {
|
||||
displayFormat = `${dateFormat} ${timeFormat}`;
|
||||
} else if (showTimeSelectOnly) {
|
||||
displayFormat = timeFormat;
|
||||
}
|
||||
|
||||
// Format the date according to the specified format
|
||||
const formattedValue = React.useMemo(() => {
|
||||
return selectedDate && !isNaN(selectedDate.getTime())
|
||||
? dayjs(selectedDate).format(displayFormat)
|
||||
: '';
|
||||
}, [selectedDate, displayFormat]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
openChange?.(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
disableOutsideClick
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={ref}
|
||||
value={formattedValue}
|
||||
readOnly
|
||||
className={cn(
|
||||
'bg-bg-card hover:text-text-primary border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<CalendarIcon
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors"
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-2" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={handleDateSelect}
|
||||
/>
|
||||
{showTimeSelect && (
|
||||
<TimePicker
|
||||
value={selectedDate}
|
||||
onChange={(value: Date | undefined) => {
|
||||
handleTimeSelect(value);
|
||||
}}
|
||||
showNow
|
||||
/>
|
||||
// <TimePicker onChange={onChange} value={value} />
|
||||
)}
|
||||
<div className="w-full flex justify-end mt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="text-sm mr-2"
|
||||
onClick={() => {
|
||||
onChange?.(value);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="text-sm text-text-primary-inverse "
|
||||
onClick={() => {
|
||||
onChange?.(selectedDate);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DateInput.displayName = 'DateInput';
|
||||
|
||||
export { DateInput };
|
||||
@@ -1,8 +1,9 @@
|
||||
// src/components/ui/modal.tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Loader, X } from 'lucide-react';
|
||||
import { AlertCircle, CheckCircle, Info, Loader, X } from 'lucide-react';
|
||||
import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DialogDescription } from '../dialog';
|
||||
import { createPortalModal } from './modal-manage';
|
||||
@@ -12,13 +13,15 @@ export interface ModalProps {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
title?: ReactNode;
|
||||
titleClassName?: string;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
content?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
footerClassName?: string;
|
||||
showfooter?: boolean;
|
||||
className?: string;
|
||||
size?: 'small' | 'default' | 'large';
|
||||
closable?: boolean;
|
||||
showCancel?: boolean;
|
||||
closeIcon?: ReactNode;
|
||||
maskClosable?: boolean;
|
||||
destroyOnClose?: boolean;
|
||||
@@ -33,25 +36,42 @@ export interface ModalProps {
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
zIndex?: number;
|
||||
type?: 'warning' | 'info' | 'success' | 'error' | 'confirm';
|
||||
}
|
||||
|
||||
export interface ModalType extends FC<ModalProps> {
|
||||
show: typeof modalIns.show;
|
||||
hide: typeof modalIns.hide;
|
||||
destroy: typeof modalIns.destroy;
|
||||
warning: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
info: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
success: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
error: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
confirm: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
}
|
||||
|
||||
const typeIcons = {
|
||||
warning: <AlertCircle className="w-6 h-6 text-yellow-500" />,
|
||||
info: <Info className="w-6 h-6 text-blue-500" />,
|
||||
success: <CheckCircle className="w-6 h-6 text-green-500" />,
|
||||
error: <AlertCircle className="w-6 h-6 text-red-500" />,
|
||||
confirm: <AlertCircle className="w-6 h-6 text-yellow-500" />,
|
||||
};
|
||||
|
||||
const Modal: ModalType = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
titleClassName,
|
||||
children,
|
||||
content,
|
||||
footer,
|
||||
footerClassName,
|
||||
showfooter = true,
|
||||
className = '',
|
||||
size = 'default',
|
||||
closable = true,
|
||||
showCancel = true,
|
||||
closeIcon = <X className="w-4 h-4" />,
|
||||
maskClosable = true,
|
||||
destroyOnClose = false,
|
||||
@@ -66,6 +86,7 @@ const Modal: ModalType = ({
|
||||
disabled = false,
|
||||
style,
|
||||
zIndex = 50,
|
||||
type,
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
small: 'max-w-md',
|
||||
@@ -74,7 +95,6 @@ const Modal: ModalType = ({
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
// Handle ESC key close
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && maskClosable) {
|
||||
@@ -99,7 +119,6 @@ const Modal: ModalType = ({
|
||||
return;
|
||||
}
|
||||
onOpenChange?.(open);
|
||||
console.log('open', open, onOpenChange);
|
||||
if (open && !disabled) {
|
||||
onOk?.();
|
||||
}
|
||||
@@ -117,16 +136,18 @@ const Modal: ModalType = ({
|
||||
} else {
|
||||
footerTemp = (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCancel()}
|
||||
className={cn(
|
||||
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary ',
|
||||
cancelButtonClassName,
|
||||
)}
|
||||
>
|
||||
{cancelText ?? t('modal.cancelText')}
|
||||
</button>
|
||||
{showCancel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCancel()}
|
||||
className={cn(
|
||||
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary ',
|
||||
cancelButtonClassName,
|
||||
)}
|
||||
>
|
||||
{cancelText ?? t('modal.cancelText')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={confirmLoading || disabled}
|
||||
@@ -168,7 +189,28 @@ const Modal: ModalType = ({
|
||||
footerClassName,
|
||||
okButtonClassName,
|
||||
cancelButtonClassName,
|
||||
showCancel,
|
||||
]);
|
||||
|
||||
const contentEl = useMemo(() => {
|
||||
if (type && typeIcons[type]) {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">{typeIcons[type]}</div>
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<DialogPrimitive.Title className="text-lg font-medium text-foreground mb-2">
|
||||
{title}
|
||||
</DialogPrimitive.Title>
|
||||
)}
|
||||
<div className="text-text-secondary">{content || children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
}, [type, title, content, children]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={handleChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
@@ -180,22 +222,16 @@ const Modal: ModalType = ({
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`,
|
||||
{ 'pt-10': closable && !title },
|
||||
{ 'pt-10': closable && !title && !type },
|
||||
)}
|
||||
style={style}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DialogDescription></DialogDescription>
|
||||
{/* title */}
|
||||
{title && (
|
||||
{title && !type && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start px-6 py-4 justify-start',
|
||||
// {
|
||||
// 'justify-end': closable && !title,
|
||||
// 'justify-between': closable && title,
|
||||
// 'justify-start': !closable,
|
||||
// },
|
||||
titleClassName,
|
||||
)}
|
||||
>
|
||||
@@ -218,12 +254,10 @@ const Modal: ModalType = ({
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
|
||||
{/* content */}
|
||||
<div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[calc(100vh-280px)] focus-visible:!outline-none">
|
||||
{destroyOnClose && !open ? null : children}
|
||||
{destroyOnClose && !open ? null : contentEl}
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
{footEl}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Overlay>
|
||||
@@ -242,4 +276,49 @@ Modal.show = modalIns
|
||||
Modal.hide = modalIns.hide;
|
||||
Modal.destroy = modalIns.destroy;
|
||||
|
||||
const createStaticModal = (type: ModalProps['type']) => {
|
||||
return (props: Omit<ModalProps, 'open' | 'type'>) => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
const destroy = () => {
|
||||
root.unmount();
|
||||
container.remove();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
props.onOk?.();
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.onCancel?.();
|
||||
destroy();
|
||||
};
|
||||
|
||||
root.render(
|
||||
<Modal
|
||||
{...props}
|
||||
open={true}
|
||||
type={type}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
maskClosable={false}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Modal.warning = createStaticModal('warning');
|
||||
Modal.info = createStaticModal('info');
|
||||
Modal.success = createStaticModal('success');
|
||||
Modal.error = createStaticModal('error');
|
||||
Modal.confirm = createStaticModal('confirm');
|
||||
|
||||
export { Modal };
|
||||
|
||||
Reference in New Issue
Block a user