Files
notify/frontend/src/components/ui/datetime-picker.tsx
Michael Dong a98e12f286 first commit
2026-02-05 11:24:40 +08:00

162 lines
4.5 KiB
TypeScript

"use client";
import * as React from "react";
import { CalendarIcon } from "lucide-react";
import { format, parse, isValid } from "date-fns";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
interface DateTimePickerProps {
value?: Date;
onChange?: (date: Date | undefined) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}
function DateTimePicker({
value,
onChange,
placeholder = "选择日期和时间",
disabled = false,
className,
}: DateTimePickerProps) {
const [open, setOpen] = React.useState(false);
// 获取初始默认时间
const getDefaultDate = React.useCallback(() => {
const now = new Date();
now.setSeconds(0);
now.setMilliseconds(0);
return now;
}, []);
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(() => {
return value ?? getDefaultDate();
});
const [inputValue, setInputValue] = React.useState(() => {
return format(value ?? getDefaultDate(), "yyyy-MM-dd HH:mm");
});
// 组件挂载时,如果没有外部值,设置当前时间为默认值
const initialized = React.useRef(false);
React.useEffect(() => {
if (!initialized.current && !value && onChange) {
initialized.current = true;
const defaultDate = getDefaultDate();
onChange(defaultDate);
}
}, [value, onChange, getDefaultDate]);
// Sync with external value
React.useEffect(() => {
if (value) {
setSelectedDate(value);
setInputValue(format(value, "yyyy-MM-dd HH:mm"));
}
}, [value]);
const handleDateSelect = (date: Date | undefined) => {
setSelectedDate(date);
if (date && onChange) {
const newDate = new Date(date);
// 如果已有时间值,保留原来的时分;否则使用当前时间
if (value) {
newDate.setHours(value.getHours());
newDate.setMinutes(value.getMinutes());
} else {
const now = new Date();
newDate.setHours(now.getHours());
newDate.setMinutes(now.getMinutes());
}
newDate.setSeconds(0);
newDate.setMilliseconds(0);
onChange(newDate);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInputValue(newValue);
};
const handleInputBlur = () => {
if (!inputValue.trim()) {
onChange?.(undefined);
setSelectedDate(undefined);
return;
}
// Try to parse the input value with time
const parsed = parse(inputValue, "yyyy-MM-dd HH:mm", new Date());
if (isValid(parsed)) {
setSelectedDate(parsed);
parsed.setSeconds(0);
parsed.setMilliseconds(0);
onChange?.(parsed);
} else {
// If invalid, reset to the current value
if (value) {
setInputValue(format(value, "yyyy-MM-dd HH:mm"));
} else {
setInputValue("");
}
}
};
return (
<div className={cn("relative flex items-center", className)}>
<Input
type="text"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
placeholder={placeholder}
disabled={disabled}
className="pr-10"
/>
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
disabled={disabled}
className="absolute right-0 h-full px-3 hover:bg-transparent"
>
<CalendarIcon className="h-4 w-4 text-slate-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="end"
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div
onClick={(e) => {
// 阻止点击事件冒泡到 Dialog
e.stopPropagation();
}}
onPointerDown={(e) => {
// 阻止指针按下事件冒泡
e.stopPropagation();
}}
>
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
/>
</div>
</PopoverContent>
</Popover>
</div>
);
}
export { DateTimePicker };