144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { CalendarDays, ChevronLeft, ChevronRight, Clock3, X } from 'lucide-react';
|
|
|
|
type DateTimeInputProps = {
|
|
label?: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
};
|
|
|
|
const weekDays = ['一', '二', '三', '四', '五', '六', '日'];
|
|
|
|
function formatDate(date: Date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function parseDate(value?: string) {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
|
|
const [year, month, day] = value.split('-').map(Number);
|
|
return new Date(year, month - 1, day);
|
|
}
|
|
|
|
function getMonthLabel(date: Date) {
|
|
return `${date.getFullYear()}年${date.getMonth() + 1}月`;
|
|
}
|
|
|
|
function getCalendarDays(viewDate: Date) {
|
|
const year = viewDate.getFullYear();
|
|
const month = viewDate.getMonth();
|
|
const firstDate = new Date(year, month, 1);
|
|
const firstWeekday = (firstDate.getDay() + 6) % 7;
|
|
const startDate = new Date(year, month, 1 - firstWeekday);
|
|
|
|
return Array.from({ length: 42 }, (_, index) => {
|
|
const date = new Date(startDate);
|
|
date.setDate(startDate.getDate() + index);
|
|
return {
|
|
date,
|
|
dateString: formatDate(date),
|
|
inCurrentMonth: date.getMonth() === month,
|
|
};
|
|
});
|
|
}
|
|
|
|
function splitDateTime(value: string) {
|
|
const [date = '', time = ''] = value.split('T');
|
|
return { date, time };
|
|
}
|
|
|
|
export function DateTimeInput({ label, value, onChange, placeholder = '选择定时发送时间' }: DateTimeInputProps) {
|
|
const { date, time } = splitDateTime(value);
|
|
const [open, setOpen] = useState(false);
|
|
const [viewDate, setViewDate] = useState(() => parseDate(date) ?? new Date());
|
|
const calendarDays = useMemo(() => getCalendarDays(viewDate), [viewDate]);
|
|
|
|
function moveMonth(offset: number) {
|
|
setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1));
|
|
}
|
|
|
|
function updateDate(nextDate: string) {
|
|
onChange(`${nextDate}T${time || '09:00'}`);
|
|
}
|
|
|
|
function updateTime(nextTime: string) {
|
|
onChange(`${date || formatDate(new Date())}T${nextTime}`);
|
|
}
|
|
|
|
return (
|
|
<div className="ui-field ui-date-range-field">
|
|
{label ? <span className="ui-field__label">{label}</span> : null}
|
|
<button
|
|
className={['ui-date-range-trigger', open ? 'ui-date-range-trigger--open' : '', value ? 'ui-date-range-trigger--filled' : ''].filter(Boolean).join(' ')}
|
|
onClick={() => setOpen((current) => !current)}
|
|
type="button"
|
|
>
|
|
<CalendarDays size={16} />
|
|
<span>{value ? `${date} ${time}` : placeholder}</span>
|
|
{value ? (
|
|
<span
|
|
aria-label="清空定时发送时间"
|
|
className="ui-date-range__clear"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onChange('');
|
|
}}
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<X size={14} />
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
{open ? (
|
|
<div className="ui-date-range-popover ui-date-time-popover">
|
|
<div className="ui-calendar">
|
|
<div className="ui-calendar__header">
|
|
<button aria-label="上个月" onClick={() => moveMonth(-1)} type="button">
|
|
<ChevronLeft size={16} />
|
|
</button>
|
|
<strong>{getMonthLabel(viewDate)}</strong>
|
|
<button aria-label="下个月" onClick={() => moveMonth(1)} type="button">
|
|
<ChevronRight size={16} />
|
|
</button>
|
|
</div>
|
|
<div className="ui-calendar__weekdays">
|
|
{weekDays.map((day) => <span key={day}>{day}</span>)}
|
|
</div>
|
|
<div className="ui-calendar__grid">
|
|
{calendarDays.map((item) => (
|
|
<button
|
|
className={[
|
|
'ui-calendar__day',
|
|
item.inCurrentMonth ? '' : 'ui-calendar__day--muted',
|
|
item.dateString === date ? 'ui-calendar__day--selected' : '',
|
|
].filter(Boolean).join(' ')}
|
|
key={item.dateString}
|
|
onClick={() => updateDate(item.dateString)}
|
|
type="button"
|
|
>
|
|
{item.date.getDate()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<label className="ui-time-picker">
|
|
<span><Clock3 size={15} /> 发送时间</span>
|
|
<input onChange={(event) => updateTime(event.target.value)} type="time" value={time || '09:00'} />
|
|
</label>
|
|
<div className="ui-date-range-actions">
|
|
<button onClick={() => onChange('')} type="button">清空</button>
|
|
<button disabled={!value} onClick={() => setOpen(false)} type="button">确定</button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|