Python全栈开发项目实战——日历事件管理系统
文章目录
- 一:项目技术栈和代码分析
- 1.前端技术栈
- (1)HTML(index.html):
- (2)CSS(styles.css):
- (3)JavaScript(scripts.js):
- 2.后端技术栈
- (1)Python(Flask 框架):
- 3.数据存储
- 二:项目功能分析
- 1.前端功能
- (1)日历视图
- (2)事件添加
- (3)事件显示
- (4)事件删除
- 2.后端功能
- 3.功能完整性分析
- 三:项目改进方向
- 四:文件夹和文件说明
- 1.project_directory:
- 2.app.py:
- 3.events.json:
- 4.static 文件夹:
- 5.templates 文件夹:
- 五:项目源代码
- 1.app.py
- 2.index.html
- 3.styles.css
- 4.scripts.js
本项目是一个基于
Python Flask
(后端)+ HTML
/CSS
/JavaScript
(前端)的全栈开发项目,实现了一个日历事件管理系统,允许用户通过
Web
界面
按日期添加事件、
显示事件详情,以及
删除特定事件等亮点功能
✨✨✨完整的源代码在最后哦~前面都是对项目内容的解读,大家可以根据文章目录自行跳转✨✨✨
项目实现效果图片展示:
一:项目技术栈和代码分析
1.前端技术栈
(1)HTML(index.html):
- 用于搭建网页的基本结构,包括页面标题、表单、日历视图等
a. 文档声明与基础结构:
<!-- 声明文档类型为 HTML5 -->
<!DOCTYPE html>
<!-- 定义 HTML 文档的语言为中文 -->
<html lang="zh-CN">
<!-- 包含页面的元信息和外部资源引用 -->
<head>
<!-- 使用UTF-8编码 -->
<meta charset="UTF-8">
<!-- 适配不同屏幕尺寸(如移动设备)-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 定义浏览器标题,显示在浏览器标签上 -->
<title>日历应用</title>
<!-- 引用 CSS 样式表和 JavaScript 脚本文件,用于页面样式和动态功能 -->
<!-- {{ url_for('static', filename='styles.css') }}和{{ url_for('static', filename='scripts.js') }} 是 Flask 模板语法,引用外部CSS/JS文件 -->
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
</head>
<body>
<!-- 定义页面的主体内容 -->
</body>
</html>
b. 页面标题与主容器:
<!-- 定义一个容器,用于包裹整个页面的内容 -->
<div id="app">
<!-- 定义一级标题 "日历应用" -->
<h1>日历应用</h1>
c. 自定义年月选择器:
<!-- 定义一个容器,用于包裹自定义年份和月份选择器 -->
<div id="custom-date-selector">
<!-- 为年份选择框定义标签 -->
<label for="year">年份:</label>
<!-- 定义一个数字输入框,用于输入年份 -->
<input type="number" id="year" placeholder="年份:2025" min="1900" max="2100">
<!-- 为月份选择框定义标签 -->
<label for="month">月份:</label>
<!-- 定义一个下拉选择框,用于选择月份 -->
<select id="month">
<!-- 定义下拉选项,value 为月份的索引值(0表示一月)依此类推 -->
<option value="0">一月</option>
<option value="1">二月</option>
<option value="2">三月</option>
<option value="3">四月</option>
<option value="4">五月</option>
<option value="5">六月</option>
<option value="6">七月</option>
<option value="7">八月</option>
<option value="8">九月</option>
<option value="9">十月</option>
<option value="10">十一月</option>
<option value="11">十二月</option>
</select>
<!-- 定义一个按钮,点击后触发日历更新逻辑 -->
<button id="update-calendar">更新日历</button>
</div>
d. 日历视图:
<!-- 定义一个容器,用于动态生成和显示日历视图 -->
<div id="calendar-view">
<!-- 日历视图将在这里动态生成,内容通过 JavaScript 动态生成 -->
<!-- 初始状态为空,用户选择年份和月份后,通过脚本填充日期和事件信息 -->
</div>
e. 添加事件表单:
<!-- 定义一个表单,用于添加事件 -->
<form id="event-form">
<!-- 用户输入事件标题 -->
<input type="text" id="event-title" placeholder="事件标题">
<!-- 提供输入框供用户选择事件的日期和时间 -->
<input type="datetime-local" id="event-date">
<!-- 点击后触发表单提交逻辑,将事件保存到日历中 -->
<button type="submit">添加事件</button>
</form>
(2)CSS(styles.css):
- 用于美化页面,为日期块、事件块、删除按钮、事件表单和弹出框等样式提供视觉效果
a. 全局样式(body
和#app
):
body {
/* 设置网页字体为 Arial,若 Arial 不可用则使用 sans-serif 字体 */
font-family: Arial, sans-serif;
/* 移除网页的默认外边距和内边距,确保布局从边缘开始 */
margin: 0;
padding: 0;
/* 使用 Flexbox 布局,便于子元素的排列 */
display: flex;
/* 子元素水平居中;垂直居中;按垂直方向排列 */
justify-content: center;
align-items: center;
flex-direction: column;
/* 设置最小高度为视口高度,确保内容垂直居中 */
min-height: 100vh;
/* 设置背景颜色为浅灰色 */
background-color: #f8f9fa;
}
#app {
/* 限制 #app 容器的最大宽度为 800px */
max-width: 800px;
/* 水平居中容器(自动左右外边距) */
margin: 0 auto;
/* 容器内的文本水平居中 */
text-align: center;
}
b. 标题(h2
)和日历视图(#calendar-view
)样式:
h2 {
text-align: center;
/* 标题与下方内容之间的间距为 15px */
margin-bottom: 15px;
}
#calendar-view {
/* 使用网格布局,便于组织日期块 */
display: grid;
/* 网格项之间的间距为 5px */
gap: 5px;
/* 上下外边距为 20px,左右外边距为 0 */
margin: 20px 0;
/* 容器宽度占满父元素 */
width: 100%;
max-width: 800px;
}
c. 日历行样式(.calendar-row
):
.calendar-row {
/* 字体加粗 */
font-weight: bold;
/* 背景颜色为浅灰色 */
background-color: #ccc;
/* 内边距为 5px */
padding: 5px;
/* 设置字体大小为 18px */
font-size: 18px;
}
d. 日历日期块样式(.calendar-day
):
.calendar-day {
padding: 10px;
/* 设置 1px 的浅灰色边框 */
border: 1px solid #ccc;
/* 圆角边框,半径为 5px */
border-radius: 5px;
text-align: center;
/* 背景颜色为浅灰 */
background-color: #e9ecef;
/* 鼠标悬停时指针变为手形 */
cursor: pointer;
}
.calendar-day:hover {
/* 鼠标悬停时背景颜色变浅 */
background-color: #f0f0f0;
}
.calendar-day.has-event {
/* 有事件的日期块背景颜色为浅绿色 */
background-color: #d4edda;
}
e. 事件样式(.event
):
.event {
/* 上方外边距之间的间距为 5px */
margin-top: 5px;
font-size: 14px;
/* 字体颜色为深灰色 */
color: #333;
/* 背景颜色为浅米色 */
background-color: #ffe4b5;
padding: 5px;
border-radius: 5px;
}
#event-form {
display: flex;
gap: 10px;
margin-top: 20px;
}
f. 删除按钮样式(.delete-btn
):
.delete-btn {
/* 字体颜色为红色 */
color: red;
font-weight: bold;
cursor: pointer;
/* 左侧外边距为 5px */
margin-left: 5px;
/* 按钮以行内块级元素显示 */
display: inline-block;
}
.delete-btn:hover {
/* 鼠标悬停时按钮放大 1.2 倍 */
transform: scale(1.2);
}
g. 热图消息框样式(.event-popup
):
.event-popup {
/* 绝对定位 */
position: absolute;
/* 背景颜色为白色 */
background-color: white;
/* 浅灰色边框 */
border: 1px solid #ccc;
/* 添加阴影效果 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
/* 圆角边框,半径为 8px */
border-radius: 8px;
padding: 10px;
/* 确保在其他元素之上 */
z-index: 100;
/* 默认隐藏 */
display: none;
/* 最小宽度为 200px */
min-width: 200px;
}
(3)JavaScript(scripts.js):
- 用于实现动态交互功能,包括调用后端
API
获取或提交数据、动态生成日历视图、响应用户操作(如点击日期块显示事件、添加事件、删除事件、热图消息框等) AJAX
技术(通过Fetch API
实现),前端通过Fetch API
调用后端提供的RESTful API
,实现前后端数据交互
a. 初始化与全局变量:
// 等待页面加载完成后执行代码
// 确保 DOM 元素已经渲染到页面上,避免操作未加载的 HTML 元素
document.addEventListener('DOMContentLoaded', () => {
// 获取页面中用于显示日历、事件表单、年份/月份输入框、更新按钮的 DOM 元素
// calendarView:用于显示日历的容器
const calendarView = document.getElementById('calendar-view');
// eventForm:用于添加事件的表单
const eventForm = document.getElementById('event-form');
// yearInput 和 monthInput:输入年份和月份的输入框
const yearInput = document.getElementById('year');
const monthInput = document.getElementById('month');
// updateCalendarButton:更新日历的按钮
const updateCalendarButton = document.getElementById('update-calendar');
// 获取当前日期,并初始化 currentYear 和 currentMonth
// today:当前日期对象;new Date(): 创建当前日期对象
const today = new Date();
// currentYear:当前年份;getFullYear(): 获取当前年份
let currentYear = today.getFullYear();
// currentMonth:当前月份(0 表示 1 月,11 表示 12 月);getMonth(): 获取当前月份
let currentMonth = today.getMonth();
// 定义一周的标题,用于显示在日历顶部
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
// 初始化年月输入框为当前年月,并生成日历
yearInput.value = currentYear;
monthInput.value = currentMonth;
generateCalendar(currentYear, currentMonth);
b. 从后端获取事件:
// 从后端 API 获取所有事件数据
const fetchEvents = async () => {
// fetch('/api/events'):使用 fetch 发送 HTTP GET 请求到 /api/events
const response = await fetch('/api/events');
// 将返回的 JSON 数据解析为 JavaScript 对象
return await response.json();
};
c. 删除事件:
// 异步删除指定日期的事件
const deleteEvent = async (date) => {
// fetch: 发送 DELETE 请求到 /api/events/:date
const response = await fetch(`/api/events/${date}`, {
method: 'DELETE',
});
// 检查响应是否成功
return response.ok;
};
d. 日历生成逻辑:
// 生成指定年份和月份的日历
const generateCalendar = async (year, month) => {
// 清空 calendarView 容器,并重新生成日历,避免重复渲染
calendarView.innerHTML = '';
// firstDay: 获取指定月份的第一天
const firstDay = new Date(year, month, 1);
// lastDay: 获取指定月份的最后一天
const lastDay = new Date(year, month + 1, 0);
// daysInMonth: 获取该月的总天数
const daysInMonth = lastDay.getDate();
// 创建一个 div 容器显示星期标题,并添加到 calendarView
const weekRow = document.createElement('div');
weekRow.className = 'calendar-row';
// 使用 CSS Grid 布局,将一行分为 7 列(对应一周 7 天)
weekRow.style.display = 'grid';
weekRow.style.gridTemplateColumns = 'repeat(7, 1fr)';
// 遍历 weekdays 数组,为每个星期创建一个单元格,并设置样式
weekdays.forEach(weekday => {
const weekdayCell = document.createElement('div');
weekdayCell.textContent = weekday;
weekdayCell.style.fontWeight = 'bold';
weekdayCell.style.backgroundColor = '#ccc';
weekdayCell.style.padding = '10px';
weekRow.appendChild(weekdayCell);
});
calendarView.appendChild(weekRow);
// 创建日期容器,并设置样式
const daysContainer = document.createElement('div');
daysContainer.style.display = 'grid';
daysContainer.style.gridTemplateColumns = 'repeat(7, 1fr)';
daysContainer.style.gap = '5px';
// 填充该月第一天前的空白单元格
for (let i = 0; i < firstDay.getDay(); i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day';
daysContainer.appendChild(emptyDay);
}
// 遍历该月的每一天,创建日期单元格
const events = await fetchEvents();
for (let i = 1; i <= daysInMonth; i++) {
const day = document.createElement('div');
day.className = 'calendar-day';
// 格式化日期,并过滤出该日期的事件
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
const dayEvents = events.filter(event => event.date.startsWith(dateStr));
// 检查当天是否有事件,若有则添加 has-event 类
if (dayEvents.length > 0) {
day.classList.add('has-event');
}
// 日期数字
const dayNumber = document.createElement('div');
dayNumber.textContent = i;
day.appendChild(dayNumber);
// 为日期单元格绑定点击事件,点击后显示事件热图
day.addEventListener('click', async (event) => {
// 阻止冒泡
event.stopPropagation();
if (dayEvents.length === 0) {
return; // 没有事件时不弹出热图
}
e. 事件热图(弹出框):
// 创建或更新热图消息框,如果弹出框已存在,则直接使用;否则创建新的弹出框并添加到页面
let popup = document.querySelector(`.event-popup[data-date="${dateStr}"]`);
// 如果弹出框不存在
if (!popup) {
// 创建一个新的 div 元素,作为弹出框
popup = document.createElement('div');
// 为弹出框设置类名 event-popup,以便应用样式
popup.className = 'event-popup';
// 设置弹出框的 data-date 属性,用于标识该弹出框对应的日期
popup.setAttribute('data-date', dateStr);
// 设置弹出框的定位方式为绝对定位,以便根据鼠标位置显示
popup.style.position = 'absolute';
// 设置弹出框的左边距和上边距,使其相对于鼠标点击位置偏移 10 像素
popup.style.left = `${event.pageX + 10}px`;
popup.style.top = `${event.pageY + 10}px`;
// 创建一个新的 div 元素,作为关闭按钮
const closeButton = document.createElement('div');
// 设置关闭按钮的文本内容为 "✖ 关闭"
closeButton.textContent = '✖ 关闭';
// 设置鼠标悬停时的光标样式为指针,表示该元素是可点击的
closeButton.style.cursor = 'pointer';
// 设置关闭按钮的下边距,使其与后续内容有一定的间距
closeButton.style.marginBottom = '10px';
// 为关闭按钮添加点击事件监听器
closeButton.addEventListener('click', () => {
// 点击关闭按钮时,将弹出框的显示样式设置为 none,隐藏弹出框
popup.style.display = 'none';
});
// 将关闭按钮添加到弹出框中
popup.appendChild(closeButton);
// 将弹出框添加到页面的 body 中,使其显示在页面上
document.body.appendChild(popup);
}
// 查询弹出框中所有具有类名 event 的元素,并将它们从弹出框中移除
popup.querySelectorAll('.event').forEach(e => e.remove());
// 遍历当天的所有事件(dayEvents),为每个事件创建一个新的 div 元素
dayEvents.forEach(event => {
// 创建一个新的 div 元素,用于表示单个事件
const eventDiv = document.createElement('div');
eventDiv.className = 'event';
// 设置事件 div 的 HTML 内容,包括事件标题和删除按钮
eventDiv.innerHTML = `
<span class="event-title">${event.title}</span>
<span class="delete-btn" data-date="${event.date}">✖</span>
`;
// 将事件 div 添加到弹出框中
popup.appendChild(eventDiv);
});
// 将弹出框的显示样式设置为 block,使其可见
popup.style.display = 'block';
f. 删除事件逻辑:
// 从当前的弹窗(popup)中查询所有具有类名 delete-btn 的按钮,并将它们存储在 deleteButtons 中
const deleteButtons = popup.querySelectorAll('.delete-btn');
// 遍历每个删除按钮,为它们添加点击事件监听器
deleteButtons.forEach(button => {
// 为每个删除按钮绑定一个异步的点击事件处理函数
button.addEventListener('click', async (event) => {
// 获取被点击的删除按钮上的 data-date 属性值,该值表示事件的日期
const date = event.target.getAttribute('data-date');
// 使用 trim() 去除多余的空白字符,split('\n')[0] 提取第一行文本(事件标题)
const title = event.target.parentElement.innerText.trim().split('\n')[0];
// 发送一个 DELETE 请求到 /api/events,尝试删除事件
const success = await fetch('/api/events', {
method: 'DELETE',
// 设置请求头,指定内容类型为 JSON
headers: {
'Content-Type': 'application/json',
},
// 将请求体格式化为 JSON 字符串,包含事件的日期和事件的标题
body: JSON.stringify({
date: date,
title: title,
}),
}).then(response => response.ok);
if (success) {
// 查询所有弹窗(类名为 event-popup 的元素)
const allPopups = document.querySelectorAll('.event-popup');
// 遍历所有弹窗,将它们的 display 样式设置为 none,隐藏所有弹窗
allPopups.forEach(p => p.style.display = 'none');
// 重新生成当前年份和月份的日历,以反映删除操作后的变化
generateCalendar(year, month);
} else {
// 弹出警告框,提示用户删除失败
alert('删除失败,请重试。');
}
});
});
g. 表单提交(添加事件):
// 为事件表单(eventForm)添加提交事件监听器,并将事件数据发送到后端
eventForm.addEventListener('submit', async (event) => {
// 阻止表单的默认提交行为(如页面刷新)
event.preventDefault();
// 获取表单中事件标题和事件日期输入框的值
const title = document.getElementById('event-title').value;
const date = document.getElementById('event-date').value;
// 检查事件标题和日期是否都已填写
if (!title || !date) {
// 如果任一字段为空,弹出警告框
alert('请输入事件标题和日期!');
return;
}
// 发送一个 POST 请求到 /api/events,尝试添加新事件
const response = await fetch('/api/events', {
method: 'POST',
headers: {
// 设置请求头,指定内容类型为 JSON
'Content-Type': 'application/json',
},
// 将请求体格式化为 JSON 字符串,包含事件的标题和事件的日期
body: JSON.stringify({
title: title,
date: date,
}),
});
// 如果响应状态码为 2xx,表示请求成功
if (response.ok) {
alert('事件添加成功!');
// 调用 generateCalendar 函数,重新生成当前年份和月份的日历
generateCalendar(currentYear, currentMonth);
// 重置表单,清空输入框的内容
eventForm.reset();
} else {
// 解析响应体中的 JSON 数据,假设后端返回了错误信息
const errorData = await response.json();
// 弹出警告框,显示从后端获取的错误信息
alert(`添加失败: ${errorData.error}`);
}
});
h. 更新日历按钮:
// 监听更新按钮(updateCalendarButton)点击,根据用户输入的年份和月份重新生成日历
updateCalendarButton.addEventListener('click', () => {
// 获取年份输入框的值,并使用 parseInt 将其转换为整数;10 表示按十进制解析
const year = parseInt(yearInput.value, 10);
// 获取月份输入框的值,并使用 parseInt 将其转换为整数
const month = parseInt(monthInput.value, 10);
// 检查年份和月份的有效性
// - !year 检查年份是否为空或零
// - isNaN(year) 检查年份是否不是数字
// - month < 0 或 month > 11 检查月份是否在有效范围(0-11)之外
if (!year || isNaN(year) || month < 0 || month > 11) {
alert('请输入有效的年份和月份!');
return;
}
// 将有效的年份和月份值赋给变量,用于后续操作
currentYear = year;
currentMonth = month;
// 调用 generateCalendar 函数,根据用户输入的年份和月份重新生成日历
generateCalendar(year, month);
});
2.后端技术栈
(1)Python(Flask 框架):
- 提供路由管理、请求处理、
JSON
响应等核心功能 - 实现了事件的增删查功能,并提供了
RESTful
接口供前端调用
a. 基本构建:
# 导入模块
"""
Flask:用于创建 Web 应用
request:用于处理 HTTP 请求(获取 POST 请求的 JSON 数据)
jsonify:将 Python 数据结构(如字典、列表)转换为 JSON 响应
render_template:用于渲染 HTML 模板文件
datetime:用于处理日期和时间
json:用于加载和保存 JSON 数据
os:用于操作文件路径和文件检查
"""
from flask import Flask, request, jsonify, render_template
from datetime import datetime
import json
import os
# 初始化 Flask 应用,__name__ 是当前模块的名称
app = Flask(__name__)
# 定义事件数据的存储文件路径为 events.json
EVENTS_FILE = 'events.json'
# 检查 events.json 文件是否存在
# 如果文件不存在,则创建一个空的 JSON 文件,初始内容为一个空列表 []
if not os.path.exists(EVENTS_FILE):
with open(EVENTS_FILE, 'w', encoding='utf-8') as f:
json.dump([], f, ensure_ascii=False, indent=4)
# 启动 Flask 应用,监听所有 IP 地址(0.0.0.0),端口为 5000
if __name__ == '__main__':
# debug=True:开启调试模式,便于开发时查看错误信息
app.run(host='0.0.0.0', port=5000, debug=True)
b. 加载事件函数:
def load_events():
# 打开 JSON 文件并加载事件内容
with open(EVENTS_FILE, 'r', encoding='utf-8') as f:
# 返回一个 Python 列表(表示事件数据)
return json.load(f)
c. 保存事件函数:
def save_events(events):
# 将事件列表保存到 JSON 文件
with open(EVENTS_FILE, 'w', encoding='utf-8') as f:
json.dump(events, f, ensure_ascii=False, indent=4)
d. 首页路由:
# 定义根路径 / 的路由
@app.route('/')
def index():
# 渲染 index.html 模板文件,作为首页
return render_template('index.html')
e. 获取所有事件:
3# 定义 /api/events 的 GET 路由
@app.route('/api/events', methods=['GET'])
def get_all_events():
# 调用 load_events 加载所有事件,并以 JSON 格式返回
events = load_events()
return jsonify(events)
f. 添加事件:
# 获取请求数据,从 POST 请求的 JSON 数据中提取 title 和 date
@app.route('/api/events', methods=['POST'])
def add_event():
"""添加事件"""
data = request.get_json()
title = data.get('title')
date_str = data.get('date')
# 验证日期格式
# 使用 datetime.fromisoformat 验证日期格式,若无效则返回 400 错误
try:
event_date = datetime.fromisoformat(date_str)
except ValueError:
return jsonify({'error': '日期格式无效'}), 400
# 提取年月日,不包含具体时间
event_date_ymd = event_date.date()
events = load_events()
# 检查重复事件
# 遍历现有事件,检查同一日期(仅年月日)下是否存在相同标题的事件
for event in events:
existing_date = datetime.fromisoformat(event['date']).date()
if event['title'] == title and existing_date == event_date_ymd:
# 如果存在,返回 400 错误
return jsonify({'error': '该日期下的事件标题已存在'}), 400
# 如果不重复,则添加事件
event = {
'title': title,
'date': event_date.isoformat()
}
# 创建新事件字典,追加到事件列表
events.append(event)
# 调用 save_events 保存事件
save_events(events)
# 返回 201 状态码,表示事件添加成功
return jsonify({'message': '事件添加成功!'}), 201
g. 根据日期获取事件:
# 定义动态路由 /api/events/<date>,根据 URL 中的日期参数筛选事件
@app.route('/api/events/<date>', methods=['GET'])
def get_events_by_date(date):
# 根据日期获取事件
events = load_events()
# 筛选指定日期的事件(仅匹配事件的年月日部分)
filtered_events = [
event for event in events if datetime.fromisoformat(event['date']).date().isoformat() == date
]
return jsonify(filtered_events)
h. 删除事件:
# 从 DELETE 请求的 JSON 数据中提取 date 和 title
@app.route('/api/events', methods=['DELETE'])
def delete_specific_event():
data = request.get_json()
date_str = data.get('date')
title = data.get('title')
# 如果缺少 date 或 title,返回 400 错误
if not date_str or not title:
return jsonify({'error': '日期和标题是必填项'}), 400
events = load_events()
# 遍历事件列表,根据指定日期和标题精确删除事件
new_events = [
event for event in events if not (event['date'] == date_str and event['title'] == title)
]
# 保存更新后的事件列表
save_events(new_events)
return jsonify({'message': '事件删除成功!'}), 200
3.数据存储
- 使用
JSON
文件 (events.json
) 来存储事件数据(模拟数据库),适合小型项目或开发测试 - 每次添加、删除或查询事件时,后端会读取或修改该
JSON
文件
二:项目功能分析
该项目基于Flask
实现了一个基础的日历应用,功能完整且易于扩展。允许用户通过前端界面添加、查看和管理事件,同时提供日历视图来直观显示事件信息。事件数据存储在服务器端的 JSON
文件中,支持按日期查询、添加和删除事件。前端通过 HTML
/CSS
/JavaScript
实现交互界面;后端通过 Flask
提供 RESTful API
1.前端功能
(1)日历视图
- 按月显示日历,标记有事件的日子
- 用户手动输入年份和月份,点击“更新日历”按钮刷新日历视图
- 日历以网格形式显示,每周 7 天,按星期日到星期六排列
(2)事件添加
- 通过表单输入事件标题和日期,提交后调用后端
API
添加事件 - 用户可以通过表单输入事件标题和日期,将其添加到日历中
(3)事件显示
- 在日历中,有事件的日期会被特殊标记(浅绿色背景)
- 点击有事件的日期显示弹出框,显示该日期的所有事件
(4)事件删除
- 在弹出框中点击删除按钮,调用后端
API
删除指定事件 - 在事件详情框中,可以点击删除按钮删除某个事件
2.后端功能
-
使用
JSON
文件存储事件数据,支持数据持久化保存 -
对日期格式和重复事件进行校验,确保数据一致性
-
后端基于
Flask
框架实现,实现了事件的增删查功能,并提供了RESTful
接口供前端调用 -
GET /api/events
:返回存储的所有事件数据(JSON
格式) -
POST /api/events
:接收事件标题和日期,验证数据合法性(如日期格式、重复事件检查),将事件存储到JSON
文件中 -
GET /api/events/<date>
:根据日期参数过滤并返回事件 -
DELETE /api/events
:根据事件标题和日期删除指定事件 -
/路由
:渲染index.html
模板
3.功能完整性分析
(1)用户交互
- 用户通过前端界面输入事件信息或选择日期查看事件
(2)前端逻辑
- 前端捕获用户操作,调用后端
API
完成数据交互
(3)后端处理
- 后端接收请求,验证数据合法性,操作
JSON
文件完成数据存储或删除
(4)数据展示
- 前端根据后端返回的数据更新界面,显示事件信息或提示错误
三:项目改进方向
🎈虽然这是一个功能完整的全栈项目,但仍有改进空间:
(1)当前的 JSON
文件存储方式适合小型项目,但要想支持更复杂的查询和大规模数据存储,还是要使用关系型数据库(如 SQLite
)或 NoSQL
数据库(如 MongoDB
)替代 JSON
文件
(2)弹出框位置固定,可能遮挡内容,可以改为动态调整位置或改为模态框
(3)设置事件提醒
(4)为事件添加标签或分类,方便筛选和管理
(5)将事件数据导出为 CSV
或 iCalendar
文件
四:文件夹和文件说明
项目文件夹架构:
1.project_directory:
- 项目总文件夹:名字自定义
2.app.py:
- 包含
Flask
应用逻辑,处理API
路由(如获取事件、添加事件、删除事件等)以及渲染主页
3.events.json:
- 存储事件数据的
JSON
文件。如果文件不存在,应用会在启动时自动创建
4.static 文件夹:
- 存储
CSS
样式文件(styles.css
)和JavaScript
脚本文件(scripts.js
)
5.templates 文件夹:
- 存储
HTML
模板文件(index.html
)
五:项目源代码
1.app.py
from flask import Flask, request, jsonify, render_template
from datetime import datetime
import json
import os
app = Flask(__name__)
EVENTS_FILE = 'events.json'
if not os.path.exists(EVENTS_FILE):
with open(EVENTS_FILE, 'w', encoding='utf-8') as f:
json.dump([], f, ensure_ascii=False, indent=4)
def load_events():
with open(EVENTS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def save_events(events):
with open(EVENTS_FILE, 'w', encoding='utf-8') as f:
json.dump(events, f, ensure_ascii=False, indent=4)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/events', methods=['GET'])
def get_all_events():
events = load_events()
return jsonify(events)
@app.route('/api/events', methods=['POST'])
def add_event():
data = request.get_json()
title = data.get('title')
date_str = data.get('date')
try:
event_date = datetime.fromisoformat(date_str)
except ValueError:
return jsonify({'error': '日期格式无效'}), 400
event_date_ymd = event_date.date()
events = load_events()
for event in events:
existing_date = datetime.fromisoformat(event['date']).date()
if event['title'] == title and existing_date == event_date_ymd:
return jsonify({'error': '该日期下的事件标题已存在'}), 400
event = {
'title': title,
'date': event_date.isoformat()
}
events.append(event)
save_events(events)
return jsonify({'message': '事件添加成功!'}), 201
@app.route('/api/events/<date>', methods=['GET'])
def get_events_by_date(date):
events = load_events()
filtered_events = [
event for event in events if datetime.fromisoformat(event['date']).date().isoformat() == date
]
return jsonify(filtered_events)
@app.route('/api/events', methods=['DELETE'])
def delete_specific_event():
data = request.get_json()
date_str = data.get('date')
title = data.get('title')
if not date_str or not title:
return jsonify({'error': '日期和标题是必填项'}), 400
events = load_events()
new_events = [
event for event in events if not (event['date'] == date_str and event['title'] == title)
]
save_events(new_events)
return jsonify({'message': '事件删除成功!'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
2.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="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<div id="app">
<h1>日历应用</h1>
<div id="custom-date-selector">
<label for="year">年份:</label>
<input type="number" id="year" placeholder="年份:2025" min="1900" max="2100">
<label for="month">月份:</label>
<select id="month">
<option value="0">一月</option>
<option value="1">二月</option>
<option value="2">三月</option>
<option value="3">四月</option>
<option value="4">五月</option>
<option value="5">六月</option>
<option value="6">七月</option>
<option value="7">八月</option>
<option value="8">九月</option>
<option value="9">十月</option>
<option value="10">十一月</option>
<option value="11">十二月</option>
</select>
<button id="update-calendar">更新日历</button>
</div>
<div id="calendar-view">
</div>
<form id="event-form">
<input type="text" id="event-title" placeholder="事件标题">
<input type="datetime-local" id="event-date">
<button type="submit">添加事件</button>
</form>
</div>
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
</body>
</html>
3.styles.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 100vh;
background-color: #f8f9fa;
}
#app {
max-width: 800px;
margin: 0 auto;
text-align: center;
}
h2 {
text-align: center;
margin-bottom: 15px;
}
#calendar-view {
display: grid;
gap: 5px;
margin: 20px 0;
width: 100%;
max-width: 800px;
}
.calendar-row {
font-weight: bold;
background-color: #ccc;
padding: 5px;
font-size: 18px;
}
.calendar-day {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
background-color: #e9ecef;
cursor: pointer;
}
.calendar-day:hover {
background-color: #f0f0f0;
}
.calendar-day.has-event {
background-color: #d4edda;
}
.event {
margin-top: 5px;
font-size: 14px;
color: #333;
background-color: #ffe4b5;
padding: 5px;
border-radius: 5px;
}
.delete-btn {
color: red;
font-weight: bold;
cursor: pointer;
margin-left: 5px;
display: inline-block;
}
.delete-btn:hover {
transform: scale(1.2);
}
#event-form {
display: flex;
gap: 10px;
margin-top: 20px;
}
.calendar-day.has-event {
background-color: #d4edda;
cursor: pointer;
}
.event-popup {
position: absolute;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 10px;
z-index: 100;
display: none;
min-width: 200px;
}
.event-popup .event {
margin-bottom: 8px;
}
.event-popup .delete-btn {
color: red;
font-weight: bold;
cursor: pointer;
float: right;
}
.event-popup .delete-btn:hover {
transform: scale(1.2);
}
4.scripts.js
document.addEventListener('DOMContentLoaded', () => {
const calendarView = document.getElementById('calendar-view');
const eventForm = document.getElementById('event-form');
const yearInput = document.getElementById('year');
const monthInput = document.getElementById('month');
const updateCalendarButton = document.getElementById('update-calendar');
const today = new Date();
let currentYear = today.getFullYear();
let currentMonth = today.getMonth();
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const fetchEvents = async () => {
const response = await fetch('/api/events');
return await response.json();
};
const deleteEvent = async (date) => {
const response = await fetch(`/api/events/${date}`, {
method: 'DELETE',
});
return response.ok;
};
const generateCalendar = async (year, month) => {
calendarView.innerHTML = '';
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const weekRow = document.createElement('div');
weekRow.className = 'calendar-row';
weekRow.style.display = 'grid';
weekRow.style.gridTemplateColumns = 'repeat(7, 1fr)';
weekdays.forEach(weekday => {
const weekdayCell = document.createElement('div');
weekdayCell.textContent = weekday;
weekdayCell.style.fontWeight = 'bold';
weekdayCell.style.backgroundColor = '#ccc';
weekdayCell.style.padding = '10px';
weekRow.appendChild(weekdayCell);
});
calendarView.appendChild(weekRow);
const daysContainer = document.createElement('div');
daysContainer.style.display = 'grid';
daysContainer.style.gridTemplateColumns = 'repeat(7, 1fr)';
daysContainer.style.gap = '5px';
for (let i = 0; i < firstDay.getDay(); i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day';
daysContainer.appendChild(emptyDay);
}
const events = await fetchEvents();
for (let i = 1; i <= daysInMonth; i++) {
const day = document.createElement('div');
day.className = 'calendar-day';
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
const dayEvents = events.filter(event => event.date.startsWith(dateStr));
if (dayEvents.length > 0) {
day.classList.add('has-event');
}
const dayNumber = document.createElement('div');
dayNumber.textContent = i;
day.appendChild(dayNumber);
day.addEventListener('click', async (event) => {
event.stopPropagation();
if (dayEvents.length === 0) {
return;
}
let popup = document.querySelector(`.event-popup[data-date="${dateStr}"]`);
if (!popup) {
popup = document.createElement('div');
popup.className = 'event-popup';
popup.setAttribute('data-date', dateStr);
popup.style.position = 'absolute';
popup.style.left = `${event.pageX + 10}px`;
popup.style.top = `${event.pageY + 10}px`;
const closeButton = document.createElement('div');
closeButton.textContent = '✖ 关闭';
closeButton.style.cursor = 'pointer';
closeButton.style.marginBottom = '10px';
closeButton.addEventListener('click', () => {
popup.style.display = 'none';
});
popup.appendChild(closeButton);
document.body.appendChild(popup);
}
popup.querySelectorAll('.event').forEach(e => e.remove());
dayEvents.forEach(event => {
const eventDiv = document.createElement('div');
eventDiv.className = 'event';
eventDiv.innerHTML = `
<span class="event-title">${event.title}</span>
<span class="delete-btn" data-date="${event.date}">✖</span>
`;
popup.appendChild(eventDiv);
});
popup.style.display = 'block';
const deleteButtons = popup.querySelectorAll('.delete-btn');
deleteButtons.forEach(button => {
button.addEventListener('click', async (event) => {
const date = event.target.getAttribute('data-date');
const title = event.target.parentElement.innerText.trim().split('\n')[0];
const success = await fetch('/api/events', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
date: date,
title: title,
}),
}).then(response => response.ok);
if (success) {
const allPopups = document.querySelectorAll('.event-popup');
allPopups.forEach(p => p.style.display = 'none');
generateCalendar(year, month);
} else {
alert('删除失败,请重试。');
}
});
});
});
daysContainer.appendChild(day);
}
calendarView.appendChild(daysContainer);
};
eventForm.addEventListener('submit', async (event) => {
event.preventDefault();
const title = document.getElementById('event-title').value;
const date = document.getElementById('event-date').value;
if (!title || !date) {
alert('请输入事件标题和日期!');
return;
}
const response = await fetch('/api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title,
date: date,
}),
});
if (response.ok) {
alert('事件添加成功!');
generateCalendar(currentYear, currentMonth);
eventForm.reset();
} else {
const errorData = await response.json();
alert(`添加失败: ${errorData.error}`);
}
});
updateCalendarButton.addEventListener('click', () => {
const year = parseInt(yearInput.value, 10);
const month = parseInt(monthInput.value, 10);
if (!year || isNaN(year) || month < 0 || month > 11) {
alert('请输入有效的年份和月份!');
return;
}
currentYear = year;
currentMonth = month;
generateCalendar(year, month);
});
yearInput.value = currentYear;
monthInput.value = currentMonth;
generateCalendar(currentYear, currentMonth);
});