162 lines
4.5 KiB
TypeScript
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 };
|