TodoList Web 应用
项目简介
这是一个基于 Dash 和 SQLAlchemy 的现代化 TodoList Web 应用,提供了简单而强大的待办事项管理功能。
主要特性
- 添加新的待办事项
- 删除待办事项
- 标记待办事项为已完成/未完成
- 分页展示待办事项列表
- 实时更新和交互
技术栈
- Python
- Dash (Web框架)
- SQLAlchemy (ORM)
- SQLite (数据库)
- Dash Bootstrap Components (UI组件)
功能详细说明
待办事项管理
应用提供了一个统一的回调函数 manage_todos()
,处理以下交互逻辑:
- 删除待办事项
- 添加新的待办事项
- 切换待办事项状态
- 分页展示待办事项列表
删除功能
- 支持通过删除按钮移除指定的待办事项
- 添加了详细的错误处理和日志记录
- 确保只处理有效的点击事件
状态切换
- 支持多种状态切换方式(复选框和开关)
- 灵活处理不同类型的状态值
- 实时更新待办事项完成状态
分页
- 默认每页显示10个待办事项
- 动态计算总页数
- 支持页面间切换
调试与日志
应用内置详细的日志记录机制,记录:
- 回调上下文详情
- 触发的输入事件
- 操作执行情况
- 错误信息
运行项目
依赖安装
pip install -r requirements.txt
启动应用
python app.py
访问 http://127.0.0.1:8050/
使用应用
requirements.txt
‘’’
dash2.14.1
dash-bootstrap-components1.5.0
sqlalchemy2.0.25
plotly5.19.0
‘’’
import dash
import dash_bootstrap_components as dbc
from dash import html, dcc, Input, Output, State, dash_table
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.sql import func
from datetime import datetime
import logging
import json # 确保导入 json 模块
# 配置日志
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(levelname)s: %(message)s')
# 数据库配置
DATABASE_URL = 'sqlite:///todolist.db'
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
# 待办事项模型
class Todo(Base):
__tablename__ = 'todos'
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, nullable=True)
completed = Column(Boolean, default=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# 创建数据库表
Base.metadata.create_all(bind=engine)
# 初始化 Dash 应用
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
def get_todos(page=1, per_page=10):
"""获取分页后的待办事项"""
db = SessionLocal()
try:
total_todos = db.query(Todo).count()
offset = (page - 1) * per_page
todos = db.query(Todo).order_by(Todo.created_at.desc()).offset(offset).limit(per_page).all()
logging.debug(f"Total todos: {total_todos}, Page: {page}, Per page: {per_page}")
return todos, total_todos
finally:
db.close()
def add_todo(title, description):
"""添加新的待办事项"""
db = SessionLocal()
try:
new_todo = Todo(title=title, description=description)
db.add(new_todo)
db.commit()
db.refresh(new_todo)
logging.debug(f"Added new todo: {new_todo.title}")
return new_todo
finally:
db.close()
def update_todo_status(todo_id, completed):
"""更新待办事项状态"""
db = SessionLocal()
try:
todo = db.query(Todo).filter(Todo.id == todo_id).first()
if todo:
todo.completed = completed
db.commit()
logging.debug(f"Todo {todo_id} status updated to {completed}")
except Exception as e:
logging.error(f"Error updating todo status: {e}")
finally:
db.close()
def delete_todo(todo_id):
"""删除待办事项"""
db = SessionLocal()
try:
todo = db.query(Todo).filter(Todo.id == todo_id).first()
if todo:
db.delete(todo)
db.commit()
finally:
db.close()
def render_todo_list(todos):
"""渲染待办事项列表"""
todo_list = []
for todo in todos:
todo_list.append(
dbc.Card([
dbc.CardBody([
html.H5(todo.title, className="card-title"),
html.P(todo.description or '', className="card-text"),
dbc.Checklist(
options=[{'label': '已完成', 'value': 1}],
value=[1] if todo.completed else [],
id={'type': 'todo-status', 'index': todo.id},
switch=True
),
dbc.Button("删除", color="danger", size="sm",
id={'type': 'delete-todo', 'index': todo.id})
])
], className="mb-2", id=f'todo-card-{todo.id}')
)
return todo_list
# 应用布局
app.layout = dbc.Container([
html.H1("TodoList 应用", className="text-center my-4"),
# 添加待办事项表单
dbc.Row([
dbc.Col([
dbc.Label("标题"),
dbc.Input(id='todo-title', type='text', placeholder='输入待办事项标题')
], width=12),
], className="mb-3"),
dbc.Row([
dbc.Col([
dbc.Label("描述"),
dbc.Input(id='todo-description', type='text', placeholder='输入待办事项描述')
], width=12)
], className="mb-3"),
dbc.Row([
dbc.Col([
dbc.Button("添加待办", id='add-todo-button', color='primary')
])
], className="mb-3"),
# 待办事项列表
html.Div(id='todo-list-container'),
# 分页组件
dbc.Pagination(
id='todo-pagination',
max_value=1, # 初始化为1
fully_expanded=False
),
# 调试信息显示
html.Div(id='debug-output')
], fluid=True)
# 初始化加载待办事项
@app.callback(
[Output('todo-list-container', 'children'),
Output('todo-pagination', 'max_value')],
[Input('todo-pagination', 'active_page')]
)
def initial_load(active_page):
"""初始加载待办事项"""
page = active_page or 1
per_page = 10
todos, total_todos = get_todos(page, per_page)
# 计算总页数
total_pages = max(1, (total_todos + per_page - 1) // per_page)
# 渲染待办事项列表
todo_list = render_todo_list(todos)
return [todo_list, total_pages]
@app.callback(
[Output('todo-list-container', 'children', allow_duplicate=True),
Output('todo-pagination', 'max_value', allow_duplicate=True),
Output('todo-title', 'value'),
Output('todo-description', 'value'),
Output('debug-output', 'children')],
[Input('todo-pagination', 'active_page'),
Input('add-todo-button', 'n_clicks'),
Input({'type': 'delete-todo', 'index': dash.ALL}, 'n_clicks'),
Input({'type': 'todo-status', 'index': dash.ALL}, 'value')],
[State('todo-title', 'value'),
State('todo-description', 'value'),
State({'type': 'delete-todo', 'index': dash.ALL}, 'id'),
State({'type': 'todo-status', 'index': dash.ALL}, 'id')],
prevent_initial_call=True
)
def manage_todos(active_page, add_clicks, delete_clicks, status_values,
title, description, delete_ids, status_ids):
"""
统一管理待办事项的增删改查操作
这个回调函数处理所有的交互逻辑:
1. 删除待办事项
2. 添加新的待办事项
3. 切换待办事项状态
4. 分页展示待办事项列表
参数说明:
- active_page: 当前分页页码
- add_clicks: 添加按钮点击次数
- delete_clicks: 删除按钮点击次数列表
- status_values: 状态切换值列表
- title, description: 新建待办事项的标题和描述
- delete_ids, status_ids: 对应的待办事项ID
"""
ctx = dash.callback_context
# 记录详细的调试日志,帮助追踪回调上下文
logging.debug("Callback Context Details:")
logging.debug(f"Triggered Inputs: {ctx.triggered}")
logging.debug(f"Input Values:")
logging.debug(f"Active Page: {active_page}")
logging.debug(f"Add Clicks: {add_clicks}")
logging.debug(f"Delete Clicks: {delete_clicks}")
logging.debug(f"Delete IDs: {delete_ids}")
logging.debug(f"Status Values: {status_values}")
logging.debug(f"Status IDs: {status_ids}")
# 初始加载:如果没有触发任何事件,默认加载第一页
if not ctx.triggered:
page = 1
per_page = 10
todos, total_todos = get_todos(page, per_page)
total_pages = max(1, (total_todos + per_page - 1) // per_page)
todo_list = render_todo_list(todos)
return [todo_list, total_pages, title, description, '']
# 初始化调试消息
triggered = ctx.triggered
debug_message = "Triggered Inputs:\n"
# 处理删除待办事项
for n_clicks, delete_id in zip(delete_clicks, delete_ids):
logging.debug(f"Delete: n_clicks={n_clicks}, delete_id={delete_id}")
# 确保是有效的点击事件(非初始 None 值)
if n_clicks is not None and n_clicks > 0:
todo_id = delete_id['index']
logging.debug(f"Attempting to delete todo with ID: {todo_id}")
try:
# 执行删除操作
delete_todo(todo_id)
debug_message += f"Deleted todo with ID: {todo_id}\n"
except Exception as e:
# 记录删除错误
logging.error(f"Error deleting todo {todo_id}: {e}")
debug_message += f"Failed to delete todo {todo_id}: {e}\n"
# 处理添加新的待办事项
if add_clicks and title:
# 创建新的待办事项
add_todo(title, description or '')
# 清空输入框
title = None
description = None
# 处理待办事项状态切换
for value, status_id in zip(status_values, status_ids):
todo_id = status_id['index']
# 灵活处理不同类型的状态值
if isinstance(value, list):
completed = 1 in value # 对于复选框
else:
completed = bool(value) # 对于开关
logging.debug(f"Updating todo {todo_id} status to {completed}")
# 更新待办事项状态
update_todo_status(todo_id, completed)
debug_message += f"Updated todo {todo_id} status to {completed}\n"
# 获取当前页的待办事项
page = active_page or 1
per_page = 10
todos, total_todos = get_todos(page, per_page)
# 计算总页数
total_pages = max(1, (total_todos + per_page - 1) // per_page)
debug_message += f"Total todos: {total_todos}, Total pages: {total_pages}\n"
# 渲染待办事项列表
todo_list = render_todo_list(todos)
# 返回更新后的组件状态
return [todo_list, total_pages, title, description, debug_message]
if __name__ == '__main__':
app.run_server(debug=True)