下面我将为你提供一个完整、可运行的示例,并详细解释每一部分的作用,这个示例将包含:

(图片来源网络,侵删)
- 一个月视图的日历网格。
- 点击日期可以添加、查看和删除日程。
- 数据会保存在浏览器的
localStorage中,刷新页面后数据不会丢失。
最终效果预览
HTML 结构 (index.html)
这是页面的骨架,定义了日历的标题、星期几的表头、日期网格,以及用于添加日程的模态框。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">JavaScript 日程日历</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="calendar-container">
<header class="calendar-header">
<button id="prevMonth"><</button>
<h1 id="currentMonth"></h1>
<button id="nextMonth">></button>
</header>
<div class="calendar-grid">
<!-- 星期几的表头 -->
<div class="weekdays">
<div>日</div>
<div>一</div>
<div>二</div>
<div>三</div>
<div>四</div>
<div>五</div>
<div>六</div>
</div>
<!-- 日期格子将通过 JavaScript 动态生成 -->
<div id="daysContainer" class="days"></div>
</div>
</div>
<!-- 添加/编辑日程的模态框 -->
<div id="eventModal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h2 id="modalTitle">添加日程</h2>
<form id="eventForm">
<input type="hidden" id="eventDate">
<input type="text" id="eventTitle" placeholder="日程标题" required>
<textarea id="eventDescription" placeholder="日程描述 (可选)"></textarea>
<button type="submit">保存</button>
</form>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
CSS 样式 (style.css)
这部分负责美化日历,让它看起来更美观、易用,它定义了布局、颜色、字体和交互效果(如悬停和模态框)。
body {
font-family: 'Arial', sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.calendar-container {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 800px;
overflow: hidden;
}
.calendar-header {
background-color: #007bff;
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.calendar-header h1 {
margin: 0;
font-size: 1.5em;
}
.calendar-header button {
background: none;
border: none;
color: white;
font-size: 1.5em;
cursor: pointer;
padding: 0 10px;
transition: background-color 0.2s;
}
.calendar-header button:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
background-color: #e9ecef;
text-align: center;
font-weight: bold;
padding: 10px 0;
}
.days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: #ddd;
padding: 1px;
}
.day {
background-color: #fff;
min-height: 100px;
padding: 10px;
box-sizing: border-box;
cursor: pointer;
transition: background-color 0.2s;
position: relative;
}
.day:hover {
background-color: #f8f9fa;
}
.day.other-month {
color: #ccc;
background-color: #fafafa;
}
.day.today {
background-color: #e3f2fd;
font-weight: bold;
}
.day-number {
font-size: 0.9em;
margin-bottom: 5px;
}
.event {
background-color: #007bff;
color: white;
padding: 2px 5px;
margin: 2px 0;
border-radius: 3px;
font-size: 0.75em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.event:hover {
background-color: #0056b3;
}
/* 模态框样式 */
.modal {
display: none; /* 默认隐藏 */
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 500px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
}
#eventForm {
display: flex;
flex-direction: column;
gap: 10px;
}
#eventForm input, #eventForm textarea {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1em;
}
#eventForm button {
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
#eventForm button:hover {
background-color: #0056b3;
}
JavaScript 逻辑 (script.js)
这是整个应用的核心,负责处理所有的交互逻辑,包括渲染日历、切换月份、管理日程数据等。
document.addEventListener('DOMContentLoaded', () => {
// --- DOM 元素 ---
const currentMonthElement = document.getElementById('currentMonth');
const daysContainer = document.getElementById('daysContainer');
const prevMonthButton = document.getElementById('prevMonth');
const nextMonthButton = document.getElementById('nextMonth');
const eventModal = document.getElementById('eventModal');
const eventForm = document.getElementById('eventForm');
const modalTitle = document.getElementById('modalTitle');
const closeModal = document.querySelector('.close');
// --- 状态管理 ---
let currentDate = new Date();
let events = JSON.parse(localStorage.getItem('calendarEvents')) || {};
let selectedDate = null;
let editingEventId = null;
// --- 辅助函数 ---
const monthNames = [
"一月", "二月", "三月", "四月", "五月", "六月",
"七月", "八月", "九月", "十月", "十一月", "十二月"
];
function saveEventsToLocalStorage() {
localStorage.setItem('calendarEvents', JSON.stringify(events));
}
function formatDateKey(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// --- 渲染函数 ---
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// 更新月份标题
currentMonthElement.textContent = `${year}年 ${monthNames[month]}`;
// 清空日期容器
daysContainer.innerHTML = '';
// 获取当月第一天和最后一天
const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0);
// 获取第一天是星期几 (0 = 星期日, 6 = 星期六)
const firstDayOfWeek = firstDayOfMonth.getDay();
// 获取上个月的最后几天
const prevMonthLastDay = new Date(year, month, 0).getDate();
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const dayNumber = prevMonthLastDay - i;
const dayElement = createDayElement(new Date(year, month - 1, dayNumber), true);
daysContainer.appendChild(dayElement);
}
// 渲染当月的所有天
for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
const dayElement = createDayElement(new Date(year, month, day), false);
daysContainer.appendChild(dayElement);
}
// 渲染下个月的前几天,以填满6x7的网格
const totalCells = daysContainer.children.length;
const remainingCells = 42 - totalCells; // 6行 x 7列
for (let day = 1; day <= remainingCells; day++) {
const dayElement = createDayElement(new Date(year, month + 1, day), true);
daysContainer.appendChild(dayElement);
}
}
function createDayElement(date, isOtherMonth) {
const dayElement = document.createElement('div');
dayElement.className = 'day';
if (isOtherMonth) {
dayElement.classList.add('other-month');
}
// 标记今天
const today = new Date();
if (date.toDateString() === today.toDateString()) {
dayElement.classList.add('today');
}
const dayNumberElement = document.createElement('div');
dayNumberElement.className = 'day-number';
dayNumberElement.textContent = date.getDate();
dayElement.appendChild(dayNumberElement);
// 为选定的日期添加日程
const dateKey = formatDateKey(date);
if (events[dateKey]) {
events[dateKey].forEach(event => {
const eventElement = document.createElement('div');
eventElement.className = 'event';
eventElement.textContent = event.title;
eventElement.dataset.eventId = event.id;
eventElement.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡到 dayElement
editEvent(event.id);
});
dayElement.appendChild(eventElement);
});
}
// 点击日期添加新日程
dayElement.addEventListener('click', () => {
selectedDate = date;
openModal();
});
return dayElement;
}
// --- 模态框函数 ---
function openModal(date = null, eventId = null) {
editingEventId = eventId;
if (date) {
selectedDate = date;
}
if (editingEventId) {
modalTitle.textContent = '编辑日程';
const dateKey = formatDateKey(selectedDate);
const event = events[dateKey].find(e => e.id === editingEventId);
document.getElementById('eventTitle').value = event.title;
document.getElementById('eventDescription').value = event.description || '';
} else {
modalTitle.textContent = '添加日程';
eventForm.reset();
}
document.getElementById('eventDate').value = formatDateKey(selectedDate);
eventModal.style.display = 'block';
}
function closeModalFunc() {
eventModal.style.display = 'none';
eventForm.reset();
editingEventId = null;
}
function editEvent(eventId) {
// 需要知道这个日程属于哪一天
for (const dateKey in events) {
const event = events[dateKey].find(e => e.id === eventId);
if (event) {
openModal(new Date(dateKey), eventId);
return;
}
}
}
// --- 事件监听器 ---
prevMonthButton.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
nextMonthButton.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
closeModal.addEventListener('click', closeModalFunc);
window.addEventListener('click', (event) => {
if (event.target === eventModal) {
closeModalFunc();
}
});
eventForm.addEventListener('submit', (e) => {
e.preventDefault();
const dateKey = document.getElementById('eventDate').value;
const title = document.getElementById('eventTitle').value;
const description = document.getElementById('eventDescription').value;
if (!events[dateKey]) {
events[dateKey] = [];
}
if (editingEventId) {
// 编辑现有日程
const eventIndex = events[dateKey].findIndex(e => e.id === editingEventId);
if (eventIndex !== -1) {
events[dateKey][eventIndex] = { id: editingEventId, title, description };
}
} else {
// 添加新日程
const newEvent = {
id: generateUniqueId(),
title,
description
};
events[dateKey].push(newEvent);
}
saveEventsToLocalStorage();
renderCalendar();
closeModalFunc();
});
// --- 初始化 ---
renderCalendar();
});
如何使用
- 创建文件:创建三个文件,分别命名为
index.html、style.css和script.js。 - 复制代码:将上面提供的代码分别粘贴到对应的文件中。
- 打开网页:用浏览器打开
index.html文件。
功能扩展建议
这个基础版本已经可以满足基本需求,你可以在此基础上进行扩展:

(图片来源网络,侵删)
- 周视图/日视图:增加切换按钮,实现周视图和日视图的渲染逻辑。
- 日程提醒:使用
NotificationAPI 在特定时间弹出桌面提醒。 - 拖拽功能:允许用户拖拽日程到其他日期。
- 数据同步:连接后端 API,将日程数据存储在服务器上,实现多设备同步。
- UI/UX 优化:增加动画效果、更丰富的颜色主题、右键菜单等。
- 重复日程:支持添加每天、每周、每月重复的日程。

(图片来源网络,侵删)
