源代码:
app.py
from flask import Flask, render_template, request, redirect, url_for, jsonify
import json
import os
app = Flask(__name__)
# 数据存储文件
DATA_FILE = "todos.json"
def load_todos():
"""从文件加载待办事项"""
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, "r") as f:
return json.load(f)
except:
return []
return []
def save_todos(todos):
"""保存待办事项到文件"""
with open(DATA_FILE, "w") as f:
json.dump(todos, f)
@app.route('/')
def index():
"""显示主页"""
todos = load_todos()
# 计算完成和未完成的任务数量
completed = sum(1 for todo in todos if todo["done"])
not_completed = len(todos) - completed
return render_template('index.html', todos=todos,
completed=completed, not_completed=not_completed)
@app.route('/add', methods=['POST'])
def add_todo():
"""添加新任务"""
task = request.form.get('task')
if task:
todos = load_todos()
todos.append({"task": task, "done": False})
save_todos(todos)
return redirect(url_for('index'))
@app.route('/toggle/<int:index>')
def toggle_todo(index):
"""切换任务状态(完成/未完成)"""
todos = load_todos()
if 0 <= index < len(todos):
todos[index]["done"] = not todos[index]["done"]
save_todos(todos)
return redirect(url_for('index'))
@app.route('/delete/<int:index>')
def delete_todo(index):
"""删除任务"""
todos = load_todos()
if 0 <= index < len(todos):
todos.pop(index)
save_todos(todos)
return redirect(url_for('index'))
@app.route('/clear')
def clear_completed():
"""清除已完成的任务"""
todos = load_todos()
# 只保留未完成的任务
todos = [todo for todo in todos if not todo["done"]]
save_todos(todos)
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True)
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页版待办事项</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 30px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
.todo-form {
display: flex;
margin-bottom: 20px;
}
.todo-form input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
}
.todo-form input:focus {
border-color: #3498db;
outline: none;
}
.todo-form button {
background-color: #3498db;
color: white;
border: none;
padding: 12px 20px;
margin-left: 10px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.todo-form button:hover {
background-color: #2980b9;
}
.todo-stats {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
.todo-stats span {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
}
.total-tasks {
background-color: #e3f2fd;
color: #1976d2;
}
.completed-tasks {
background-color: #e8f5e9;
color: #388e3c;
}
.pending-tasks {
background-color: #fff3e0;
color: #f57c00;
}
.todo-list {
list-style-type: none;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
transition: background-color 0.3s;
}
.todo-item:hover {
background-color: #f9f9f9;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #95a5a6;
}
.todo-checkbox {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-text {
flex: 1;
font-size: 16px;
}
.delete-btn {
background: none;
border: none;
color: #e74c3c;
cursor: pointer;
font-size: 18px;
opacity: 0.7;
transition: opacity 0.3s;
}
.delete-btn:hover {
opacity: 1;
}
.clear-btn {
display: block;
margin: 20px auto 0;
padding: 10px 20px;
background-color: #f5f5f5;
color: #e74c3c;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.clear-btn:hover {
background-color: #ffeef0;
}
@media (max-width: 600px) {
.container {
padding: 15px;
}
.todo-form {
flex-direction: column;
}
.todo-form input {
margin-bottom: 10px;
}
.todo-form button {
margin-left: 0;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📋 待办事项清单</h1>
<form class="todo-form" action="/add" method="POST">
<input type="text" name="task" placeholder="添加新任务..." required>
<button type="submit">添加任务</button>
</form>
<div class="todo-stats">
<span class="total-tasks">总任务: {{ todos|length }}</span>
<span class="completed-tasks">已完成: {{ completed }}</span>
<span class="pending-tasks">未完成: {{ not_completed }}</span>
</div>
<ul class="todo-list">
{% for todo in todos %}
<li class="todo-item {% if todo.done %}completed{% endif %}">
<a href="{{ url_for('toggle_todo', index=loop.index0) }}">
<input type="checkbox" class="todo-checkbox" {% if todo.done %}checked{% endif %}>
</a>
<span class="todo-text">{{ todo.task }}</span>
<a href="{{ url_for('delete_todo', index=loop.index0) }}" class="delete-btn">🗑️</a>
</li>
{% else %}
<li class="todo-item">
<span class="todo-text" style="text-align: center; width: 100%;">暂无任务,添加一个吧!</span>
</li>
{% endfor %}
</ul>
{% if completed > 0 %}
<button class="clear-btn" onclick="location.href='{{ url_for('clear_completed') }}'">清除已完成任务</button>
{% endif %}
</div>
</body>
</html>
文件夹结构:
todo_web_app/
├── app.py # Flask应用主文件
├── templates/ # HTML模板文件夹
│ └── index.html # 主页模板
└── todos.json # 数据存储文件(自动生成)
代码详解:
一、项目结构设计原理
1. 为什么需要这样的文件结构?
Flask框架要求:Flask遵循MVC(模型-视图-控制器)设计模式
app.py
:控制器(Controller) - 处理业务逻辑templates/index.html
:视图(View) - 展示用户界面todos.json
:模型(Model) - 数据存储
模板文件夹命名:Flask默认在
templates
文件夹中查找HTML模板文件数据文件位置:JSON数据文件放在项目根目录,便于读写
2. 为什么需要Web框架?
处理HTTP协议:管理请求/响应生命周期
路由管理:将URL映射到处理函数
模板渲染:动态生成HTML内容
会话管理:处理用户状态(本项目未使用)
二、app.py 代码逐行详解
# 导入必要的库
from flask import Flask, render_template, request, redirect, url_for
import json
import os
# 创建Flask应用实例
app = Flask(__name__)
# 数据存储文件
DATA_FILE = "todos.json"
def load_todos():
"""从文件加载待办事项"""
# 检查文件是否存在
if os.path.exists(DATA_FILE):
try:
# 打开文件并读取JSON内容
with open(DATA_FILE, "r") as f:
return json.load(f)
except:
# 如果读取失败(如文件为空或格式错误),返回空列表
return []
# 文件不存在时返回空列表
return []
def save_todos(todos):
"""保存待办事项到文件"""
# 将待办事项列表写入JSON文件
with open(DATA_FILE, "w") as f:
json.dump(todos, f)
# 定义根路由,处理主页请求
@app.route('/')
def index():
"""显示主页"""
# 加载待办事项
todos = load_todos()
# 计算已完成任务数
completed = sum(1 for todo in todos if todo["done"])
# 计算未完成任务数
not_completed = len(todos) - completed
# 渲染index.html模板,并传入数据
return render_template('index.html',
todos=todos,
completed=completed,
not_completed=not_completed)
# 添加任务的路由,只接受POST请求
@app.route('/add', methods=['POST'])
def add_todo():
"""添加新任务"""
# 从表单获取任务内容
task = request.form.get('task')
if task:
# 加载现有任务
todos = load_todos()
# 添加新任务(默认为未完成)
todos.append({"task": task, "done": False})
# 保存更新后的任务列表
save_todos(todos)
# 重定向回主页
return redirect(url_for('index'))
# 切换任务状态的路由
@app.route('/toggle/<int:index>')
def toggle_todo(index):
"""切换任务状态"""
todos = load_todos()
# 检查索引是否有效
if 0 <= index < len(todos):
# 切换完成状态(True变False,False变True)
todos[index]["done"] = not todos[index]["done"]
save_todos(todos)
return redirect(url_for('index'))
# 删除任务的路由
@app.route('/delete/<int:index>')
def delete_todo(index):
"""删除任务"""
todos = load_todos()
if 0 <= index < len(todos):
# 删除指定索引的任务
todos.pop(index)
save_todos(todos)
return redirect(url_for('index'))
# 清除已完成任务的路由
@app.route('/clear')
def clear_completed():
"""清除已完成的任务"""
todos = load_todos()
# 创建新列表,只包含未完成的任务
new_todos = [todo for todo in todos if not todo["done"]]
save_todos(new_todos)
return redirect(url_for('index'))
# 程序入口
if __name__ == '__main__':
# 确保templates文件夹存在
if not os.path.exists('templates'):
os.makedirs('templates')
print("已创建templates文件夹")
# 启动Flask开发服务器
# debug=True 表示开启调试模式(自动重载代码并显示详细错误)
app.run(debug=True, port=5001) # 指定端口5001,避免与其他应用冲突
关键点解析:
路由系统:
@app.route('/')
:装饰器将URL路径映射到处理函数动态路由
@app.route('/toggle/<int:index>')
:<int:index>
捕获URL中的整数参数
请求方法:
默认只处理GET请求
methods=['POST']
明确指定处理POST请求
重定向模式:
操作后重定向回主页(
redirect(url_for('index'))
)避免浏览器重复提交(POST/重定向/GET模式)
数据持久化:
load_todos()
和save_todos()
封装数据读写JSON格式简单易读,适合小型应用
调试模式:
app.run(debug=True)
启用调试模式修改代码后自动重启服务器
显示详细错误信息
三、index.html 代码逐行详解
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页版待办事项</title>
<style>
/* 基础样式重置 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 页面整体样式 */
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
/* 内容容器 */
.container {
max-width: 800px;
margin: 0 auto;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 30px;
}
/* 标题样式 */
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
/* 任务表单样式 */
.todo-form {
display: flex;
margin-bottom: 20px;
}
.todo-form input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
}
.todo-form input:focus {
border-color: #3498db;
outline: none;
}
.todo-form button {
background-color: #3498db;
color: white;
border: none;
padding: 12px 20px;
margin-left: 10px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.todo-form button:hover {
background-color: #2980b9;
}
/* 任务统计样式 */
.todo-stats {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
.todo-stats span {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
}
.total-tasks {
background-color: #e3f2fd;
color: #1976d2;
}
.completed-tasks {
background-color: #e8f5e9;
color: #388e3c;
}
.pending-tasks {
background-color: #fff3e0;
color: #f57c00;
}
/* 任务列表样式 */
.todo-list {
list-style-type: none;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
transition: background-color 0.3s;
}
.todo-item:hover {
background-color: #f9f9f9;
}
/* 已完成任务样式 */
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #95a5a6;
}
/* 复选框样式 */
.todo-checkbox {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
/* 任务文本样式 */
.todo-text {
flex: 1;
font-size: 16px;
}
/* 删除按钮样式 */
.delete-btn {
background: none;
border: none;
color: #e74c3c;
cursor: pointer;
font-size: 18px;
opacity: 0.7;
transition: opacity 0.3s;
}
.delete-btn:hover {
opacity: 1;
}
/* 清除按钮样式 */
.clear-btn {
display: block;
margin: 20px auto 0;
padding: 10px 20px;
background-color: #f5f5f5;
color: #e74c3c;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.clear-btn:hover {
background-color: #ffeef0;
}
/* 响应式设计 - 小屏幕适配 */
@media (max-width: 600px) {
.container {
padding: 15px;
}
.todo-form {
flex-direction: column;
}
.todo-form input {
margin-bottom: 10px;
}
.todo-form button {
margin-left: 0;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📋 待办事项清单</h1>
<!-- 添加任务的表单 -->
<!-- action="/add" 表示提交到/add路由 -->
<!-- method="POST" 使用POST方法提交 -->
<form class="todo-form" action="/add" method="POST">
<input type="text" name="task" placeholder="添加新任务..." required>
<button type="submit">添加任务</button>
</form>
<!-- 任务统计信息 -->
<!-- 使用Jinja2模板变量显示统计 -->
<div class="todo-stats">
<span class="total-tasks">总任务: {{ todos|length }}</span>
<span class="completed-tasks">已完成: {{ completed }}</span>
<span class="pending-tasks">未完成: {{ not_completed }}</span>
</div>
<!-- 任务列表 -->
<ul class="todo-list">
<!-- 遍历待办事项 -->
{% for todo in todos %}
<!-- 根据任务状态添加completed类 -->
<li class="todo-item {% if todo.done %}completed{% endif %}">
<!-- 切换状态链接 -->
<a href="{{ url_for('toggle_todo', index=loop.index0) }}">
<!-- 根据状态显示复选框 -->
<input type="checkbox" class="todo-checkbox" {% if todo.done %}checked{% endif %}>
</a>
<!-- 任务内容 -->
<span class="todo-text">{{ todo.task }}</span>
<!-- 删除任务链接 -->
<a href="{{ url_for('delete_todo', index=loop.index0) }}" class="delete-btn">🗑️</a>
</li>
<!-- 如果没有任务 -->
{% else %}
<li class="todo-item">
<span class="todo-text" style="text-align: center; width: 100%;">暂无任务,添加一个吧!</span>
</li>
{% endfor %}
</ul>
<!-- 清除已完成任务按钮(只在有完成的任务时显示) -->
{% if completed > 0 %}
<button class="clear-btn" onclick="location.href='{{ url_for('clear_completed') }}'">
清除已完成任务
</button>
{% endif %}
</div>
</body>
</html>
关键点解析:
Jinja2模板引擎:
{{ variable }}
:输出变量值{% for ... %}
:循环结构{% if ... %}
:条件判断loop.index0
:当前循环索引(从0开始)
动态内容生成:
后端传入
todos
、completed
、not_completed
等变量模板根据这些数据动态生成HTML
URL生成:
url_for('函数名')
:生成对应路由的URLurl_for('toggle_todo', index=loop.index0)
:生成带参数的URL
响应式设计:
使用CSS媒体查询适配不同屏幕尺寸
移动端优化布局
用户交互元素:
表单提交:添加新任务
链接点击:切换状态、删除任务
按钮点击:清除已完成任务
四、工作流程解析
1. 用户访问主页 (GET /)
用户请求 -> Flask路由(index函数) -> 加载数据 -> 渲染模板 -> 返回HTML
2. 用户添加任务 (POST /add)
表单提交 -> Flask路由(add_todo函数) -> 处理数据 -> 保存到文件 -> 重定向到主页
3. 用户切换任务状态 (GET /toggle/<index>)
点击链接 -> Flask路由(toggle_todo函数) -> 修改状态 -> 保存数据 -> 重定向
4. 用户删除任务 (GET /delete/<index>)
点击删除图标 -> Flask路由(delete_todo函数) -> 删除任务 -> 保存数据 -> 重定向
5. 用户清除已完成任务 (GET /clear)
点击按钮 -> Flask路由(clear_completed函数) -> 过滤任务 -> 保存数据 -> 重定向
运行结果:
打开网址网页版待办事项
注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!