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_pagetodos = 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_todosfinally: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_todofinally: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 = completeddb.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, # 初始化为1fully_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 1per_page = 10todos, 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 = 1per_page = 10todos, 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.triggereddebug_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 = Nonedescription = 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 1per_page = 10todos, 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)