|
|
|
|
import React, { useMemo, useState } from 'react';
|
|
|
|
|
import { Button, Segmented, Select } from 'antd';
|
|
|
|
|
import { PlusOutlined, GlobalOutlined, BranchesOutlined } from '@ant-design/icons';
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import styles from './ActivityCalendar.less';
|
|
|
|
|
|
|
|
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
|
|
|
|
|
|
|
|
const calendarEvents = [
|
|
|
|
|
{
|
|
|
|
|
id: 'earth-day',
|
|
|
|
|
title: '世界环保日',
|
|
|
|
|
start: dayjs('2020-12-14'),
|
|
|
|
|
end: dayjs('2020-12-15'),
|
|
|
|
|
tone: 'sand',
|
|
|
|
|
icon: <GlobalOutlined />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'tree-day',
|
|
|
|
|
title: '全国植树日',
|
|
|
|
|
start: dayjs('2020-12-24'),
|
|
|
|
|
end: dayjs('2020-12-31'),
|
|
|
|
|
tone: 'mint',
|
|
|
|
|
icon: <BranchesOutlined />,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const buildGrid = (referenceDate, viewMode) => {
|
|
|
|
|
if (viewMode === 'week') {
|
|
|
|
|
const startOfWeek = referenceDate.startOf('week');
|
|
|
|
|
const days = Array.from({ length: 7 }, (_, index) => startOfWeek.add(index, 'day'));
|
|
|
|
|
return { days, gridStart: startOfWeek, weeks: 1 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const startOfMonth = referenceDate.startOf('month');
|
|
|
|
|
const gridStart = startOfMonth.startOf('week');
|
|
|
|
|
const days = Array.from({ length: 42 }, (_, index) => gridStart.add(index, 'day'));
|
|
|
|
|
|
|
|
|
|
return { days, gridStart, weeks: 6 };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildEventSegments = (events, gridStart, weeks) => {
|
|
|
|
|
const gridEnd = gridStart.add(weeks * 7 - 1, 'day').endOf('day');
|
|
|
|
|
const segments = [];
|
|
|
|
|
|
|
|
|
|
events.forEach((event) => {
|
|
|
|
|
const start = event.start;
|
|
|
|
|
const end = event.end;
|
|
|
|
|
|
|
|
|
|
if (end.isBefore(gridStart) || start.isAfter(gridEnd)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let weekIndex = 0; weekIndex < weeks; weekIndex += 1) {
|
|
|
|
|
const weekStart = gridStart.add(weekIndex * 7, 'day');
|
|
|
|
|
const weekEnd = weekStart.add(6, 'day').endOf('day');
|
|
|
|
|
|
|
|
|
|
const segmentStart = start.isAfter(weekStart) ? start : weekStart;
|
|
|
|
|
const segmentEnd = end.isBefore(weekEnd) ? end : weekEnd;
|
|
|
|
|
|
|
|
|
|
if (segmentEnd.isBefore(weekStart) || segmentStart.isAfter(weekEnd)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columnStart = segmentStart.diff(weekStart, 'day') + 1;
|
|
|
|
|
const columnEnd = segmentEnd.diff(weekStart, 'day') + 2;
|
|
|
|
|
|
|
|
|
|
segments.push({
|
|
|
|
|
id: `${event.id}-${weekIndex}`,
|
|
|
|
|
title: event.title,
|
|
|
|
|
tone: event.tone,
|
|
|
|
|
icon: event.icon,
|
|
|
|
|
rowStart: weekIndex + 1,
|
|
|
|
|
rowEnd: weekIndex + 2,
|
|
|
|
|
columnStart,
|
|
|
|
|
columnEnd,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return segments;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ActivityCalendar = () => {
|
|
|
|
|
const [currentDate, setCurrentDate] = useState(dayjs('2020-12-01'));
|
|
|
|
|
const [viewMode, setViewMode] = useState('month');
|
|
|
|
|
|
|
|
|
|
const { days, gridStart, weeks } = useMemo(() => buildGrid(currentDate, viewMode), [currentDate, viewMode]);
|
|
|
|
|
const segments = useMemo(() => buildEventSegments(calendarEvents, gridStart, weeks), [gridStart, weeks]);
|
|
|
|
|
|
|
|
|
|
const yearOptions = useMemo(() => {
|
|
|
|
|
const baseYear = dayjs().year();
|
|
|
|
|
const startYear = Math.min(2020, baseYear - 4);
|
|
|
|
|
return Array.from({ length: 10 }, (_, index) => startYear + index);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleYearChange = (year) => {
|
|
|
|
|
setCurrentDate((prev) => prev.year(year));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMonthChange = (month) => {
|
|
|
|
|
setCurrentDate((prev) => prev.month(month - 1));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleViewChange = (mode) => {
|
|
|
|
|
setViewMode(mode);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBackToToday = () => {
|
|
|
|
|
const today = dayjs();
|
|
|
|
|
setCurrentDate(today);
|
|
|
|
|
setViewMode('month');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={styles.container}>
|
|
|
|
|
<div className={styles.toolbar}>
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} className={styles.createButton}>
|
|
|
|
|
新建活动
|
|
|
|
|
</Button>
|
|
|
|
|
<div className={styles.toolbarRight}>
|
|
|
|
|
<Select
|
|
|
|
|
size="middle"
|
|
|
|
|
value={currentDate.year()}
|
|
|
|
|
className={styles.select}
|
|
|
|
|
popupClassName={styles.dropdown}
|
|
|
|
|
onChange={handleYearChange}
|
|
|
|
|
options={yearOptions.map((year) => ({ value: year, label: `${year}` }))}
|
|
|
|
|
/>
|
|
|
|
|
<Select
|
|
|
|
|
size="middle"
|
|
|
|
|
value={currentDate.month() + 1}
|
|
|
|
|
className={styles.select}
|
|
|
|
|
popupClassName={styles.dropdown}
|
|
|
|
|
onChange={handleMonthChange}
|
|
|
|
|
options={Array.from({ length: 12 }, (_, index) => ({
|
|
|
|
|
value: index + 1,
|
|
|
|
|
label: `${index + 1}月`,
|
|
|
|
|
}))}
|
|
|
|
|
/>
|
|
|
|
|
<Segmented
|
|
|
|
|
value={viewMode}
|
|
|
|
|
onChange={handleViewChange}
|
|
|
|
|
options={[
|
|
|
|
|
{ label: '月', value: 'month' },
|
|
|
|
|
{ label: '周', value: 'week' },
|
|
|
|
|
]}
|
|
|
|
|
className={styles.segmented}
|
|
|
|
|
/>
|
|
|
|
|
<Button className={styles.dayButton} onClick={handleBackToToday}>
|
|
|
|
|
日
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className={styles.calendarWrapper}>
|
|
|
|
|
<div className={styles.weekHeader}>
|
|
|
|
|
{weekDays.map((day) => (
|
|
|
|
|
<div key={day} className={styles.weekCell}>
|
|
|
|
|
{day}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className={styles.calendarGrid}>
|
|
|
|
|
{days.map((day) => {
|
|
|
|
|
const isCurrentMonth = day.month() === currentDate.month();
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={day.format('YYYY-MM-DD')}
|
|
|
|
|
className={`${styles.dayCell} ${isCurrentMonth ? '' : styles.dimmed}`}
|
|
|
|
|
>
|
|
|
|
|
<span className={styles.dayNumber}>{day.date()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{segments.map((segment) => (
|
|
|
|
|
<div
|
|
|
|
|
key={segment.id}
|
|
|
|
|
className={`${styles.eventBlock} ${styles[segment.tone]}`}
|
|
|
|
|
style={{
|
|
|
|
|
gridColumnStart: segment.columnStart,
|
|
|
|
|
gridColumnEnd: segment.columnEnd,
|
|
|
|
|
gridRowStart: segment.rowStart,
|
|
|
|
|
gridRowEnd: segment.rowEnd,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className={styles.eventInfo}>
|
|
|
|
|
<span className={styles.eventDot} />
|
|
|
|
|
<span className={styles.eventTitle}>{segment.title}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.eventIcon}>{segment.icon}</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ActivityCalendar;
|