在个人时间管理需求持续攀升的背景下,简洁高效的日程管理系统,是帮助用户合理规划每日任务的核心工具。通过整合日历视图与日程列表的卡片式界面,用户可兼顾月度日程全局概览与当日任务细节查看,既能快速洞悉日程分布、便捷添加重要事项,更能以直观的交互设计,提升日程管理效率。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现日程卡片。效果演示
这个日程管理卡片提供了完整的日程管理功能。用户可以在日历中切换月份,点击日期选择特定日期,在下方的任务列表中查看该日期的安排。用户可以添加新任务,包括标题、时间和描述,也可以删除现有任务。当某一天有任务时,日历中的日期会显示一个小圆点提示。
页面结构
日历区域
<div class="calendar"> <div class="calendar-header"> <div class="month-year" id="monthYear"></div> <div class="btn-group"> <button class="btn-round nav-btn" id="prevMonth">‹</button> <button class="btn-round nav-btn" id="nextMonth">›</button> </div> </div> <div class="calendar-grid" id="calendar"></div></div>
任务列表区域
显示当前选择日期的任务。
<div class="tasks-title"> <div>任务安排 <span class="tasks-count" id="tasksCount">0</span></div> <div class="btn-group"> <button class="btn-round opt-btn" id="todayBtn" style="display: none; margin-right: 8px;">今</button> <button class="btn-round opt-btn" id="addBtn">+</button> </div></div><div class="task-list" id="taskList"><div class="no-tasks">暂无安排</div></div>
表单弹窗
<div class="overlay" id="overlay"></div><div class="form" id="form"> <h4>添加任务</h4> <form id="taskForm"> <div class="form-group"> <label>任务标题</label> <input type="text" id="taskTitle" required> </div> <div class="form-group"> <label>时间</label> <input type="time" id="taskTime" required> </div> <div class="form-group"> <label>描述</label> <textarea id="taskDesc"></textarea> </div> <div class="form-buttons"> <button type="button" class="btn btn-secondary" id="cancelBtn">取消</button> <button type="submit" class="btn btn-primary">添加</button> </div> </form></div>
核心功能实现
日历渲染机制
日历渲染的核心是计算每个月的日期网格,包括显示前一个月和后一个月的日期以填满网格。ScheduleCard 类的 renderCalendar 方法处理这个逻辑,首先生成星期标题,然后计算当月第一天是星期几,从而确定起始日期,最后生成42个日期格子。renderCalendar() { const calendar = document.getElementById('calendar'); const monthYear = document.getElementById('monthYear'); const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; monthYear.textContent = `${this.currentDate.getFullYear()}年${monthNames[this.currentDate.getMonth()]}`; calendar.innerHTML = this.getWeekdayElements(); const firstDay = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1); const startDate = new Date(firstDay); const dayOfWeek = firstDay.getDay(); const offset = dayOfWeek === 0 ? 6 : dayOfWeek - 1; startDate.setDate(startDate.getDate() - offset); const days = []; for (let i = 0; i < 42; i++) { const date = new Date(startDate); date.setDate(startDate.getDate() + i); days.push(this.createDayElement(date)); } calendar.innerHTML += days.join(''); const todayBtn = document.getElementById('todayBtn'); const today = new Date(); todayBtn.style.display = this.isSameDate(this.selectedDate, today) ? 'none' : 'block';}
任务数据管理
任务数据通过 localStorage 进行持久化存储,使用 loadTasks 和 saveTasks 方法管理数据。ScheduleCard 类初始化时加载存储的任务数据,如果不存在则使用默认示例数据。saveTasks() { localStorage.setItem('scheduleTasks', JSON.stringify(this.tasks));}
loadTasks() { const saved = localStorage.getItem('scheduleTasks'); return saved ? JSON.parse(saved) : this.getDefaultTasks();}
日期选择与任务显示
selectDate 方法处理日期选择逻辑,更新选中日期并重新渲染日历和任务列表。renderTasks 方法根据当前选择的日期过滤任务并显示在列表中。selectDate(date) { this.selectedDate = new Date(date); this.renderCalendar(); this.renderTasks();}
renderTasks() { const taskList = document.getElementById('taskList'); const tasksCount = document.getElementById('tasksCount'); const dateStr = this.formatDate(this.selectedDate); const dayTasks = this.tasks.filter(task => task.date === dateStr); tasksCount.textContent = dayTasks.length; if (dayTasks.length === 0) { taskList.innerHTML = '<div class="no-tasks">暂无安排</div>'; return; } dayTasks.sort((a, b) => a.time.localeCompare(b.time)); taskList.innerHTML = dayTasks.map(task => `<div class="task-item" data-task-id="${task.id}"> <button class="btn-round delete-btn">×</button> <div class="task-time">${task.time}</div> <div class="task-title">${task.title}</div> ${task.description ? `<div class="task-desc">${task.description}</div>` : ''} </div>`).join('');}
扩展建议
添加任务编辑功能,允许修改已创建的任务
实现任务分类标签,支持不同颜色标识
添加任务搜索功能,快速查找特定任务
添加重复任务功能,支持周期性任务设置
完整代码
git地址:https://gitee.com/ironpro/hjdemo/blob/master/schedule/index.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; } body { background: #f5f5f5; min-height: 100vh; padding: 20px; } .container { margin: 0 auto; background: white; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); overflow: hidden; width: 320px; } .header { background: #0086f6; color: white; text-align: center; padding: 10px 0; } .header h1 { font-size: 18px; font-weight: 600; } .calendar { padding: 16px; background: white; color: #333; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .month-year { font-size: 16px; font-weight: 600; color: #1e293b; } .btn-group { display: flex; } .btn-round { border-radius: 50%; cursor: pointer; font-size: 14px; border: none; } .nav-btn { border: 1px solid #e2e8f0; background: #f1f5f9; color: #64748b; width: 28px; height: 28px; margin: 0 4px; transition: all 0.2s; } .nav-btn:hover { background: #e2e8f0; color: #475569; } .opt-btn { width: 32px; height: 32px; font-size: 16px; background: #f1f5f9; color: #64748b; border: 1px solid #e2e8f0; transition: all 0.2s; } .opt-btn:hover { background: #e2e8f0; color: #475569; } .delete-btn { position: absolute; top: 5px; right: 5px; width: 20px; height: 20px; color: #ef4444; font-size: 16px; background: none; border: none; cursor: pointer; transition: all 0.2s; } .delete-btn:hover { transform: scale(1.1); color: #dc2626; } .btn { padding: 6px 14px; border: none; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } .btn-primary { background: #3b82f6; color: white; } .btn-primary:hover { background: #2563eb; } .btn-secondary { background: #e2e8f0; color: #64748b; } .btn-secondary:hover { background: #cbd5e1; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; } .weekday { text-align: center; font-size: 11px; padding: 4px 0; font-weight: 600; color: #64748b; } .day { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border-radius: 8px; cursor: pointer; font-size: 14px; position: relative; font-weight: 500; color: #1e293b; transition: all 0.2s; } .day:hover:not(.other-month):not(.disabled) { background: #bfe0fc; color: #0f172a; } .day.other-month { opacity: 0.5; color: #94a3b8; cursor: default; background: none; } .day.today { background: transparent; color: #0086f6; font-weight: 700; border: 1px solid #0086f6; } .day.selected { background: #0086f6; color: white; font-weight: 700; } .day.has-task::after { content: ''; position: absolute; bottom: 3px; width: 5px; height: 5px; background: #f59e0b; border-radius: 50%; } .tasks { padding: 16px; display: flex; flex-direction: column; } .tasks-title { display: flex; justify-content: space-between; align-items: center; font-size: 14px; font-weight: 600; color: #1e293b; padding: 6px 14px; background-color: #f8fafc; border-top: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0; } .tasks-count { background: #dbeafe; color: #1d4ed8; font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; } .task-item { padding: 14px; margin-bottom: 8px; background: #f8fafc; border-radius: 8px; position: relative; border: 1px solid #e2e8f0; transition: all 0.2s; } .task-item:hover { background: #f1f5f9; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); } .task-time { font-size: 14px; color: #3b82f6; margin-bottom: 4px; font-weight: 600; } .task-title { font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .task-desc { font-size: 14px; color: #64748b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .no-tasks { text-align: center; padding: 20px 10px; color: #94a3b8; font-size: 13px; background: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 8px; margin-top: 8px; } .task-list { flex: 1; height: 300px; overflow-y: auto; padding: 8px; } .form { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 24px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); z-index: 1000; width: 340px; display: none; border: 1px solid #e2e8f0; } .form h4 { margin-bottom: 16px; color: #1e293b; font-size: 18px; font-weight: 600; } .form-group { margin-bottom: 16px; } .form-group label { display: block; margin-bottom: 6px; font-size: 13px; color: #334155; font-weight: 500; } .form-group input, .form-group textarea { width: 100%; padding: 10px 14px; border: 1px solid #cbd5e1; font-size: 14px; background: #fff; transition: all 0.2s; } .form-group input:focus, .form-group textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } .form-group textarea { resize: vertical; min-height: 60px; } .form-buttons { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; } .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.4); z-index: 999; display: none; }
</style></head><body><div class="container"> <div class="header"><h1>我的任务</h1></div> <div class="schedule-card"> <div class="calendar"> <div class="calendar-header"> <div class="month-year" id="monthYear"></div> <div class="btn-group"> <button class="btn-round nav-btn" id="prevMonth">‹</button> <button class="btn-round nav-btn" id="nextMonth">›</button> </div> </div> <div class="calendar-grid" id="calendar"></div> </div> <div class="tasks-title"> <div>任务安排 <span class="tasks-count" id="tasksCount">0</span></div> <div class="btn-group"> <button class="btn-round opt-btn" id="todayBtn" style="display: none; margin-right: 8px;">今</button> <button class="btn-round opt-btn" id="addBtn">+</button> </div> </div> <div class="task-list" id="taskList"><div class="no-tasks">暂无安排</div></div> </div></div><div class="overlay" id="overlay"></div><div class="form" id="form"> <h4>添加任务</h4> <form id="taskForm"> <div class="form-group"> <label>任务标题</label> <input type="text" id="taskTitle" required> </div> <div class="form-group"> <label>时间</label> <input type="time" id="taskTime" required> </div> <div class="form-group"> <label>描述</label> <textarea id="taskDesc"></textarea> </div> <div class="form-buttons"> <button type="button" class="btn btn-secondary" id="cancelBtn">取消</button> <button type="submit" class="btn btn-primary">添加</button> </div> </form></div>
<script> class ScheduleCard { constructor() { this.currentDate = new Date(); this.selectedDate = new Date(); this.tasks = this.loadTasks(); this.init(); }
init() { this.renderCalendar(); this.renderTasks(); this.bindEvents(); }
bindEvents() { document.getElementById('prevMonth').addEventListener('click', () => this.changeMonth(-1)); document.getElementById('nextMonth').addEventListener('click', () => this.changeMonth(1)); document.getElementById('todayBtn').addEventListener('click', () => this.goToToday()); document.getElementById('addBtn').addEventListener('click', () => this.showForm()); document.getElementById('cancelBtn').addEventListener('click', () => this.hideForm()); document.getElementById('overlay').addEventListener('click', () => this.hideForm()); document.getElementById('taskForm').addEventListener('submit', (e) => this.addTask(e));
document.getElementById('taskList').addEventListener('click', (e) => { if (e.target.classList.contains('delete-btn')) { const taskItem = e.target.closest('.task-item'); const taskId = parseInt(taskItem.dataset.taskId); this.deleteTask(taskId); } });
document.getElementById('calendar').addEventListener('click', (e) => { if (e.target.classList.contains('day') && !e.target.classList.contains('other-month')) { const day = parseInt(e.target.textContent); const date = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), day); this.selectDate(date); } }); }
changeMonth(direction) { this.currentDate.setMonth(this.currentDate.getMonth() + direction); this.renderCalendar(); }
goToToday() { this.currentDate = this.selectedDate = new Date(); this.renderCalendar(); this.renderTasks(); }
renderCalendar() { const calendar = document.getElementById('calendar'); const monthYear = document.getElementById('monthYear'); const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; monthYear.textContent = `${this.currentDate.getFullYear()}年${monthNames[this.currentDate.getMonth()]}`; calendar.innerHTML = this.getWeekdayElements(); const firstDay = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1); const startDate = new Date(firstDay); const dayOfWeek = firstDay.getDay(); const offset = dayOfWeek === 0 ? 6 : dayOfWeek - 1; startDate.setDate(startDate.getDate() - offset); const days = []; for (let i = 0; i < 42; i++) { const date = new Date(startDate); date.setDate(startDate.getDate() + i); days.push(this.createDayElement(date)); } calendar.innerHTML += days.join(''); const todayBtn = document.getElementById('todayBtn'); const today = new Date(); todayBtn.style.display = this.isSameDate(this.selectedDate, today) ? 'none' : 'block'; }
getWeekdayElements() { const weekdays = ['一', '二', '三', '四', '五', '六', '日']; return weekdays.map(day => `<div class="weekday">${day}</div>`).join(''); }
createDayElement(date) { const today = new Date(); const isCurrentMonth = date.getMonth() === this.currentDate.getMonth(); const isToday = this.isSameDate(date, today); const isSelected = this.isSameDate(date, this.selectedDate); const hasTask = this.hasTasks(date); const classes = ['day', !isCurrentMonth && 'other-month', isToday && 'today', isSelected && 'selected', hasTask && 'has-task'].filter(Boolean).join(' '); return `<div class="${classes}">${date.getDate()}</div>`; }
selectDate(date) { this.selectedDate = new Date(date); this.renderCalendar(); this.renderTasks(); }
renderTasks() { const taskList = document.getElementById('taskList'); const tasksCount = document.getElementById('tasksCount'); const dateStr = this.formatDate(this.selectedDate); const dayTasks = this.tasks.filter(task => task.date === dateStr); tasksCount.textContent = dayTasks.length; if (dayTasks.length === 0) { taskList.innerHTML = '<div class="no-tasks">暂无安排</div>'; return; } dayTasks.sort((a, b) => a.time.localeCompare(b.time)); taskList.innerHTML = dayTasks.map(task => `<div class="task-item" data-task-id="${task.id}"> <button class="btn-round delete-btn">×</button> <div class="task-time">${task.time}</div> <div class="task-title">${task.title}</div> ${task.description ? `<div class="task-desc">${task.description}</div>` : ''} </div>`).join(''); }
showForm() { document.getElementById('overlay').style.display = 'block'; document.getElementById('form').style.display = 'block'; }
hideForm() { document.getElementById('overlay').style.display = 'none'; document.getElementById('form').style.display = 'none'; document.getElementById('taskForm').reset(); }
addTask(e) { e.preventDefault(); const title = document.getElementById('taskTitle').value; const time = document.getElementById('taskTime').value; const description = document.getElementById('taskDesc').value; const newTask = { id: Date.now(), title, date: this.formatDate(this.selectedDate), time, description }; this.tasks.push(newTask); this.saveTasks(); this.hideForm(); this.renderCalendar(); this.renderTasks(); }
deleteTask(taskId) { if (confirm('确定要删除这个任务吗?')) { this.tasks = this.tasks.filter(task => task.id !== taskId); this.saveTasks(); this.renderCalendar(); this.renderTasks(); } }
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}`; }
isSameDate(date1, date2) { return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); }
hasTasks(date) { const dateStr = this.formatDate(date); return this.tasks.some(task => task.date === dateStr); }
saveTasks() { localStorage.setItem('scheduleTasks', JSON.stringify(this.tasks)); }
loadTasks() { const saved = localStorage.getItem('scheduleTasks'); return saved ? JSON.parse(saved) : this.getDefaultTasks(); }
getDefaultTasks() { const today = new Date(); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); return [ { id: 1, title: '晨会', date: this.formatDate(today), time: '09:00', description: '团队日常晨会' }, { id: 2, title: '项目评审', date: this.formatDate(today), time: '14:00', description: 'Q4项目评审会议' }, { id: 3, title: '健身', date: this.formatDate(tomorrow), time: '18:00', description: '游泳一小时' } ]; } } new ScheduleCard();</script></body></html>
阅读原文:https://mp.weixin.qq.com/s/eMdLiVG9iiPDt6Y12yBDbA
该文章在 2026/1/9 11:41:46 编辑过