该 HTML 文件是一款功能完整的交互式美观日历工具,支持年月切换、今日快速定位、事件添加 / 删除 / 分类标记(普通 / 重要 / 纪念日),并通过本地存储(localStorage)持久化保存事件数据,同时具备响应式设计与精致视觉效果,兼顾实用性与美观度。
大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。
演示效果


HTML&CSS
html
<!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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.calendar-container {
width: 100%;
max-width: 900px;
background-color: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
padding: 20px;
}
.calendar{
font-size: 24px;
color: #4facfe;
margin: 5px auto;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
color: white;
border-radius: 5px 5px 0 0;
}
.calendar-header h1 {
font-size: 1.8rem;
font-weight: 500;
}
.calendar-nav {
display: flex;
gap: 15px;
}
.nav-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
background-color: #f8f9fa;
padding: 10px 0;
border-left: 1px solid #eaeaea;
border-right: 1px solid #eaeaea;
}
.weekday {
text-align: center;
font-weight: 600;
color: #6c757d;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: #eaeaea;
border: 1px solid #eaeaea;
}
.day {
min-height: 80px;
background-color: white;
padding: 6px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.day:hover {
background-color: #f8f9fa;
transform: scale(1.02);
z-index: 1;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.day-number {
font-size: 1rem;
font-weight: 600;
margin-bottom: 3px;
}
.event {
background-color: #e9ecef;
border-radius: 3px;
padding: 2px 4px;
margin-top: 2px;
font-size: 0.7rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.event.important {
background-color: #ffeaa7;
color: #d35400;
}
.event.anniversary {
background-color: #fab1a0;
color: #e84393;
}
.other-month {
color: #adb5bd;
background-color: #f8f9fa;
}
.today {
background-color: #e3f2fd;
border: 2px solid #4facfe;
}
.event-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
width: 90%;
max-width: 500px;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: modalAppear 0.3s ease;
}
@keyframes modalAppear {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
color: #4facfe;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6c757d;
}
.event-form input,
.event-form textarea,
.event-form select {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #eaeaea;
border-radius: 8px;
font-size: 1rem;
}
.event-form textarea {
min-height: 100px;
resize: vertical;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary {
background-color: #4facfe;
color: white;
}
.btn-primary:hover {
background-color: #3a9bf7;
}
.btn-secondary {
background-color: #e9ecef;
color: #6c757d;
}
.btn-secondary:hover {
background-color: #dee2e6;
}
.btn-danger {
background-color: #ff7675;
color: white;
}
.btn-danger:hover {
background-color: #ff5e57;
}
.event-list {
margin-top: 20px;
max-height: 200px;
overflow-y: auto;
}
.event-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eaeaea;
}
.event-item:last-child {
border-bottom: none;
}
.event-details h4 {
margin-bottom: 5px;
}
.event-details p {
color: #6c757d;
font-size: 0.9rem;
}
.event-actions {
display: flex;
gap: 10px;
}
.event-type-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
margin-left: 8px;
}
.anniversary-tag {
background-color: #fab1a0;
color: #e84393;
}
.important-tag {
background-color: #ffeaa7;
color: #d35400;
}
.no-events {
text-align: center;
color: #adb5bd;
padding: 20px;
}
@media (max-width: 768px) {
.calendar-header {
flex-direction: column;
gap: 15px;
}
.day {
min-height: 70px;
}
.event {
font-size: 0.65rem;
}
}
</style>
</head>
<body>
<div class="calendar-container">
<div class="calendar">
我的专属日历
</div>
<div class="calendar-header">
<h1 id="current-month-year">2023年11月</h1>
<div class="calendar-nav">
<button class="nav-btn" id="prev-month">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
</svg>
</button>
<button class="nav-btn" id="today-btn">今</button>
<button class="nav-btn" id="next-month">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</svg>
</button>
</div>
</div>
<div class="calendar-weekdays">
<div class="weekday">周日</div>
<div class="weekday">周一</div>
<div class="weekday">周二</div>
<div class="weekday">周三</div>
<div class="weekday">周四</div>
<div class="weekday">周五</div>
<div class="weekday">周六</div>
</div>
<div class="calendar-days" id="calendar-days">
</div>
</div>
<div class="event-modal" id="event-modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-date">2023年11月15日</h2>
<button class="close-btn" id="close-modal">×</button>
</div>
<div class="event-list" id="event-list">
<!-- 事件列表将通过JavaScript动态生成 -->
</div>
<form class="event-form" id="event-form">
<input type="text" id="event-title" placeholder="事件标题" required>
<textarea id="event-description" placeholder="事件描述"></textarea>
<select id="event-type">
<option value="normal">普通事件</option>
<option value="important">重要事件</option>
<option value="anniversary">纪念日</option>
</select>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="cancel-event">取消</button>
<button type="submit" class="btn btn-primary">添加事件</button>
</div>
</form>
</div>
</div>
<script>
// 当前日期和事件数据
let currentDate = new Date();
let events = JSON.parse(localStorage.getItem('calendarEvents')) || {};
// DOM元素
const calendarDays = document.getElementById('calendar-days');
const currentMonthYear = document.getElementById('current-month-year');
const prevMonthBtn = document.getElementById('prev-month');
const nextMonthBtn = document.getElementById('next-month');
const todayBtn = document.getElementById('today-btn');
const eventModal = document.getElementById('event-modal');
const modalDate = document.getElementById('modal-date');
const eventList = document.getElementById('event-list');
const eventForm = document.getElementById('event-form');
const closeModal = document.getElementById('close-modal');
const cancelEvent = document.getElementById('cancel-event');
// 初始化日历
function initCalendar() {
renderCalendar(currentDate);
setupEventListeners();
}
// 渲染日历
function renderCalendar(date) {
// 清除现有日历
calendarDays.innerHTML = '';
// 更新当前年月显示
currentMonthYear.textContent = `${date.getFullYear()}年 ${date.getMonth() + 1}月`;
// 获取当月第一天和最后一天
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
// 获取当月第一天是星期几(0是周日)
const firstDayIndex = firstDay.getDay();
// 获取上个月的最后几天
const prevLastDay = new Date(date.getFullYear(), date.getMonth(), 0).getDate();
// 获取下个月的前几天
const nextDays = 7 - lastDay.getDay() - 1;
// 获取今天的日期
const today = new Date();
const isToday = (day, month, year) => {
return day === today.getDate() &&
month === today.getMonth() &&
year === today.getFullYear();
};
// 添加上个月的日期
for (let i = firstDayIndex; i > 0; i--) {
const day = prevLastDay - i + 1;
const dayElement = document.createElement('div');
dayElement.className = 'day other-month';
dayElement.innerHTML = `
<div class="day-number">${day}</div>
`;
calendarDays.appendChild(dayElement);
}
// 添加当月的日期
for (let i = 1; i <= lastDay.getDate(); i++) {
const dayElement = document.createElement('div');
dayElement.className = 'day';
// 检查是否是今天
if (isToday(i, date.getMonth(), date.getFullYear())) {
dayElement.classList.add('today');
}
dayElement.innerHTML = `
<div class="day-number">${i}</div>
`;
// 添加事件到日期格子
const dateKey = formatDateKey(new Date(date.getFullYear(), date.getMonth(), i));
if (events[dateKey]) {
events[dateKey].forEach(event => {
const eventElement = document.createElement('div');
eventElement.className = `event ${event.type}`;
eventElement.textContent = event.title;
dayElement.appendChild(eventElement);
});
}
// 添加点击事件
dayElement.addEventListener('click', () => openEventModal(new Date(date.getFullYear(), date.getMonth(), i)));
calendarDays.appendChild(dayElement);
}
// 添加下个月的日期
for (let i = 1; i <= nextDays; i++) {
const dayElement = document.createElement('div');
dayElement.className = 'day other-month';
dayElement.innerHTML = `
<div class="day-number">${i}</div>
`;
calendarDays.appendChild(dayElement);
}
}
// 设置事件监听器
function setupEventListeners() {
prevMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar(currentDate);
});
nextMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar(currentDate);
});
todayBtn.addEventListener('click', () => {
currentDate = new Date();
renderCalendar(currentDate);
});
closeModal.addEventListener('click', closeEventModal);
cancelEvent.addEventListener('click', closeEventModal);
eventForm.addEventListener('submit', addEvent);
}
// 打开事件模态框
function openEventModal(date) {
modalDate.textContent = `${date.getFullYear()}年 ${date.getMonth() + 1}月 ${date.getDate()}日`;
// 显示该日期的事件
const dateKey = formatDateKey(date);
displayEvents(dateKey);
// 设置表单的日期
eventForm.dataset.date = dateKey;
// 显示模态框
eventModal.style.display = 'flex';
}
// 关闭事件模态框
function closeEventModal() {
eventModal.style.display = 'none';
eventForm.reset();
}
// 显示事件列表
function displayEvents(dateKey) {
eventList.innerHTML = '';
if (!events[dateKey] || events[dateKey].length === 0) {
eventList.innerHTML = '<div class="no-events">该日期暂无事件</div>';
return;
}
events[dateKey].forEach((event, index) => {
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
const typeTag = event.type === 'anniversary' ?
'<span class="event-type-tag anniversary-tag">纪念日</span>' :
event.type === 'important' ?
'<span class="event-type-tag important-tag">重要</span>' : '';
eventItem.innerHTML = `
<div class="event-details">
<h4>${event.title} ${typeTag}</h4>
<p>${event.description || '无描述'}</p>
</div>
<div class="event-actions">
<button class="btn btn-danger delete-event" data-index="${index}">删除</button>
</div>
`;
eventList.appendChild(eventItem);
});
// 添加删除事件监听器
document.querySelectorAll('.delete-event').forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
deleteEvent(dateKey, index);
});
});
}
// 添加事件
function addEvent(e) {
e.preventDefault();
const dateKey = eventForm.dataset.date;
const title = document.getElementById('event-title').value;
const description = document.getElementById('event-description').value;
const type = document.getElementById('event-type').value;
if (!title.trim()) return;
// 创建新事件
const newEvent = {
title: title.trim(),
description: description.trim(),
type: type
};
// 添加到事件列表
if (!events[dateKey]) {
events[dateKey] = [];
}
events[dateKey].push(newEvent);
// 保存到本地存储
localStorage.setItem('calendarEvents', JSON.stringify(events));
// 更新显示
displayEvents(dateKey);
// 重新渲染日历以显示新事件
renderCalendar(currentDate);
// 重置表单并关闭模态框
eventForm.reset();
closeEventModal(); // 修复:添加事件后关闭模态框
}
// 删除事件
function deleteEvent(dateKey, index) {
if (events[dateKey] && events[dateKey][index]) {
events[dateKey].splice(index, 1);
// 如果该日期没有事件了,删除该日期键
if (events[dateKey].length === 0) {
delete events[dateKey];
}
// 保存到本地存储
localStorage.setItem('calendarEvents', JSON.stringify(events));
// 更新显示
displayEvents(dateKey);
// 重新渲染日历
renderCalendar(currentDate);
}
}
// 格式化日期键 (YYYY-MM-DD)
function formatDateKey(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}`;
}
// 初始化日历
initCalendar();
</script>
</body>
</html>
HTML
- calendar-container:日历主容器:包裹所有日历组件,提供白色卡片背景与阴影,与页面渐变背景区分,聚焦核心功能
- calendar:日历标题:显示 "我的专属日历",强化工具专属感,视觉上作为日历顶部标识
- calendar-header:日历头部导航:包含当前年月显示(#current-month-year)与操作按钮(上月 / 今日 / 下月),控制日历切换
- nav-btn:导航按钮:左 / 右按钮切换上月 / 下月(内嵌 SVG 箭头图标),中间 "今" 按钮快速定位今日,统一圆形样式,交互直观
- calendar-weekdays:星期栏:采用 7 列网格(grid-template-columns: repeat(7, 1fr)),显示 "周日 - 周六",作为日期栏的表头
- weekday:单个星期文本:居中显示,加粗字体,浅灰色调,明确区分星期与日期
- calendar-days" id="calendar-days:日期网格容器:7 列网格布局,动态生成上月剩余天数、当月天数、下月开头天数,是日历核心交互区域
- day:单个日期格子:包含日期数字(.day-number)与事件标签(.event),点击可打开事件模态框;根据状态添加不同类(today 标记今日、other-month 标记非当月日期)
- event:事件标签:显示事件标题,根据类型添加不同类(important 重要事件、anniversary 纪念日),用颜色区分优先级
- event-modal:事件模态框:默认隐藏,点击日期时显示,用于添加 / 查看 / 删除事件;半透明黑色背景(rgba(0,0,0,0.5))覆盖页面,聚焦事件操作
- modal-content:模态框内容区:白色卡片背景,包含日期显示(#modal-date)、事件列表(#event-list)与事件表单(#event-form),动画弹出效果提升交互体验
- close-btn:模态框关闭按钮:右上角 "×" 图标,点击关闭模态框,样式简洁,不干扰其他操作
- event-form 事件表单:包含事件标题输入框、描述文本域、类型下拉框(普通 / 重要 / 纪念日)与操作按钮(取消 / 添加),实现事件添加功能
- event-list:事件列表:动态显示当前日期的所有事件,包含事件标题、描述、类型标签与删除按钮,支持事件删除交互
- btn:模态框功能按钮:"取消" 按钮关闭模态框,"添加" 按钮提交事件,"删除" 按钮删除对应事件,按功能区分颜色(蓝 / 灰 / 红),引导操作
CSS
- .calendar-container 日历主容器:
- 尺寸:宽度 100%(最大 900px,避免大屏幕拉伸);
- 视觉:白色背景(white)、圆角 15px、阴影(0 10px 30px rgba(0,0,0,0.1)),模拟卡片效果,与渐变背景形成层次;
- 溢出:overflow: hidden,确保内部元素不超出容器边界;
- 内边距:padding: 20px,隔离内部组件与容器边缘。
- .calendar 日历标题:
- 字体:24px、浅蓝(#4facfe)、居中显示;
- 间距:margin: 5px auto,与头部导航轻微分隔,强化标识作用。
- .calendar-header 日历头部:
- 布局:display: flex+justify-content: space-between+align-items: center,使年月显示与导航按钮左右分布;
- 视觉:渐变背景(linear-gradient(to right, #4facfe 0%, #00f2fe 100%))、白色文字、圆角(5px 5px 0 0),突出头部区域;
- 内边距:padding: 20px 30px,确保内容不拥挤。
- .nav-btn 导航按钮:
- 形状:圆形(border-radius: 50%)、40×40px 尺寸,方便点击;
- 视觉:半透明白色背景(rgba(255,255,255,0.2))、白色图标 / 文字,与头部渐变背景融合;
- 交互:transition: all 0.3s ease,hover 时背景加深(rgba(255,255,255,0.3))+ 缩放(scale(1.05)),提供明确反馈。
- .calendar-weekdays 星期栏:
- 布局:grid-template-columns: repeat(7, 1fr),7 列等宽,与日期栏对齐;
- 视觉:浅灰背景(#f8f9fa)、左右边框(1px solid #eaeaea),明确区分星期与日期区域;
- 内边距:padding: 10px 0,确保星期文本垂直居中。
- .weekday 星期文本:
- 排版:居中显示、600 字重,突出表头属性;
- 颜色:浅灰(#6c757d),避免与日期数字混淆。
- .calendar-days 日期网格:
- 布局:grid-template-columns: repeat(7, 1fr),与星期栏对齐;
- 视觉:灰色间距(gap: 1px)+ 边框(1px solid #eaeaea),使每个日期格子独立,视觉清晰。
- .day 日期格子:
- 尺寸:min-height: 80px,确保有足够空间显示日期与事件;
- 视觉:白色背景,overflow: hidden 防止事件文本溢出;
- 交互:cursor: pointer(提示可点击)、transition: all 0.2s ease,hover 时背景变浅灰(#f8f9fa)+ 缩放(scale(1.02))+ 阴影(0 4px 8px rgba(0,0,0,0.1)),模拟 "浮起" 效果,提升交互感。
- .day-number 日期数字:
- 字体:1rem、600 字重,突出日期标识;
- 间距:margin-bottom: 3px,与下方事件标签分隔。
- .event 事件标签:
- 基础样式:浅灰背景(#e9ecef)、圆角 3px、内边距 2px 4px、0.7rem 字体,紧凑显示事件标题;
- 文本处理:overflow: hidden+text-overflow: ellipsis+white-space: nowrap,确保长标题不换行,用省略号显示;
- 分类样式:important(黄背景#ffeaa7+ 橙文字#d35400)、anniversary(粉背景#fab1a0+ 红文字#e84393),用颜色区分事件优先级。
- .today 今日标记:
- 背景:浅蓝(#e3f2fd),与普通日期区分;
- 边框:2px solid 浅蓝(#4facfe),强化 "今日" 视觉标识。
- .other-month 非当月日期:
- 颜色:浅灰(#adb5bd),降低视觉权重,避免干扰当月日期;
- 背景:浅灰(#f8f9fa),进一步区分 "非当前月" 属性。
- .event-modal 事件模态框:
- 定位:fixed 全屏覆盖,z-index: 100(确保在最上层);
- 背景:半透明黑(rgba(0,0,0,0.5)),模糊背景,聚焦模态框内容;
- 布局:display: flex+justify-content: center+align-items: center,使模态框垂直居中;
- 默认状态:display: none,点击日期时显示。
- .modal-content 模态框内容区:
- 尺寸:宽度 90%(最大 500px),适配小屏幕;
- 视觉:白色背景、圆角 15px、阴影(0 10px 30px rgba(0,0,0,0.2)),卡片效果;
- 动画:animation: modalAppear 0.3s ease,从透明 + 上移 20px 到正常,提升弹出流畅度。
- .btn 功能按钮:
- 基础样式:padding: 10px 20px、无边框、圆角 8px、600 字重,transition: all 0.3s ease;
- 颜色区分:btn-primary(蓝#4facfe,添加事件)、btn-secondary(浅灰#e9ecef,取消)、btn-danger(红#ff7675,删除),用颜色暗示功能(蓝 = 确认、灰 = 取消、红 = 删除),降低操作成本。
JavaScript
- 初始化与数据定义
js
// 1. 状态变量:当前显示的日期(默认当前系统日期)、事件数据(从localStorage读取,无则初始化为空对象)
let currentDate = new Date();
let events = JSON.parse(localStorage.getItem('calendarEvents')) || {};
// 2. DOM元素获取:核心交互元素(日期网格、导航按钮、模态框、表单等)
const calendarDays = document.getElementById('calendar-days');
const currentMonthYear = document.getElementById('current-month-year');
const prevMonthBtn = document.getElementById('prev-month');
const nextMonthBtn = document.getElementById('next-month');
const todayBtn = document.getElementById('today-btn');
const eventModal = document.getElementById('event-modal');
const modalDate = document.getElementById('modal-date');
const eventList = document.getElementById('event-list');
const eventForm = document.getElementById('event-form');
const closeModal = document.getElementById('close-modal');
const cancelEvent = document.getElementById('cancel-event');
// 3. 初始化入口:页面加载完成后执行,渲染日历并绑定事件
initCalendar();
- 日历渲染核心逻辑(renderCalendar)
负责生成日历的所有日期格子(上月剩余天数、当月天数、下月开头天数),并添加状态标记(今日、非当月)与事件标签:
js
function renderCalendar(date) {
// 1. 清空现有日期格子,避免重复渲染
calendarDays.innerHTML = '';
// 2. 更新当前年月显示(如"2024年5月")
currentMonthYear.textContent = `${date.getFullYear()}年 ${date.getMonth() + 1}月`;
// 3. 计算关键日期:当月第一天、当月最后一天、上个月最后一天、当月第一天是星期几
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); // 当月1号
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0); // 当月最后一天
const firstDayIndex = firstDay.getDay(); // 当月1号是星期几(0=周日)
const prevLastDay = new Date(date.getFullYear(), date.getMonth(), 0).getDate(); // 上月最后一天日期
const nextDays = 7 - lastDay.getDay() - 1; // 下月需要显示的开头天数(补满7列网格)
// 4. 辅助函数:判断某日期是否为今天
const today = new Date();
const isToday = (day, month, year) => {
return day === today.getDate() && month === today.getMonth() && year === today.getFullYear();
};
// 5. 添加上月剩余天数(灰色显示,非当月)
for (let i = firstDayIndex; i > 0; i--) {
const dayElement = document.createElement('div');
dayElement.className = 'day other-month'; // 标记为非当月
dayElement.innerHTML = `<div class="day-number">${prevLastDay - i + 1}</div>`;
calendarDays.appendChild(dayElement);
}
// 6. 添加当月天数(核心部分,包含事件标签与今日标记)
for (let i = 1; i <= lastDay.getDate(); i++) {
const dayElement = document.createElement('div');
dayElement.className = 'day';
// 标记"今日"
if (isToday(i, date.getMonth(), date.getFullYear())) {
dayElement.classList.add('today');
}
// 添加日期数字
dayElement.innerHTML = `<div class="day-number">${i}</div>`;
// 添加当前日期的事件标签(从events对象读取)
const dateKey = formatDateKey(new Date(date.getFullYear(), date.getMonth(), i)); // 格式化为YYYY-MM-DD作为键
if (events[dateKey]) { // 若该日期有事件
events[dateKey].forEach(event => {
const eventElement = document.createElement('div');
eventElement.className = `event ${event.type}`; // 按事件类型添加类
eventElement.textContent = event.title; // 显示事件标题
dayElement.appendChild(eventElement);
});
}
// 绑定点击事件:打开事件模态框(传入当前日期)
dayElement.addEventListener('click', () => openEventModal(new Date(date.getFullYear(), date.getMonth(), i)));
calendarDays.appendChild(dayElement);
}
// 7. 添加下月开头天数(灰色显示,非当月)
for (let i = 1; i <= nextDays; i++) {
const dayElement = document.createElement('div');
dayElement.className = 'day other-month'; // 标记为非当月
dayElement.innerHTML = `<div class="day-number">${i}</div>`;
calendarDays.appendChild(dayElement);
}
}
- 事件监听与交互逻辑(setupEventListeners) 绑定所有用户操作的事件,实现日历切换、模态框控制、事件添加 / 删除:
js
function setupEventListeners() {
// 1. 上月切换:当前日期减1个月,重新渲染
prevMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar(currentDate);
});
// 2. 下月切换:当前日期加1个月,重新渲染
nextMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar(currentDate);
});
// 3. 今日定位:重置currentDate为当前系统日期,重新渲染
todayBtn.addEventListener('click', () => {
currentDate = new Date();
renderCalendar(currentDate);
});
// 4. 关闭模态框:隐藏模态框并重置表单
closeModal.addEventListener('click', closeEventModal);
cancelEvent.addEventListener('click', closeEventModal);
// 5. 提交事件表单:添加新事件(阻止默认表单提交,自定义逻辑)
eventForm.addEventListener('submit', addEvent);
}
- 事件模态框控制(openEventModal/closeEventModal)
处理模态框的显示 / 隐藏,加载当前日期的事件列表:
js
// 打开模态框:传入当前点击的日期,显示日期信息与事件列表
function openEventModal(date) {
// 1. 更新模态框标题(如"2024年5月20日")
modalDate.textContent = `${date.getFullYear()}年 ${date.getMonth() + 1}月 ${date.getDate()}日`;
// 2. 加载当前日期的事件列表
const dateKey = formatDateKey(date);
displayEvents(dateKey);
// 3. 存储当前日期的key到表单(用于后续添加事件)
eventForm.dataset.date = dateKey;
// 4. 显示模态框
eventModal.style.display = 'flex';
}
// 关闭模态框:隐藏模态框并重置表单内容
function closeEventModal() {
eventModal.style.display = 'none';
eventForm.reset();
}
- 事件列表显示与删除(displayEvents/deleteEvent)
动态生成事件列表,支持删除事件并更新本地存储:
js
// 显示当前日期的事件列表
function displayEvents(dateKey) {
eventList.innerHTML = ''; // 清空现有列表
// 1. 无事件时显示提示
if (!events[dateKey] || events[dateKey].length === 0) {
eventList.innerHTML = '<div class="no-events">该日期暂无事件</div>';
return;
}
// 2. 有事件时,遍历生成事件项
events[dateKey].forEach((event, index) => {
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
// 根据事件类型生成标签(重要/纪念日)
const typeTag = event.type === 'anniversary' ?
'<span class="event-type-tag anniversary-tag">纪念日</span>' :
event.type === 'important' ?
'<span class="event-type-tag important-tag">重要</span>' : '';
// 事件项内容:标题+标签+描述+删除按钮
eventItem.innerHTML = `
<div class="event-details">
<h4>${event.title} ${typeTag}</h4>
<p>${event.description || '无描述'}</p>
</div>
<div class="event-actions">
<button class="btn btn-danger delete-event" data-index="${index}">删除</button>
</div>
`;
eventList.appendChild(eventItem);
});
// 3. 绑定删除按钮事件:删除对应索引的事件
document.querySelectorAll('.delete-event').forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index); // 获取事件索引
deleteEvent(dateKey, index); // 执行删除
});
});
}
// 删除事件:从events对象中移除,更新本地存储,重新渲染
function deleteEvent(dateKey, index) {
if (events[dateKey] && events[dateKey][index]) {
// 1. 从数组中删除该事件
events[dateKey].splice(index, 1);
// 2. 若该日期无事件,删除对应的dateKey(优化存储)
if (events[dateKey].length === 0) {
delete events[dateKey];
}
// 3. 保存到localStorage,持久化数据
localStorage.setItem('calendarEvents', JSON.stringify(events));
// 4. 更新事件列表显示
displayEvents(dateKey);
// 5. 重新渲染日历,更新日期格子中的事件标签
renderCalendar(currentDate);
}
}
- 事件添加与数据持久化(addEvent/formatDateKey)
处理表单提交,添加新事件并保存到本地存储:
js
// 添加事件:从表单获取数据,更新events对象,保存到本地存储
function addEvent(e) {
e.preventDefault(); // 阻止表单默认提交行为
// 1. 获取表单数据
const dateKey = eventForm.dataset.date; // 从表单获取当前日期的key
const title = document.getElementById('event-title').value.trim(); // 事件标题(去空格)
const description = document.getElementById('event-description').value.trim(); // 事件描述
const type = document.getElementById('event-type').value; // 事件类型(普通/重要/纪念日)
// 2. 校验:标题不能为空
if (!title) return;
// 3. 创建新事件对象
const newEvent = { title, description, type };
// 4. 添加到events对象(若该日期无事件,先初始化数组)
if (!events[dateKey]) {
events[dateKey] = [];
}
events[dateKey].push(newEvent);
// 5. 保存到localStorage,持久化数据(刷新页面不丢失)
localStorage.setItem('calendarEvents', JSON.stringify(events));
// 6. 更新事件列表显示
displayEvents(dateKey);
// 7. 重新渲染日历,显示新添加的事件标签
renderCalendar(currentDate);
// 8. 重置表单并关闭模态框
eventForm.reset();
closeEventModal();
}
// 格式化日期key:转为YYYY-MM-DD格式,作为events对象的键(统一格式,避免歧义)
function formatDateKey(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份补0(如5月→05)
const day = String(date.getDate()).padStart(2, '0'); // 日期补0(如3日→03)
return `${year}-${month}-${day}`;
}
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!