first commit
This commit is contained in:
161
frontend/src/components/ui/datetime-picker.tsx
Normal file
161
frontend/src/components/ui/datetime-picker.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user