HTML&CSS:高颜值交互式日历,贴心记录每一天

该 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">&times;</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 日历主容器:
  1. 尺寸:宽度 100%(最大 900px,避免大屏幕拉伸);
  2. 视觉:白色背景(white)、圆角 15px、阴影(0 10px 30px rgba(0,0,0,0.1)),模拟卡片效果,与渐变背景形成层次;
  3. 溢出:overflow: hidden,确保内部元素不超出容器边界;
  4. 内边距:padding: 20px,隔离内部组件与容器边缘。
  • .calendar 日历标题:
  1. 字体:24px、浅蓝(#4facfe)、居中显示;
  2. 间距:margin: 5px auto,与头部导航轻微分隔,强化标识作用。
  • .calendar-header 日历头部:
  1. 布局:display: flex+justify-content: space-between+align-items: center,使年月显示与导航按钮左右分布;
  2. 视觉:渐变背景(linear-gradient(to right, #4facfe 0%, #00f2fe 100%))、白色文字、圆角(5px 5px 0 0),突出头部区域;
  3. 内边距:padding: 20px 30px,确保内容不拥挤。
  • .nav-btn 导航按钮:
  1. 形状:圆形(border-radius: 50%)、40×40px 尺寸,方便点击;
  2. 视觉:半透明白色背景(rgba(255,255,255,0.2))、白色图标 / 文字,与头部渐变背景融合;
  3. 交互:transition: all 0.3s ease,hover 时背景加深(rgba(255,255,255,0.3))+ 缩放(scale(1.05)),提供明确反馈。
  • .calendar-weekdays 星期栏:
  1. 布局:grid-template-columns: repeat(7, 1fr),7 列等宽,与日期栏对齐;
  2. 视觉:浅灰背景(#f8f9fa)、左右边框(1px solid #eaeaea),明确区分星期与日期区域;
  3. 内边距:padding: 10px 0,确保星期文本垂直居中。
  • .weekday 星期文本:
  1. 排版:居中显示、600 字重,突出表头属性;
  2. 颜色:浅灰(#6c757d),避免与日期数字混淆。
  • .calendar-days 日期网格:
  1. 布局:grid-template-columns: repeat(7, 1fr),与星期栏对齐;
  2. 视觉:灰色间距(gap: 1px)+ 边框(1px solid #eaeaea),使每个日期格子独立,视觉清晰。
  • .day 日期格子:
  1. 尺寸:min-height: 80px,确保有足够空间显示日期与事件;
  2. 视觉:白色背景,overflow: hidden 防止事件文本溢出;
  3. 交互:cursor: pointer(提示可点击)、transition: all 0.2s ease,hover 时背景变浅灰(#f8f9fa)+ 缩放(scale(1.02))+ 阴影(0 4px 8px rgba(0,0,0,0.1)),模拟 "浮起" 效果,提升交互感。
  • .day-number 日期数字:
  1. 字体:1rem、600 字重,突出日期标识;
  2. 间距:margin-bottom: 3px,与下方事件标签分隔。
  • .event 事件标签:
  1. 基础样式:浅灰背景(#e9ecef)、圆角 3px、内边距 2px 4px、0.7rem 字体,紧凑显示事件标题;
  2. 文本处理:overflow: hidden+text-overflow: ellipsis+white-space: nowrap,确保长标题不换行,用省略号显示;
  3. 分类样式:important(黄背景#ffeaa7+ 橙文字#d35400)、anniversary(粉背景#fab1a0+ 红文字#e84393),用颜色区分事件优先级。
  • .today 今日标记:
  1. 背景:浅蓝(#e3f2fd),与普通日期区分;
  2. 边框:2px solid 浅蓝(#4facfe),强化 "今日" 视觉标识。
  • .other-month 非当月日期:
  1. 颜色:浅灰(#adb5bd),降低视觉权重,避免干扰当月日期;
  2. 背景:浅灰(#f8f9fa),进一步区分 "非当前月" 属性。
  • .event-modal 事件模态框:
  1. 定位:fixed 全屏覆盖,z-index: 100(确保在最上层);
  2. 背景:半透明黑(rgba(0,0,0,0.5)),模糊背景,聚焦模态框内容;
  3. 布局:display: flex+justify-content: center+align-items: center,使模态框垂直居中;
  4. 默认状态:display: none,点击日期时显示。
  • .modal-content 模态框内容区:
  1. 尺寸:宽度 90%(最大 500px),适配小屏幕;
  2. 视觉:白色背景、圆角 15px、阴影(0 10px 30px rgba(0,0,0,0.2)),卡片效果;
  3. 动画:animation: modalAppear 0.3s ease,从透明 + 上移 20px 到正常,提升弹出流畅度。
  • .btn 功能按钮:
  1. 基础样式:padding: 10px 20px、无边框、圆角 8px、600 字重,transition: all 0.3s ease;
  2. 颜色区分:btn-primary(蓝#4facfe,添加事件)、btn-secondary(浅灰#e9ecef,取消)、btn-danger(红#ff7675,删除),用颜色暗示功能(蓝 = 确认、灰 = 取消、红 = 删除),降低操作成本。

JavaScript

  1. 初始化与数据定义
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();
  1. 日历渲染核心逻辑(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);
    }
}
  1. 事件监听与交互逻辑(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);
}
  1. 事件模态框控制(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();
}
  1. 事件列表显示与删除(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);
    }
}
  1. 事件添加与数据持久化(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}`;
}

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

相关推荐
一只专注做软件的湖南人2 小时前
京东商品评论接口(jingdong.ware.comment.get)技术解析:数据拉取与情感分析优化
前端·后端·api
千码君20162 小时前
React Native:使用vite创建react项目并熟悉react语法
javascript·css·react native·react.js·html·vite·jsx
刺客_Andy2 小时前
React 第三十八节 Router 中useRoutes 的使用详解及注意事项
前端·react.js
刺客_Andy2 小时前
React 第三十六节 Router 中 useParams 的具体使用及详细介绍
前端·react.js
刺客_Andy2 小时前
React 第三十九节 React Router 中的 unstable_usePrompt Hook的详细用法及案例
前端
薄雾晚晴2 小时前
大屏开发实战:自定义原子样式,用 Less 混合自动生成间距类,告别重复样式代码
前端·css·vue.js
HYI2 小时前
vue3 作用域插槽下不能通过ref获取多个实例的坑
javascript·vue.js
进阶的鱼2 小时前
注意!使用props给子组件传参需要多想一步
前端·javascript·react.js
我是天龙_绍3 小时前
什么时候用ref,什么时候用reactive?
前端