Initial CMPP frontend prototype
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { CalendarDays, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
||||
|
||||
export type DateRangeValue = {
|
||||
start?: string;
|
||||
end?: string;
|
||||
};
|
||||
|
||||
type DateRangeInputProps = {
|
||||
label?: string;
|
||||
value: DateRangeValue;
|
||||
onChange: (value: DateRangeValue) => void;
|
||||
};
|
||||
|
||||
const weekDays = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
const presets = [
|
||||
{ label: '今天', days: 0 },
|
||||
{ label: '近7天', days: 6 },
|
||||
{ label: '近30天', days: 29 },
|
||||
];
|
||||
|
||||
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 isSameDate(left: Date, right?: Date) {
|
||||
if (!right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return formatDate(left) === formatDate(right);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function DateRangeInput({ label, value, onChange }: DateRangeInputProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [viewDate, setViewDate] = useState(() => parseDate(value.start) ?? new Date());
|
||||
const startDate = parseDate(value.start);
|
||||
const endDate = parseDate(value.end);
|
||||
|
||||
const calendarDays = useMemo(() => getCalendarDays(viewDate), [viewDate]);
|
||||
|
||||
const displayValue = useMemo(() => {
|
||||
if (value.start && value.end) {
|
||||
return `${value.start} 至 ${value.end}`;
|
||||
}
|
||||
|
||||
if (value.start) {
|
||||
return `${value.start} 至 ...`;
|
||||
}
|
||||
|
||||
return '选择提交时间区间';
|
||||
}, [value.end, value.start]);
|
||||
|
||||
function moveMonth(offset: number) {
|
||||
setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1));
|
||||
}
|
||||
|
||||
function applyPreset(days: number) {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setDate(end.getDate() - days);
|
||||
onChange({ start: formatDate(start), end: formatDate(end) });
|
||||
setViewDate(start);
|
||||
}
|
||||
|
||||
function selectDate(dateString: string) {
|
||||
if (!value.start || value.end) {
|
||||
onChange({ start: dateString, end: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
if (dateString < value.start) {
|
||||
onChange({ start: dateString, end: value.start });
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ start: value.start, end: dateString });
|
||||
}
|
||||
|
||||
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.start || value.end ? 'ui-date-range-trigger--filled' : ''].filter(Boolean).join(' ')}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<CalendarDays size={16} />
|
||||
<span>{displayValue}</span>
|
||||
{value.start || value.end ? (
|
||||
<span
|
||||
aria-label="清空时间区间"
|
||||
className="ui-date-range__clear"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onChange({});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onChange({});
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<X size={14} />
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="ui-date-range-popover">
|
||||
<div className="ui-date-range-presets">
|
||||
{presets.map((item) => (
|
||||
<button key={item.label} onClick={() => applyPreset(item.days)} type="button">{item.label}</button>
|
||||
))}
|
||||
</div>
|
||||
<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) => {
|
||||
const isStart = isSameDate(item.date, startDate);
|
||||
const isEnd = isSameDate(item.date, endDate);
|
||||
const isInRange = Boolean(startDate && endDate && item.date > startDate && item.date < endDate);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={[
|
||||
'ui-calendar__day',
|
||||
item.inCurrentMonth ? '' : 'ui-calendar__day--muted',
|
||||
isStart ? 'ui-calendar__day--start' : '',
|
||||
isEnd ? 'ui-calendar__day--end' : '',
|
||||
isInRange ? 'ui-calendar__day--range' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
key={item.dateString}
|
||||
onClick={() => selectDate(item.dateString)}
|
||||
type="button"
|
||||
>
|
||||
{item.date.getDate()}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ui-date-range-actions">
|
||||
<button onClick={() => onChange({})} type="button">清空</button>
|
||||
<button disabled={!value.start} onClick={() => setOpen(false)} type="button">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user