python 的框架 dash 开发TodoList Web 应用

发布于:2025-02-16 ⋅ 阅读:(35) ⋅ 点赞:(0)

TodoList Web 应用

在这里插入图片描述

项目简介

这是一个基于 Dash 和 SQLAlchemy 的现代化 TodoList Web 应用,提供了简单而强大的待办事项管理功能。

主要特性

  • 添加新的待办事项
  • 删除待办事项
  • 标记待办事项为已完成/未完成
  • 分页展示待办事项列表
  • 实时更新和交互

技术栈

  • Python
  • Dash (Web框架)
  • SQLAlchemy (ORM)
  • SQLite (数据库)
  • Dash Bootstrap Components (UI组件)

功能详细说明

待办事项管理

应用提供了一个统一的回调函数 manage_todos(),处理以下交互逻辑:

  1. 删除待办事项
  2. 添加新的待办事项
  3. 切换待办事项状态
  4. 分页展示待办事项列表
删除功能
  • 支持通过删除按钮移除指定的待办事项
  • 添加了详细的错误处理和日志记录
  • 确保只处理有效的点击事件
状态切换
  • 支持多种状态切换方式(复选框和开关)
  • 灵活处理不同类型的状态值
  • 实时更新待办事项完成状态
分页
  • 默认每页显示10个待办事项
  • 动态计算总页数
  • 支持页面间切换

调试与日志

应用内置详细的日志记录机制,记录:

  • 回调上下文详情
  • 触发的输入事件
  • 操作执行情况
  • 错误信息

运行项目

依赖安装

pip install -r requirements.txt

启动应用

python app.py

访问 http://127.0.0.1:8050/ 使用应用

requirements.txt
‘’’
dash2.14.1
dash-bootstrap-components
1.5.0
sqlalchemy2.0.25
plotly
5.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)


网站公告

今日签到

点亮在社区的每一天
去签到