当前位置: 首页 > news >正文

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)将事件数据导出为 CSViCalendar 文件

四:文件夹和文件说明

项目文件夹架构:
在这里插入图片描述

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);
});

相关文章:

  • QuarkPi-CA2 RK3588S卡片电脑:6.0Tops NPU+8K视频编解码+接口丰富,高性能嵌入式开发!
  • 2020年INS SCI1区TOP:平衡复合运动优化算法BCMO,深度解析+性能实测
  • Unity VideoPlayer 播放无声音
  • 【leetcode hot 100 300】最长递增子序列
  • NoV病毒抗原抗体,助力疫苗研究与诊断试剂开发!
  • 大型语言模型智能应用Coze、Dify、FastGPT、MaxKB 对比,选择合适自己的LLM工具
  • 某局jsvmp算法分析(dunshan.js/lzkqow23819/lzkqow39189)
  • BERT - 段嵌入(Segment Embedding)
  • Composer安装Laravel步骤
  • mybatis多表查询
  • Python实例题:Python实现iavaweb项目远端自动化更新部署
  • 解决双系统ubuntu24.04开机出现花屏等情况
  • Java面试黄金宝典48
  • Java 多线程编程之原子类 AtomicBoolean(构造方法、获取与设置、比较并设置)
  • rancher 解决拉取dashboard-shell镜像失败的问题
  • Wincc管对象的使用
  • 【ESP32-microros(vscode-Platformio)】
  • Go 语言中的 package main、 func main() 和main.go的使用规范
  • 浮点数比较在Eigen数学库中的处理方法
  • AI前沿周报:2025年3月技术深度解析
  • 教育发展基金会网站建设/百度指数热度榜
  • 婚恋交友网站建设方案/2021近期时事新闻热点事件简短
  • 石碣做网站优化/广州seo公司排名
  • 岳阳公司网站制作/惠州seo优化服务
  • 长沙网站seo诊断/长沙做优化的公司
  • php网站有点/app拉新推广