目录
专栏导读
🌸 欢迎来到Python办公自动化专栏—Python处理办公问题,解放您的双手
🏳️🌈 博客主页:请点击——> 一晌小贪欢的博客主页求关注
👍 该系列文章专栏:请点击——>Python办公自动化专栏求订阅
🕷 此外还有爬虫专栏:请点击——>Python爬虫基础专栏求订阅
📕 此外还有python基础专栏:请点击——>Python基础学习专栏求订阅
文章作者技术和水平有限,如果文中出现错误,希望大家能指正🙏
❤️ 欢迎各位佬关注! ❤️
项目简介
-
这是一个功能完整的日历计划表应用,采用纯前端技术栈开发,支持活动的增删改查,并具备智能的悬停提示功能。用户可以直观地查看每日活动安排,通过鼠标悬停快速预览活动详情。
功能特性
🗓️ 核心功能
- 月份切换:支持前后月份的快速切换浏览
- 日期选择:点击日期可选中并设置为活动日期
- 活动管理:完整的CRUD操作(创建、读取、更新、删除)
- 今日高亮:自动标识当前日期
- 活动指示器:有活动的日期显示红色圆点标记
✨ 特色功能
- 悬停提示:鼠标悬停在有活动的日期上时,显示活动详情提示框
- 本地存储:使用localStorage实现数据持久化
- 响应式设计:适配不同屏幕尺寸的设备
- 优雅动画:流畅的过渡效果和交互反馈
技术实现
技术栈
- HTML5:语义化标签构建页面结构
- CSS3:现代样式特性,包括Grid布局、动画效果
- JavaScript ES6+:面向对象编程,模块化代码组织
核心技术点
1. 日历渲染算法
renderCalendar() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
// 获取月份第一天和最后一天
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
// 生成42天(6周)的完整日历视图
for (let i = 0; i < 42; i++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + i);
const dayElement = this.createDayElement(date, month);
calendarDays.appendChild(dayElement);
}
}
2. 悬停提示功能实现
addHoverTooltip(dayElement, events) {
let tooltip = null;
dayElement.addEventListener('mouseenter', (e) => {
// 创建提示框
tooltip = document.createElement('div');
tooltip.className = 'event-tooltip';
const tooltipContent = events.map(event => {
return `<div class="tooltip-event">
<div class="tooltip-title">${event.title}</div>
<div class="tooltip-time">${event.time || '时间未设置'}</div>
</div>`;
}).join('');
tooltip.innerHTML = tooltipContent;
document.body.appendChild(tooltip);
// 智能定位提示框
const rect = dayElement.getBoundingClientRect();
tooltip.style.left = rect.left + rect.width / 2 + 'px';
tooltip.style.top = rect.top - 10 + 'px';
});
dayElement.addEventListener('mouseleave', () => {
if (tooltip) {
document.body.removeChild(tooltip);
tooltip = null;
}
});
}
3. CSS动画与样式设计
.event-tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 10px;
border-radius: 8px;
font-size: 12px;
z-index: 1000;
max-width: 200px;
transform: translateX(-50%) translateY(-100%);
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
animation: tooltipFadeIn 0.2s ease;
}
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-100%) scale(0.9);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(-100%) scale(1);
}
}
项目结构
日历计划表/
├── index.html # 主页面结构
├── style.css # 样式文件
├── script.js # 核心逻辑
开发亮点
1. 面向对象设计
- 采用ES6 Class语法,将日历功能封装在
CalendarApp
类中,代码结构清晰,易于维护和扩展。
2. 事件驱动架构
- 通过事件监听器实现用户交互,包括点击、悬停、表单提交等操作的响应处理。
3. 数据持久化
- 使用
localStorage
实现客户端数据存储,确保用户数据在浏览器关闭后仍能保持。
4. 响应式布局
- 采用CSS Grid和Flexbox布局,配合媒体查询实现多设备适配。
5. 用户体验优化
- 平滑的动画过渡效果
- 直观的视觉反馈
- 智能的提示框定位
- 优雅的交互设计
使用方法
- 添加活动:填写活动表单,点击"添加活动"按钮
- 查看活动:点击有红点标记的日期查看详情,或将鼠标悬停快速预览
- 删除活动:点击活动详情中的"删除活动"按钮
- 切换月份:使用左右箭头按钮浏览不同月份
技术特色
智能提示框定位
提示框会根据日期元素的位置自动调整显示位置,确保在任何情况下都能完整显示内容。
优雅的动画效果
- 日期悬停时的缩放效果
- 提示框的淡入动画
- 按钮的悬停反馈
- 模态框的滑入效果
现代化UI设计
- 渐变背景和阴影效果
- 圆角设计语言
- 合理的色彩搭配
- 清晰的信息层级
浏览器兼容性
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
未来扩展
- 添加活动分类和颜色标记
- 支持重复活动设置
- 集成提醒功能
- 数据导入导出
- 多语言支持
总结
-
这个日历计划表项目展示了现代前端开发的最佳实践,通过合理的架构设计、优雅的用户界面和流畅的交互体验,为用户提供了一个实用且美观的日程管理工具。悬停提示功能的加入,进一步提升了用户体验,使得活动信息的查看更加便捷高效。
技术栈:HTML5 + CSS3 + JavaScript ES6+
源码
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>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>我的日历计划表</h1>
</header>
<div class="calendar-controls">
<button id="prevMonth"><</button>
<h2 id="currentMonth"></h2>
<button id="nextMonth">></button>
</div>
<div class="calendar-grid">
<div class="weekdays">
<div>日</div>
<div>一</div>
<div>二</div>
<div>三</div>
<div>四</div>
<div>五</div>
<div>六</div>
</div>
<div id="calendarDays" class="days"></div>
</div>
<div class="event-section">
<h3>添加活动</h3>
<form id="eventForm">
<input type="date" id="eventDate" required>
<input type="text" id="eventTitle" placeholder="活动标题" required>
<input type="time" id="eventTime">
<textarea id="eventDescription" placeholder="活动描述"></textarea>
<button type="submit">添加活动</button>
</form>
</div>
<div class="events-list">
<h3>今日活动</h3>
<div id="todayEvents"></div>
</div>
</div>
<!-- 活动详情模态框 -->
<div id="eventModal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h3>活动详情</h3>
<div id="eventDetails"></div>
<button id="deleteEvent">删除活动</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
script.js
class CalendarApp {
constructor() {
this.currentDate = new Date();
this.selectedDate = null;
this.events = JSON.parse(localStorage.getItem('calendarEvents')) || {};
this.init();
}
init() {
this.bindEvents();
this.renderCalendar();
this.updateTodayEvents();
}
bindEvents() {
// 月份切换
document.getElementById('prevMonth').addEventListener('click', () => {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.renderCalendar();
});
document.getElementById('nextMonth').addEventListener('click', () => {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.renderCalendar();
});
// 活动表单提交
document.getElementById('eventForm').addEventListener('submit', (e) => {
e.preventDefault();
this.addEvent();
});
// 模态框关闭
document.querySelector('.close').addEventListener('click', () => {
this.closeModal();
});
document.getElementById('eventModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('eventModal')) {
this.closeModal();
}
});
// 删除活动
document.getElementById('deleteEvent').addEventListener('click', () => {
this.deleteCurrentEvent();
});
}
renderCalendar() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
// 更新月份标题
const monthNames = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
document.getElementById('currentMonth').textContent = `${year}年 ${monthNames[month]}`;
// 获取月份第一天和最后一天
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
// 清空日历
const calendarDays = document.getElementById('calendarDays');
calendarDays.innerHTML = '';
// 生成42天(6周)
for (let i = 0; i < 42; i++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + i);
const dayElement = this.createDayElement(date, month);
calendarDays.appendChild(dayElement);
}
}
createDayElement(date, currentMonth) {
const dayDiv = document.createElement('div');
dayDiv.className = 'day';
const dayNumber = document.createElement('div');
dayNumber.className = 'day-number';
dayNumber.textContent = date.getDate();
dayDiv.appendChild(dayNumber);
// 添加样式类
if (date.getMonth() !== currentMonth) {
dayDiv.classList.add('other-month');
}
if (this.isToday(date)) {
dayDiv.classList.add('today');
}
// 检查是否有活动
const dateKey = this.formatDate(date);
if (this.events[dateKey] && this.events[dateKey].length > 0) {
const indicator = document.createElement('div');
indicator.className = 'event-indicator';
dayDiv.appendChild(indicator);
// 添加悬停显示活动详情功能
this.addHoverTooltip(dayDiv, this.events[dateKey]);
}
// 点击事件
dayDiv.addEventListener('click', () => {
this.selectDate(date);
});
return dayDiv;
}
addHoverTooltip(dayElement, events) {
let tooltip = null;
dayElement.addEventListener('mouseenter', (e) => {
// 创建提示框
tooltip = document.createElement('div');
tooltip.className = 'event-tooltip';
const tooltipContent = events.map(event => {
return `<div class="tooltip-event">
<div class="tooltip-title">${event.title}</div>
<div class="tooltip-time">${event.time || '时间未设置'}</div>
</div>`;
}).join('');
tooltip.innerHTML = tooltipContent;
document.body.appendChild(tooltip);
// 定位提示框
const rect = dayElement.getBoundingClientRect();
tooltip.style.left = rect.left + rect.width / 2 + 'px';
tooltip.style.top = rect.top - 10 + 'px';
});
dayElement.addEventListener('mouseleave', () => {
if (tooltip) {
document.body.removeChild(tooltip);
tooltip = null;
}
});
}
selectDate(date) {
// 移除之前的选中状态
document.querySelectorAll('.day.selected').forEach(day => {
day.classList.remove('selected');
});
// 添加选中状态
event.target.closest('.day').classList.add('selected');
this.selectedDate = new Date(date);
// 更新表单日期
document.getElementById('eventDate').value = this.formatDate(date);
// 显示该日期的活动
this.showDateEvents(date);
}
showDateEvents(date) {
const dateKey = this.formatDate(date);
const events = this.events[dateKey] || [];
if (events.length > 0) {
this.showEventModal(events[0], dateKey, 0);
}
}
addEvent() {
const date = document.getElementById('eventDate').value;
const title = document.getElementById('eventTitle').value;
const time = document.getElementById('eventTime').value;
const description = document.getElementById('eventDescription').value;
if (!date || !title) {
alert('请填写日期和活动标题!');
return;
}
const event = {
id: Date.now(),
title,
time,
description,
date
};
if (!this.events[date]) {
this.events[date] = [];
}
this.events[date].push(event);
this.saveEvents();
this.renderCalendar();
this.updateTodayEvents();
// 清空表单
document.getElementById('eventForm').reset();
alert('活动添加成功!');
}
showEventModal(event, dateKey, eventIndex) {
const modal = document.getElementById('eventModal');
const details = document.getElementById('eventDetails');
details.innerHTML = `
<h4>${event.title}</h4>
<p><strong>日期:</strong>${event.date}</p>
<p><strong>时间:</strong>${event.time || '未设置'}</p>
<p><strong>描述:</strong>${event.description || '无描述'}</p>
`;
// 存储当前活动信息用于删除
modal.dataset.dateKey = dateKey;
modal.dataset.eventIndex = eventIndex;
modal.style.display = 'block';
}
closeModal() {
document.getElementById('eventModal').style.display = 'none';
}
deleteCurrentEvent() {
const modal = document.getElementById('eventModal');
const dateKey = modal.dataset.dateKey;
const eventIndex = parseInt(modal.dataset.eventIndex);
if (dateKey && this.events[dateKey]) {
this.events[dateKey].splice(eventIndex, 1);
// 如果该日期没有活动了,删除该日期键
if (this.events[dateKey].length === 0) {
delete this.events[dateKey];
}
this.saveEvents();
this.renderCalendar();
this.updateTodayEvents();
this.closeModal();
alert('活动已删除!');
}
}
updateTodayEvents() {
const today = this.formatDate(new Date());
const todayEvents = this.events[today] || [];
const container = document.getElementById('todayEvents');
if (todayEvents.length === 0) {
container.innerHTML = '<p style="color: #6c757d;">今天没有安排活动</p>';
return;
}
container.innerHTML = todayEvents.map((event, index) => `
<div class="event-item" onclick="app.showEventModal(${JSON.stringify(event).replace(/"/g, '"')}, '${today}', ${index})">
<div class="event-title">${event.title}</div>
<div class="event-time">${event.time || '时间未设置'}</div>
</div>
`).join('');
}
saveEvents() {
localStorage.setItem('calendarEvents', JSON.stringify(this.events));
}
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}`;
}
isToday(date) {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
}
}
// 初始化应用
const app = new CalendarApp();
// 设置今天的日期为默认值
document.getElementById('eventDate').value = app.formatDate(new Date());
style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
header {
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
text-align: center;
padding: 20px;
}
header h1 {
font-size: 2.5em;
font-weight: 300;
}
.calendar-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.calendar-controls button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 18px;
transition: all 0.3s ease;
}
.calendar-controls button:hover {
background: #0056b3;
transform: translateY(-2px);
}
#currentMonth {
font-size: 1.8em;
color: #333;
font-weight: 400;
}
.calendar-grid {
padding: 20px;
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
margin-bottom: 10px;
}
.weekdays div {
background: #6c757d;
color: white;
text-align: center;
padding: 15px;
font-weight: bold;
font-size: 14px;
}
.days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #e9ecef;
}
.day {
background: white;
min-height: 100px;
padding: 10px;
border: 1px solid #e9ecef;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.day:hover {
background: #f8f9fa;
transform: scale(1.02);
}
.day.other-month {
background: #f8f9fa;
color: #6c757d;
}
.day.today {
background: #007bff;
color: white;
font-weight: bold;
}
.day.selected {
background: #28a745;
color: white;
}
.day-number {
font-weight: bold;
margin-bottom: 5px;
}
.event-indicator {
width: 8px;
height: 8px;
background: #dc3545;
border-radius: 50%;
position: absolute;
top: 5px;
right: 5px;
}
.event-section {
background: #f8f9fa;
padding: 30px;
border-top: 1px solid #e9ecef;
}
.event-section h3 {
margin-bottom: 20px;
color: #333;
font-size: 1.5em;
}
#eventForm {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 15px;
align-items: start;
}
#eventForm input,
#eventForm textarea {
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
#eventForm input:focus,
#eventForm textarea:focus {
outline: none;
border-color: #007bff;
}
#eventDescription {
grid-column: span 3;
resize: vertical;
min-height: 80px;
}
#eventForm button {
grid-column: span 3;
background: #28a745;
color: white;
border: none;
padding: 15px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
#eventForm button:hover {
background: #218838;
transform: translateY(-2px);
}
.events-list {
padding: 30px;
border-top: 1px solid #e9ecef;
}
.events-list h3 {
margin-bottom: 20px;
color: #333;
font-size: 1.5em;
}
.event-item {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.event-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.event-title {
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.event-time {
color: #6c757d;
font-size: 14px;
}
/* 模态框样式 */
.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: white;
margin: 15% auto;
padding: 30px;
border-radius: 15px;
width: 80%;
max-width: 500px;
position: relative;
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
position: absolute;
top: 15px;
right: 20px;
}
.close:hover {
color: #000;
}
#deleteEvent {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 15px;
transition: background 0.3s ease;
}
#deleteEvent:hover {
background: #c82333;
}
/* 悬停提示框样式 */
.event-tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 10px;
border-radius: 8px;
font-size: 12px;
z-index: 1000;
max-width: 200px;
transform: translateX(-50%) translateY(-100%);
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
animation: tooltipFadeIn 0.2s ease;
}
.event-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.9);
}
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-100%) scale(0.9);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(-100%) scale(1);
}
}
.tooltip-event {
margin-bottom: 8px;
}
.tooltip-event:last-child {
margin-bottom: 0;
}
.tooltip-title {
font-weight: bold;
margin-bottom: 2px;
}
.tooltip-time {
font-size: 11px;
color: #ccc;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 10px;
}
.calendar-controls {
padding: 15px 20px;
}
#currentMonth {
font-size: 1.4em;
}
.day {
min-height: 80px;
padding: 5px;
}
#eventForm {
grid-template-columns: 1fr;
}
#eventDescription {
grid-column: span 1;
}
#eventForm button {
grid-column: span 1;
}
.modal-content {
width: 95%;
margin: 10% auto;
}
}
-
希望对初学者有帮助;致力于办公自动化的小小程序员一枚
-
希望能得到大家的【❤️一个免费关注❤️】感谢!
-
求个 🤞 关注 🤞 +❤️ 喜欢 ❤️ +👍 收藏 👍
-
此外还有办公自动化专栏,欢迎大家订阅:Python办公自动化专栏
-
此外还有爬虫专栏,欢迎大家订阅:Python爬虫基础专栏
-
此外还有Python基础专栏,欢迎大家订阅:Python基础学习专栏