使用 HTML + JavaScript 实现一个日历任务管理系统
在现代快节奏的生活中,有效的时间管理变得越来越重要。本项目是一个基于 HTML 和 JavaScript 开发的日历任务管理系统,旨在为用户提供一个直观、便捷的时间管理工具。系统不仅能够清晰地展示当月日期,还支持事件的添加、编辑和删除操作,并通过本地存储(localStorage)实现数据的保存。
效果演示
项目概述
本项目主要包含以下核心功能:
- 日历展示:动态生成当月日期,并支持月份切换
- 事件管理:支持添加、编辑、删除事件
- 类型区分:支持三种类型事件
页面结构
HTML 部分定义了页面的主要布局,包含日历头部、日历主体、模态框三大部分,其中日历日期将通过JavaScript动态生成。
<div class="calendar-container"><div class="calendar-header"><h1 id="current-month"></h1><div class="calendar-nav"><button id="prev-month">上个月</button><button id="next-month">下个月</button><button id="add-event">添加事件</button></div></div><div class="calendar-grid" id="calendar-grid"><div class="calendar-day-header">周日</div><div class="calendar-day-header">周一</div><div class="calendar-day-header">周二</div><div class="calendar-day-header">周三</div><div class="calendar-day-header">周四</div><div class="calendar-day-header">周五</div><div class="calendar-day-header">周六</div><!-- 日历日期将通过JavaScript动态生成 --></div>
</div>
<div id="event-modal" class="modal"><div class="modal-content"><span class="close">×</span><h2 id="modal-title">添加新事件</h2><form id="event-form"><input type="hidden" id="event-id"><input type="hidden" id="event-date"><div class="form-group"><label for="event-title">标题</label><input type="text" id="event-title" required></div><div class="form-group"><label for="event-type">类型</label><select id="event-type"><option value="event">事件</option><option value="task">任务</option><option value="important">重要</option></select></div><div class="form-group"><label for="event-start">开始时间</label><input type="time" id="event-start"></div><div class="form-group"><label for="event-end">结束时间</label><input type="time" id="event-end"></div><div class="form-group"><label for="event-description">描述</label><textarea id="event-description" rows="3"></textarea></div><div class="form-actions"><button type="button" id="delete-event" style="display: none; background: #f44336; color: white; border: none;">删除</button><button type="button" id="cancel-event">取消</button><button type="submit" id="save-event" style="background: #4CAF50; color: white; border: none;">保存</button></div></form></div>
</div>
核心功能实现
定义基础数据
获取页面核心 DOM 元素,为后续操作做准备
const calendarGrid = document.getElementById('calendar-grid');
const currentMonthElement = document.getElementById('current-month');
const prevMonthButton = document.getElementById('prev-month');
const nextMonthButton = document.getElementById('next-month');
const addEventButton = document.getElementById('add-event');
const modal = document.getElementById('event-modal');
const closeButton = document.querySelector('.close');
const cancelButton = document.getElementById('cancel-event');
const deleteButton = document.getElementById('delete-event');
const eventForm = document.getElementById('event-form');
初始化当前日期信息
let currentDate = new Date();
let currentYear = currentDate.getFullYear();
let currentMonth = currentDate.getMonth();
读取或初始化事件数据
let events = JSON.parse(localStorage.getItem('calendarEvents')) || [];
渲染日历
日历渲染算法流程如下:
-
计算当月第一天和最后一天的日期对象
-
确定当月第一天是星期几,用于定位起始位置
-
填充上个月末尾的日期(灰色显示)
-
循环生成当月的所有日期单元格
-
计算是否需要补充下个月初的日期
-
为今天添加特殊样式
-
最后调用 renderEvents 方法渲染当月所有事件
function renderCalendar(month, year) {// 更新当前月份显示currentMonthElement.textContent = `${year}年${month + 1}月`;// 清除之前的日历日期while (calendarGrid.children.length > 7) {calendarGrid.removeChild(calendarGrid.lastChild);}// 获取当月第一天和最后一天const firstDay = new Date(year, month, 1);const lastDay = new Date(year, month + 1, 0);// 获取第一天是星期几 (0-6, 0是周日)const firstDayOfWeek = firstDay.getDay();// 获取上个月的最后几天const prevMonthLastDay = new Date(year, month, 0).getDate();// 添加上个月的几天for (let i = firstDayOfWeek - 1; i >= 0; i--) {const dayElement = createDayElement(prevMonthLastDay - i, true);calendarGrid.appendChild(dayElement);}// 添加当月的所有天for (let i = 1; i <= lastDay.getDate(); i++) {const dayElement = createDayElement(i, false);// 检查是否是今天const today = new Date();if (year === today.getFullYear() && month === today.getMonth() && i === today.getDate()) {dayElement.style.backgroundColor = '#e8f5e9';}calendarGrid.appendChild(dayElement);}// 计算还需要添加多少天const totalDays = firstDayOfWeek + lastDay.getDate();const remainingDays = 7 - (totalDays % 7);// 添加下个月的前几天if (remainingDays < 7) {for (let i = 1; i <= remainingDays; i++) {const dayElement = createDayElement(i, true);calendarGrid.appendChild(dayElement);}}// 渲染事件renderEvents();
}
渲染事件
每个日期单元格都可能包含多个事件标记。
function renderEvents() {// 获取所有日期元素const dayElements = document.querySelectorAll('.calendar-day:not(.inactive)');dayElements.forEach(dayElement => {// 清除之前的事件const existingEvents = dayElement.querySelectorAll('.event');existingEvents.forEach(event => event.remove());// 获取当前日期const day = parseInt(dayElement.querySelector('.day-number').textContent);const date = new Date(currentYear, currentMonth, day);// 查找当天的所有事件const dayEvents = events.filter(event => {const eventDate = new Date(event.date);return eventDate.toDateString() === date.toDateString();});// 添加事件到日期dayEvents.forEach(event => {const eventElement = document.createElement('div');eventElement.className = `event ${event.type}`;eventElement.textContent = `${event.startTime} ${event.title}`;eventElement.dataset.id = event.id;// 添加点击事件eventElement.addEventListener('click', (e) => {e.stopPropagation();openModal(null, event.id);});dayElement.appendChild(eventElement);});});
}
新增/编辑事件
打开表单的模态框,点击空白日期时打开添加事件模态框,点击已有事件时打开编辑该事件的模态框。
function openModal(date = null, eventId = null) {const modalTitle = document.getElementById('modal-title');const eventDateInput = document.getElementById('event-date');const eventIdInput = document.getElementById('event-id');const eventTitleInput = document.getElementById('event-title');const eventTypeInput = document.getElementById('event-type');const eventStartInput = document.getElementById('event-start');const eventEndInput = document.getElementById('event-end');const eventDescriptionInput = document.getElementById('event-description');// 重置表单eventForm.reset();if (eventId) {// 编辑现有事件const event = events.find(e => e.id === eventId);if (event) {modalTitle.textContent = '编辑事件';eventIdInput.value = event.id;eventDateInput.value = event.date;eventTitleInput.value = event.title;eventTypeInput.value = event.type;eventStartInput.value = event.startTime || '';eventEndInput.value = event.endTime || '';eventDescriptionInput.value = event.description || '';deleteButton.style.display = 'inline-block';}} else {// 添加新事件modalTitle.textContent = '添加新事件';eventIdInput.value = generateId();if (date) {// 设置日期为点击的日期const formattedDate = formatDate(date);eventDateInput.value = formattedDate;} else {// 默认为今天const today = new Date();const formattedDate = formatDate(today);eventDateInput.value = formattedDate;}deleteButton.style.display = 'none';}modal.style.display = 'block';
}
事件验证后把事件保存到本地,并重新渲染日历。
function saveEvent() {const eventId = document.getElementById('event-id').value;const eventDate = document.getElementById('event-date').value;const eventTitle = document.getElementById('event-title').value;const eventType = document.getElementById('event-type').value;const eventStart = document.getElementById('event-start').value;const eventEnd = document.getElementById('event-end').value;const eventDescription = document.getElementById('event-description').value;// 验证if (!eventTitle) {alert('请输入标题');return;}// 查找是否已存在该事件const existingEventIndex = events.findIndex(e => e.id === eventId);const event = {id: eventId,date: eventDate,title: eventTitle,type: eventType,startTime: eventStart,endTime: eventEnd,description: eventDescription};if (existingEventIndex !== -1) {// 更新现有事件events[existingEventIndex] = event;} else {// 添加新事件events.push(event);}// 保存到本地存储localStorage.setItem('calendarEvents', JSON.stringify(events));// 重新渲染日历renderCalendar(currentMonth, currentYear);// 关闭模态框closeModal();
}
删除事件
function deleteEvent() {const eventId = document.getElementById('event-id').value;if (confirm('确定要删除这个事件吗?')) {// 从数组中移除事件events = events.filter(e => e.id !== eventId);// 保存到本地存储localStorage.setItem('calendarEvents', JSON.stringify(events));// 重新渲染日历renderCalendar(currentMonth, currentYear);// 关闭模态框closeModal();}
}
月份切换
点击【上个月】【下个月】按钮切换月份,系统会自动更新日历显示。
prevMonthButton.addEventListener('click', () => {currentMonth--;if (currentMonth < 0) {currentMonth = 11;currentYear--;}renderCalendar(currentMonth, currentYear);
});nextMonthButton.addEventListener('click', () => {currentMonth++;if (currentMonth > 11) {currentMonth = 0;currentYear++;}renderCalendar(currentMonth, currentYear);
});
扩展建议
- 增加事件提醒功能
- 支持“日”、“周”、“月”视图切换功能
- 添加拖拽排序功能
- 添加右键菜单快速操作功能
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>日历任务管理系统</title><style>body {margin: 0;padding: 20px;}.calendar-container {max-width: 1000px;margin: 0 auto;}.calendar-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;}.calendar-nav button {background: #4CAF50;color: white;border: none;padding: 8px 16px;border-radius: 4px;cursor: pointer;}.calendar-nav button:hover {background: #45a049;}.calendar-grid {display: grid;grid-template-columns: repeat(7, 1fr);gap: 10px;}.calendar-day-header {text-align: center;font-weight: bold;padding: 10px;background: #f2f2f2;}.calendar-day {border: 1px solid #ddd;min-height: 100px;padding: 5px;position: relative;}.calendar-day.inactive {background: #f9f9f9;color: #aaa;}.day-number {font-weight: bold;margin-bottom: 5px;}.event {background: #e3f2fd;border-left: 3px solid #2196F3;padding: 2px 5px;margin: 2px 0;font-size: 12px;border-radius: 2px;cursor: pointer;}.event.task {background: #e8f5e9;border-left-color: #4CAF50;}.event.important {background: #ffebee;border-left-color: #f44336;}.modal {display: none;position: fixed;z-index: 1;left: 0;top: 0;width: 100%;height: 100%;background-color: rgba(0,0,0,0.4);}.modal-content {background-color: #fefefe;margin: 10% auto;padding: 20px;border: 1px solid #888;width: 80%;max-width: 500px;}.close {color: #aaa;float: right;font-size: 28px;font-weight: bold;cursor: pointer;}.form-group {margin-bottom: 15px;}.form-group label {display: block;margin-bottom: 5px;}.form-group input, .form-group select, .form-group textarea {width: 100%;padding: 8px;box-sizing: border-box;}.form-actions {text-align: right;}.form-actions button {padding: 8px 16px;margin-left: 10px;}</style>
</head>
<body>
<div class="calendar-container"><div class="calendar-header"><h1 id="current-month"></h1><div class="calendar-nav"><button id="prev-month">上个月</button><button id="next-month">下个月</button><button id="add-event">添加事件</button></div></div><div class="calendar-grid" id="calendar-grid"><!-- 日历头部 - 星期几 --><div class="calendar-day-header">周日</div><div class="calendar-day-header">周一</div><div class="calendar-day-header">周二</div><div class="calendar-day-header">周三</div><div class="calendar-day-header">周四</div><div class="calendar-day-header">周五</div><div class="calendar-day-header">周六</div><!-- 日历日期将通过JavaScript动态生成 --></div>
</div>
<!-- 添加/编辑事件的模态框 -->
<div id="event-modal" class="modal"><div class="modal-content"><span class="close">×</span><h2 id="modal-title">添加新事件</h2><form id="event-form"><input type="hidden" id="event-id"><input type="hidden" id="event-date"><div class="form-group"><label for="event-title">标题</label><input type="text" id="event-title" required></div><div class="form-group"><label for="event-type">类型</label><select id="event-type"><option value="event">事件</option><option value="task">任务</option><option value="important">重要</option></select></div><div class="form-group"><label for="event-start">开始时间</label><input type="time" id="event-start"></div><div class="form-group"><label for="event-end">结束时间</label><input type="time" id="event-end"></div><div class="form-group"><label for="event-description">描述</label><textarea id="event-description" rows="3"></textarea></div><div class="form-actions"><button type="button" id="delete-event" style="display: none; background: #f44336; color: white; border: none;">删除</button><button type="button" id="cancel-event">取消</button><button type="submit" id="save-event" style="background: #4CAF50; color: white; border: none;">保存</button></div></form></div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() {const calendarGrid = document.getElementById('calendar-grid');const currentMonthElement = document.getElementById('current-month');const prevMonthButton = document.getElementById('prev-month');const nextMonthButton = document.getElementById('next-month');const addEventButton = document.getElementById('add-event');const modal = document.getElementById('event-modal');const closeButton = document.querySelector('.close');const cancelButton = document.getElementById('cancel-event');const deleteButton = document.getElementById('delete-event');const eventForm = document.getElementById('event-form');let currentDate = new Date();let currentYear = currentDate.getFullYear();let currentMonth = currentDate.getMonth();let events = JSON.parse(localStorage.getItem('calendarEvents')) || [];// 初始化日历renderCalendar(currentMonth, currentYear);// 月份切换prevMonthButton.addEventListener('click', () => {currentMonth--;if (currentMonth < 0) {currentMonth = 11;currentYear--;}renderCalendar(currentMonth, currentYear);});nextMonthButton.addEventListener('click', () => {currentMonth++;if (currentMonth > 11) {currentMonth = 0;currentYear++;}renderCalendar(currentMonth, currentYear);});addEventButton.addEventListener('click', () => {openModal();});closeButton.addEventListener('click', () => {closeModal();});cancelButton.addEventListener('click', () => {closeModal();});window.addEventListener('click', (event) => {if (event.target === modal) {closeModal();}});eventForm.addEventListener('submit', (e) => {e.preventDefault();saveEvent();});deleteButton.addEventListener('click', () => {deleteEvent();});// 渲染日历function renderCalendar(month, year) {// 更新当前月份显示currentMonthElement.textContent = `${year}年${month + 1}月`;// 清除之前的日历日期while (calendarGrid.children.length > 7) {calendarGrid.removeChild(calendarGrid.lastChild);}// 获取当月第一天和最后一天const firstDay = new Date(year, month, 1);const lastDay = new Date(year, month + 1, 0);// 获取第一天是星期几 (0-6, 0是周日)const firstDayOfWeek = firstDay.getDay();// 获取上个月的最后几天const prevMonthLastDay = new Date(year, month, 0).getDate();// 添加上个月的几天for (let i = firstDayOfWeek - 1; i >= 0; i--) {const dayElement = createDayElement(prevMonthLastDay - i, true);calendarGrid.appendChild(dayElement);}// 添加当月的所有天for (let i = 1; i <= lastDay.getDate(); i++) {const dayElement = createDayElement(i, false);// 检查是否是今天const today = new Date();if (year === today.getFullYear() && month === today.getMonth() && i === today.getDate()) {dayElement.style.backgroundColor = '#e8f5e9';}calendarGrid.appendChild(dayElement);}// 计算还需要添加多少天const totalDays = firstDayOfWeek + lastDay.getDate();const remainingDays = 7 - (totalDays % 7);// 添加下个月的前几天if (remainingDays < 7) {for (let i = 1; i <= remainingDays; i++) {const dayElement = createDayElement(i, true);calendarGrid.appendChild(dayElement);}}// 渲染事件renderEvents();}// 创建日期元素function createDayElement(day, inactive) {const dayElement = document.createElement('div');dayElement.className = 'calendar-day' + (inactive ? ' inactive' : '');const dayNumber = document.createElement('div');dayNumber.className = 'day-number';dayNumber.textContent = day;dayElement.appendChild(dayNumber);// 如果不是非活动日期,添加点击事件if (!inactive) {dayElement.addEventListener('click', () => {const currentDate = new Date(currentYear, currentMonth, day);openModal(currentDate);});}return dayElement;}// 渲染事件到日历function renderEvents() {// 获取所有日期元素const dayElements = document.querySelectorAll('.calendar-day:not(.inactive)');dayElements.forEach(dayElement => {// 清除之前的事件const existingEvents = dayElement.querySelectorAll('.event');existingEvents.forEach(event => event.remove());// 获取当前日期const day = parseInt(dayElement.querySelector('.day-number').textContent);const date = new Date(currentYear, currentMonth, day);// 查找当天的所有事件const dayEvents = events.filter(event => {const eventDate = new Date(event.date);return eventDate.toDateString() === date.toDateString();});// 添加事件到日期dayEvents.forEach(event => {const eventElement = document.createElement('div');eventElement.className = `event ${event.type}`;eventElement.textContent = `${event.startTime} ${event.title}`;eventElement.dataset.id = event.id;// 添加点击事件eventElement.addEventListener('click', (e) => {e.stopPropagation();openModal(null, event.id);});dayElement.appendChild(eventElement);});});}// 打开模态框function openModal(date = null, eventId = null) {const modalTitle = document.getElementById('modal-title');const eventDateInput = document.getElementById('event-date');const eventIdInput = document.getElementById('event-id');const eventTitleInput = document.getElementById('event-title');const eventTypeInput = document.getElementById('event-type');const eventStartInput = document.getElementById('event-start');const eventEndInput = document.getElementById('event-end');const eventDescriptionInput = document.getElementById('event-description');// 重置表单eventForm.reset();if (eventId) {// 编辑现有事件const event = events.find(e => e.id === eventId);if (event) {modalTitle.textContent = '编辑事件';eventIdInput.value = event.id;eventDateInput.value = event.date;eventTitleInput.value = event.title;eventTypeInput.value = event.type;eventStartInput.value = event.startTime || '';eventEndInput.value = event.endTime || '';eventDescriptionInput.value = event.description || '';deleteButton.style.display = 'inline-block';}} else {// 添加新事件modalTitle.textContent = '添加新事件';eventIdInput.value = generateId();if (date) {// 设置日期为点击的日期const formattedDate = formatDate(date);eventDateInput.value = formattedDate;} else {// 默认为今天const today = new Date();const formattedDate = formatDate(today);eventDateInput.value = formattedDate;}deleteButton.style.display = 'none';}modal.style.display = 'block';}// 关闭模态框function closeModal() {modal.style.display = 'none';}// 保存事件function saveEvent() {const eventId = document.getElementById('event-id').value;const eventDate = document.getElementById('event-date').value;const eventTitle = document.getElementById('event-title').value;const eventType = document.getElementById('event-type').value;const eventStart = document.getElementById('event-start').value;const eventEnd = document.getElementById('event-end').value;const eventDescription = document.getElementById('event-description').value;// 验证if (!eventTitle) {alert('请输入标题');return;}// 查找是否已存在该事件const existingEventIndex = events.findIndex(e => e.id === eventId);const event = {id: eventId,date: eventDate,title: eventTitle,type: eventType,startTime: eventStart,endTime: eventEnd,description: eventDescription};if (existingEventIndex !== -1) {// 更新现有事件events[existingEventIndex] = event;} else {// 添加新事件events.push(event);}// 保存到本地存储localStorage.setItem('calendarEvents', JSON.stringify(events));// 重新渲染日历renderCalendar(currentMonth, currentYear);// 关闭模态框closeModal();}// 删除事件function deleteEvent() {const eventId = document.getElementById('event-id').value;if (confirm('确定要删除这个事件吗?')) {// 从数组中移除事件events = events.filter(e => e.id !== eventId);// 保存到本地存储localStorage.setItem('calendarEvents', JSON.stringify(events));// 重新渲染日历renderCalendar(currentMonth, currentYear);// 关闭模态框closeModal();}}// 生成唯一IDfunction generateId() {return Date.now().toString(36) + Math.random().toString(36).substr(2);}// 格式化日期为YYYY-MM-DDfunction formatDate(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}`;}});
</script>
</body>
</html>