文章目录
- 一:项目技术栈和代码分析
- 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/eventsconst response = await fetch('/api/events');// 将返回的 JSON 数据解析为 JavaScript 对象return await response.json();};
c. 删除事件:
// 异步删除指定日期的事件
const deleteEvent = async (date) => {// fetch: 发送 DELETE 请求到 /api/events/:dateconst 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 容器显示星期标题,并添加到 calendarViewconst 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', // 设置请求头,指定内容类型为 JSONheaders: { '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': '日期和标题是必填项'}), 400events = 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 osapp = 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': '日期格式无效'}), 400event_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': '该日期下的事件标题已存在'}), 400event = {'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': '日期和标题是必填项'}), 400events = 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': '事件删除成功!'}), 200if __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);
});