todo_list

This commit is contained in:
jeremygan2021
2025-11-16 17:36:42 +08:00
parent a2682dc040
commit bb04bd8fa5
25 changed files with 1198 additions and 34 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,10 +6,11 @@ from typing import Optional, List
import json
import os
import secrets
from datetime import datetime
from database import get_db
from models import Device as DeviceModel, Content as ContentModel
from schemas import DeviceCreate, ContentCreate
from models import Device as DeviceModel, Content as ContentModel, Todo as TodoModel
from schemas import DeviceCreate, ContentCreate, TodoCreate, TodoUpdate
from image_processor import image_processor
from mqtt_manager import mqtt_manager
@@ -31,22 +32,33 @@ async def admin_dashboard(request: Request, db: Session = Depends(get_db)):
# 获取内容数量
content_count = db.query(ContentModel).count()
active_content_count = db.query(ContentModel).filter(ContentModel.is_active == True).count()
# 获取待办事项数量
todo_count = db.query(TodoModel).count()
completed_todo_count = db.query(TodoModel).filter(TodoModel.is_completed == True).count()
pending_todo_count = todo_count - completed_todo_count
# 获取最近上线的设备
recent_devices = db.query(DeviceModel).order_by(DeviceModel.last_online.desc()).limit(5).all()
# 获取最近创建的内容
recent_contents = db.query(ContentModel).order_by(ContentModel.created_at.desc()).limit(5).all()
# 获取最近创建的待办事项
recent_todos_query = db.query(TodoModel, DeviceModel).join(
DeviceModel, TodoModel.device_id == DeviceModel.device_id
).order_by(TodoModel.created_at.desc()).limit(5).all()
# 转换为包含todo和device的对象列表
recent_todos = [{"todo": todo, "device": device} for todo, device in recent_todos_query]
return templates.TemplateResponse("admin/dashboard.html", {
"request": request,
"device_count": device_count,
"active_device_count": active_device_count,
"content_count": content_count,
"active_content_count": active_content_count,
"todo_count": todo_count,
"completed_todo_count": completed_todo_count,
"pending_todo_count": pending_todo_count,
"recent_devices": recent_devices,
"recent_contents": recent_contents
"recent_todos": recent_todos
})
@admin_router.get("/devices", response_class=HTMLResponse)
@@ -341,4 +353,262 @@ async def upload_image(request: Request, db: Session = Depends(get_db)):
"request": request,
"devices": devices,
"error": f"图片处理失败: {str(e)}"
})
})
# 待办事项管理路由
@admin_router.get("/todos", response_class=HTMLResponse)
async def todos_list(request: Request, device_id: Optional[str] = None, db: Session = Depends(get_db)):
"""
待办事项列表页面
"""
if device_id:
# 获取特定设备的待办事项
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
todos = db.query(TodoModel).filter(TodoModel.device_id == device_id).order_by(TodoModel.created_at.desc()).all()
return templates.TemplateResponse("admin/todos.html", {
"request": request,
"todos": todos,
"device": device,
"filtered": True
})
else:
# 获取所有待办事项
todos = db.query(TodoModel).order_by(TodoModel.created_at.desc()).all()
devices = db.query(DeviceModel).all()
# 为每个待办事项添加设备信息
todo_list = []
for todo in todos:
device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first()
todo_list.append({
"todo": todo,
"device": device
})
return templates.TemplateResponse("admin/todos.html", {
"request": request,
"todo_list": todo_list,
"devices": devices,
"filtered": False
})
@admin_router.get("/todos/add", response_class=HTMLResponse)
@admin_router.post("/todos/add", response_class=HTMLResponse)
async def add_todo(request: Request, device_id: Optional[str] = None, db: Session = Depends(get_db)):
"""
添加待办事项页面和处理
"""
if request.method == "GET":
devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all()
return templates.TemplateResponse("admin/todo_add.html", {
"request": request,
"devices": devices,
"selected_device": device_id
})
# 处理POST请求
form = await request.form()
device_id = form.get("device_id")
title = form.get("title")
description = form.get("description")
due_date_str = form.get("due_date")
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all()
return templates.TemplateResponse("admin/todo_add.html", {
"request": request,
"devices": devices,
"error": "设备不存在"
})
# 处理截止日期
due_date = None
if due_date_str:
try:
from datetime import datetime
due_date = datetime.strptime(due_date_str, "%Y-%m-%dT%H:%M")
except ValueError:
devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all()
return templates.TemplateResponse("admin/todo_add.html", {
"request": request,
"devices": devices,
"error": "截止日期格式不正确"
})
# 创建新的待办事项
new_todo = TodoModel(
title=title,
description=description,
device_id=device_id,
due_date=due_date
)
db.add(new_todo)
db.commit()
# 发送MQTT通知给设备
mqtt_manager.send_todo_command(device_id, "create", {
"id": new_todo.id,
"title": title,
"description": description,
"due_date": due_date.isoformat() if due_date else None
})
return RedirectResponse(url="/admin/todos", status_code=303)
@admin_router.get("/todos/{todo_id}", response_class=HTMLResponse)
async def todo_detail(request: Request, todo_id: int, db: Session = Depends(get_db)):
"""
待办事项详情页面
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
# 获取设备信息
device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first()
return templates.TemplateResponse("admin/todo_detail.html", {
"request": request,
"todo": todo,
"device": device
})
@admin_router.post("/todos/{todo_id}/toggle", response_class=HTMLResponse)
async def toggle_todo_status(todo_id: int, db: Session = Depends(get_db)):
"""
切换待办事项完成状态
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
# 切换状态
todo.is_completed = not todo.is_completed
if todo.is_completed:
todo.completed_at = datetime.utcnow()
else:
todo.completed_at = None
todo.updated_at = datetime.utcnow()
db.commit()
# 发送MQTT通知给设备
mqtt_manager.send_todo_command(todo.device_id, "update", {
"id": todo.id,
"is_completed": todo.is_completed,
"completed_at": todo.completed_at.isoformat() if todo.completed_at else None
})
return RedirectResponse(url=f"/admin/todos/{todo_id}", status_code=303)
@admin_router.get("/todos/{todo_id}/edit", response_class=HTMLResponse)
async def edit_todo_page(request: Request, todo_id: int, db: Session = Depends(get_db)):
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
devices = db.query(DeviceModel).all()
return templates.TemplateResponse("admin/todo_edit.html", {
"request": request,
"todo": todo,
"devices": devices
})
@admin_router.post("/todos/{todo_id}/edit")
async def edit_todo(
request: Request,
todo_id: int,
title: str = Form(...),
description: str = Form(""),
device_id: str = Form(...),
due_date: Optional[str] = Form(None),
is_completed: bool = Form(False),
db: Session = Depends(get_db)
):
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
# 更新待办事项
todo.title = title
todo.description = description if description else None
todo.device_id = device_id
todo.due_date = datetime.fromisoformat(due_date) if due_date else None
# 检查完成状态变化
was_completed = todo.is_completed
todo.is_completed = is_completed
# 如果从未完成变为已完成,设置完成时间
if not was_completed and is_completed:
todo.completed_at = datetime.utcnow()
# 如果从已完成变为未完成,清除完成时间
elif was_completed and not is_completed:
todo.completed_at = None
todo.updated_at = datetime.utcnow()
db.commit()
# 发送MQTT通知到设备
if hasattr(request.app.state, 'mqtt_manager') and request.app.state.mqtt_manager:
try:
await request.app.state.mqtt_manager.send_todo_command(
device_id=device_id,
action="update",
todo_data={
"id": todo.id,
"title": todo.title,
"description": todo.description,
"due_date": todo.due_date.isoformat() if todo.due_date else None,
"is_completed": todo.is_completed
}
)
except Exception as e:
print(f"发送待办事项更新MQTT消息失败: {e}")
return RedirectResponse(url=f"/admin/todos/{todo_id}", status_code=303)
@admin_router.post("/todos/{todo_id}/delete")
async def delete_todo(
request: Request,
todo_id: int,
db: Session = Depends(get_db)
):
"""
删除待办事项
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
device_id = todo.device_id
# 发送MQTT通知到设备
if hasattr(request.app.state, 'mqtt_manager') and request.app.state.mqtt_manager:
try:
await request.app.state.mqtt_manager.send_todo_command(
device_id=device_id,
action="delete",
todo_data={
"id": todo.id
}
)
except Exception as e:
print(f"发送待办事项删除MQTT消息失败: {e}")
db.delete(todo)
db.commit()
return RedirectResponse(url=f"/admin/todos?device_id={device_id}", status_code=303)

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter
from api import devices, contents
from api import devices, contents, todos
api_router = APIRouter()
# 注册所有路由
api_router.include_router(devices.router)
api_router.include_router(contents.router)
api_router.include_router(contents.router)
api_router.include_router(todos.router)

Binary file not shown.

168
api/todos.py Normal file
View File

@@ -0,0 +1,168 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
from database import get_db
from schemas import Todo as TodoSchema, TodoCreate, TodoUpdate
from models import Todo as TodoModel
from database import Device as DeviceModel
router = APIRouter(
prefix="/api/todos",
tags=["todos"]
)
@router.post("/", response_model=TodoSchema, status_code=status.HTTP_201_CREATED)
async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
"""
创建新的待办事项
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 创建新的待办事项
db_todo = TodoModel(
title=todo.title,
description=todo.description,
device_id=todo.device_id,
due_date=todo.due_date
)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@router.get("/", response_model=List[TodoSchema])
async def list_todos(
skip: int = 0,
limit: int = 100,
device_id: Optional[str] = None,
is_completed: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""
获取待办事项列表
"""
query = db.query(TodoModel)
if device_id:
query = query.filter(TodoModel.device_id == device_id)
if is_completed is not None:
query = query.filter(TodoModel.is_completed == is_completed)
todos = query.order_by(TodoModel.created_at.desc()).offset(skip).limit(limit).all()
return todos
@router.get("/{todo_id}", response_model=TodoSchema)
async def get_todo(todo_id: int, db: Session = Depends(get_db)):
"""
获取待办事项详情
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
return todo
@router.put("/{todo_id}", response_model=TodoSchema)
async def update_todo(
todo_id: int,
todo_update: TodoUpdate,
db: Session = Depends(get_db)
):
"""
更新待办事项
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
# 更新待办事项信息
update_data = todo_update.model_dump(exclude_unset=True)
# 如果状态从未完成变为完成,设置完成时间
if "is_completed" in update_data and update_data["is_completed"] and not todo.is_completed:
update_data["completed_at"] = datetime.utcnow()
# 如果状态从完成变为未完成,清除完成时间
elif "is_completed" in update_data and not update_data["is_completed"] and todo.is_completed:
update_data["completed_at"] = None
for field, value in update_data.items():
setattr(todo, field, value)
todo.updated_at = datetime.utcnow()
db.commit()
db.refresh(todo)
return todo
@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
删除待办事项
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
db.delete(todo)
db.commit()
@router.post("/{todo_id}/complete", response_model=TodoSchema)
async def complete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
标记待办事项为完成
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
todo.is_completed = True
todo.completed_at = datetime.utcnow()
todo.updated_at = datetime.utcnow()
db.commit()
db.refresh(todo)
return todo
@router.post("/{todo_id}/incomplete", response_model=TodoSchema)
async def incomplete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
标记待办事项为未完成
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
todo.is_completed = False
todo.completed_at = None
todo.updated_at = datetime.utcnow()
db.commit()
db.refresh(todo)
return todo

View File

@@ -40,6 +40,22 @@ class Content(Base):
# 关联设备
device = relationship("Device", back_populates="contents")
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
description = Column(Text, nullable=True)
device_id = Column(String(50), ForeignKey("devices.device_id"), nullable=False)
is_completed = Column(Boolean, default=False)
due_date = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联设备
device = relationship("Device")
# 创建数据库连接
engine = create_engine(settings.database_url)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@@ -2,6 +2,6 @@ from sqlalchemy.orm import Session
from database import Base
# 导入所有模型以确保它们被注册到Base.metadata
from database import Device, Content
from database import Device, Content, Todo
__all__ = ["Device", "Content"]
__all__ = ["Device", "Content", "Todo"]

View File

@@ -165,6 +165,43 @@ class MQTTManager:
logger.info(f"已取消订阅设备 {device_id} 状态")
except Exception as e:
logger.error(f"取消订阅设备状态失败: {str(e)}")
def send_todo_command(self, device_id: str, action: str, todo_data: Dict[str, Any]) -> bool:
"""
发送待办事项命令
Args:
device_id: 设备ID
action: 动作类型 (create, update, delete)
todo_data: 待办事项数据
Returns:
是否发送成功
"""
if not self.connected:
logger.error("MQTT未连接无法发送待办事项命令")
return False
try:
topic = f"esp32/{device_id}/todo"
payload = {
"type": "todo",
"action": action,
"data": todo_data,
"timestamp": int(time.time())
}
result = self.client.publish(topic, json.dumps(payload))
if result.rc == mqtt.MQTT_ERR_SUCCESS:
logger.info(f"成功向设备 {device_id} 发送待办事项命令: {action}")
return True
else:
logger.error(f"向设备 {device_id} 发送待办事项命令失败,错误码: {result.rc}")
return False
except Exception as e:
logger.error(f"发送待办事项命令失败: {str(e)}")
return False
# 全局MQTT管理器实例
mqtt_manager = MQTTManager()

View File

@@ -86,4 +86,30 @@ class MQTTStatus(BaseModel):
content_version: Optional[int] = None
timestamp: int = Field(..., description="时间戳")
device_id: str = Field(..., description="设备ID")
message: Optional[str] = None
message: Optional[str] = None
# 待办事项相关模型
class TodoBase(BaseModel):
title: str = Field(..., description="待办事项标题")
description: Optional[str] = Field(None, description="待办事项描述")
due_date: Optional[datetime] = Field(None, description="截止日期")
class TodoCreate(TodoBase):
device_id: str = Field(..., description="设备ID")
class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
due_date: Optional[datetime] = None
is_completed: Optional[bool] = None
class Todo(TodoBase):
id: int
device_id: str
is_completed: bool
completed_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True

View File

@@ -35,6 +35,11 @@
<i class="fas fa-file-image me-1"></i>内容管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/todos' in request.url.path %}active{% endif %}" href="/admin/todos">
<i class="fas fa-tasks me-1"></i>待办事项
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/upload' in request.url.path %}active{% endif %}" href="/admin/upload">
<i class="fas fa-upload me-1"></i>图片上传

View File

@@ -100,20 +100,45 @@
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-play-circle"></i>
<i class="fas fa-tasks"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">活跃内容</p>
<h3 class="card-title">{{ active_content_count }}</h3>
<p class="card-category">待办事项</p>
<h3 class="card-title">{{ todo_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-chart-line"></i> 活跃率: {{ ((active_content_count / content_count * 100) | round(1) if content_count > 0 else 0) }}%
<i class="fas fa-check-circle"></i> 完成率: {{ ((completed_todo_count / todo_count * 100) | round(1) if todo_count > 0 else 0) }}%
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stats bg-danger text-white shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-exclamation-triangle"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">待完成</p>
<h3 class="card-title">{{ pending_todo_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-clock"></i> 待处理任务
</div>
</div>
</div>
@@ -172,36 +197,44 @@
</div>
</div>
<!-- 最近创建的内容 -->
<!-- 最近待办事项 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-clock me-2"></i>最近创建的内容
<i class="fas fa-tasks me-2"></i>最近待办事项
</h6>
<a href="/admin/contents" class="btn btn-sm btn-primary">
<a href="/admin/todos" class="btn btn-sm btn-primary">
<i class="fas fa-list me-1"></i>查看全部
</a>
</div>
<div class="card-body">
{% if recent_contents %}
{% if recent_todos %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th><i class="fas fa-barcode me-1"></i>设备ID</th>
<th><i class="fas fa-heading me-1"></i>标题</th>
<th><i class="fas fa-code-branch me-1"></i>版本</th>
<th><i class="fas fa-mobile-alt me-1"></i>设备</th>
<th><i class="fas fa-check me-1"></i>状态</th>
<th><i class="fas fa-calendar me-1"></i>创建时间</th>
</tr>
</thead>
<tbody>
{% for content in recent_contents %}
{% for item in recent_todos %}
{% set todo = item.todo %}
{% set device = item.device %}
<tr>
<td><a href="/admin/devices/{{ content.device_id }}" class="text-decoration-none"><code>{{ content.device_id }}</code></a></td>
<td><a href="/admin/devices/{{ content.device_id }}/contents/{{ content.version }}" class="text-decoration-none">{{ content.title }}</a></td>
<td><span class="badge bg-info">v{{ content.version }}</span></td>
<td>{{ content.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td><a href="/admin/todos/{{ todo.id }}" class="text-decoration-none">{{ todo.title }}</a></td>
<td><a href="/admin/devices/{{ device.device_id }}" class="text-decoration-none">{{ device.name or device.device_id }}</a></td>
<td>
{% if todo.is_completed %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i>已完成</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>待完成</span>
{% endif %}
</td>
<td>{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
@@ -209,8 +242,8 @@
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无内容</p>
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无待办事项</p>
</div>
{% endif %}
</div>
@@ -240,13 +273,13 @@
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/upload" class="btn btn-success btn-lg btn-block">
<i class="fas fa-upload me-2"></i> 上传图片
<a href="/admin/todos/add" class="btn btn-warning btn-lg btn-block">
<i class="fas fa-plus me-2"></i> 添加待办
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/devices" class="btn btn-warning btn-lg btn-block">
<i class="fas fa-tv me-2"></i> 设备管理
<a href="/admin/todos" class="btn btn-success btn-lg btn-block">
<i class="fas fa-tasks me-2"></i> 待办管理
</a>
</div>
</div>

View File

@@ -0,0 +1,108 @@
{% extends "admin/base.html" %}
{% block title %}添加待办事项{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-plus me-2"></i>添加待办事项
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos{% if device_id %}?device_id={{ device_id }}{% endif %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回列表
</a>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">首页</a></li>
<li class="breadcrumb-item"><a href="/admin/todos{% if device_id %}?device_id={{ device_id }}{% endif %}">待办事项管理</a></li>
<li class="breadcrumb-item active">添加待办事项</li>
</ol>
</nav>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-clipboard-list me-2"></i>待办事项信息
</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/todos/add">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="device_id" class="form-label">关联设备 <span class="text-danger">*</span></label>
<select class="form-select" id="device_id" name="device_id" required>
<option value="">请选择设备</option>
{% for device in devices %}
<option value="{{ device.device_id }}" {% if device_id and device.device_id == device_id %}selected{% endif %}>
{{ device.name or device.device_id }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="due_date" class="form-label">截止时间</label>
<input type="datetime-local" class="form-control" id="due_date" name="due_date">
<div class="form-text">留空表示无截止时间</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_completed" name="is_completed">
<label class="form-check-label" for="is_completed">
标记为已完成
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/admin/todos{% if device_id %}?device_id={{ device_id }}{% endif %}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>取消
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>保存
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle me-2"></i>帮助信息
</h5>
</div>
<div class="card-body">
<p>创建一个新的待办事项:</p>
<ul>
<li><strong>标题:</strong>待办事项的简短描述</li>
<li><strong>描述:</strong>待办事项的详细说明(可选)</li>
<li><strong>关联设备:</strong>选择要显示此待办事项的设备</li>
<li><strong>截止时间:</strong>设置待办事项的截止时间(可选)</li>
<li><strong>状态:</strong>可以选择直接标记为已完成</li>
</ul>
<p class="text-muted">创建后,待办事项将发送到关联的设备,并可以在设备上标记完成状态。</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,161 @@
{% extends "admin/base.html" %}
{% block title %}待办事项详情{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-tasks me-2"></i>待办事项详情
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos{% if todo.device_id %}?device_id={{ todo.device_id }}{% endif %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回列表
</a>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">首页</a></li>
<li class="breadcrumb-item"><a href="/admin/todos{% if todo.device_id %}?device_id={{ todo.device_id }}{% endif %}">待办事项管理</a></li>
<li class="breadcrumb-item active">{{ todo.title }}</li>
</ol>
</nav>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-clipboard-list me-2"></i>待办事项信息
</h5>
<span class="badge {% if todo.is_completed %}bg-success{% else %}bg-warning{% %} fs-6">
{% if todo.is_completed %}已完成{% else %}未完成{% endif %}
</span>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-3 fw-bold">标题:</div>
<div class="col-sm-9">{{ todo.title }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">描述:</div>
<div class="col-sm-9">{{ todo.description or '无' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">关联设备:</div>
<div class="col-sm-9">
<a href="/admin/devices/{{ device.device_id }}" class="text-decoration-none">
{{ device.name or device.device_id }}
</a>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">截止时间:</div>
<div class="col-sm-9">
{% if todo.due_date %}
{{ todo.due_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">创建时间:</div>
<div class="col-sm-9">{{ todo.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">更新时间:</div>
<div class="col-sm-9">{{ todo.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
{% if todo.is_completed and todo.completed_at %}
<div class="row mb-3">
<div class="col-sm-3 fw-bold">完成时间:</div>
<div class="col-sm-9">{{ todo.completed_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
{% endif %}
</div>
<div class="card-footer">
<div class="btn-group">
<a href="/admin/todos/{{ todo.id }}/edit" class="btn btn-primary">
<i class="fas fa-edit me-1"></i>编辑
</a>
<form method="post" action="/admin/todos/{{ todo.id }}/toggle" style="display: inline;">
<button type="submit" class="btn {% if todo.is_completed %}btn-warning{% else %}btn-success{% %}">
<i class="fas {% if todo.is_completed %}fa-undo{% else %}fa-check{% %} me-1"></i>
{% if todo.is_completed %}标记为未完成{% else %}标记为已完成{% endif %}
</button>
</form>
<form method="post" action="/admin/todos/{{ todo.id }}/delete" style="display: inline;" onsubmit="return confirm('确定要删除这个待办事项吗?');">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-1"></i>删除
</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle me-2"></i>设备信息
</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-4 fw-bold">设备ID:</div>
<div class="col-sm-8">{{ device.device_id }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">设备名称:</div>
<div class="col-sm-8">{{ device.name or '未设置' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">设备类型:</div>
<div class="col-sm-8">{{ device.device_type or '未知' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">状态:</div>
<div class="col-sm-8">
{% if device.status == 'online' %}
<span class="badge bg-success">在线</span>
{% else %}
<span class="badge bg-danger">离线</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">最后活跃:</div>
<div class="col-sm-8">
{% if device.last_seen %}
{{ device.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
未知
{% endif %}
</div>
</div>
<div class="text-center mt-3">
<a href="/admin/devices/{{ device.device_id }}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-tv me-1"></i>查看设备详情
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,121 @@
{% extends "admin/base.html" %}
{% block title %}编辑待办事项{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-edit me-2"></i>编辑待办事项
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos/{{ todo.id }}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回详情
</a>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">首页</a></li>
<li class="breadcrumb-item"><a href="/admin/todos{% if todo.device_id %}?device_id={{ todo.device_id }}{% endif %}">待办事项管理</a></li>
<li class="breadcrumb-item"><a href="/admin/todos/{{ todo.id }}">{{ todo.title }}</a></li>
<li class="breadcrumb-item active">编辑</li>
</ol>
</nav>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-clipboard-list me-2"></i>待办事项信息
</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/todos/{{ todo.id }}/edit">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" value="{{ todo.title }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ todo.description or '' }}</textarea>
</div>
<div class="mb-3">
<label for="device_id" class="form-label">关联设备 <span class="text-danger">*</span></label>
<select class="form-select" id="device_id" name="device_id" required>
{% for device in devices %}
<option value="{{ device.device_id }}" {% if device.device_id == todo.device_id %}selected{% endif %}>
{{ device.name or device.device_id }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="due_date" class="form-label">截止时间</label>
<input type="datetime-local" class="form-control" id="due_date" name="due_date"
{% if todo.due_date %}value="{{ todo.due_date.strftime('%Y-%m-%dT%H:%M') }}"{% endif %}>
<div class="form-text">留空表示无截止时间</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_completed" name="is_completed"
{% if todo.is_completed %}checked{% endif %}>
<label class="form-check-label" for="is_completed">
标记为已完成
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/admin/todos/{{ todo.id }}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>取消
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>保存更改
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle me-2"></i>当前信息
</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4 fw-bold">创建时间:</div>
<div class="col-sm-8">{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4 fw-bold">更新时间:</div>
<div class="col-sm-8">{{ todo.updated_at.strftime('%Y-%m-%d %H:%M') }}</div>
</div>
{% if todo.is_completed and todo.completed_at %}
<div class="row mb-2">
<div class="col-sm-4 fw-bold">完成时间:</div>
<div class="col-sm-8">{{ todo.completed_at.strftime('%Y-%m-%d %H:%M') }}</div>
</div>
{% endif %}
<hr>
<p class="text-muted">修改待办事项后,更新将发送到关联的设备。</p>
</div>
</div>
</div>
</div>
{% endblock %}

218
templates/admin/todos.html Normal file
View File

@@ -0,0 +1,218 @@
{% extends "admin/base.html" %}
{% block title %}待办事项管理{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-tasks me-2"></i>待办事项管理
{% if filtered %}
<small class="text-muted">- {{ device.name or device.device_id }}</small>
{% endif %}
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos/add{% if filtered %}?device_id={{ device.device_id }}{% endif %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-plus me-1"></i>添加待办事项
</a>
</div>
{% if not filtered %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-filter me-1"></i>筛选设备
</button>
<ul class="dropdown-menu">
{% for device in devices %}
<li><a class="dropdown-item" href="/admin/todos?device_id={{ device.device_id }}">{{ device.name or device.device_id }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% if filtered %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin/todos">所有待办事项</a></li>
<li class="breadcrumb-item active">{{ device.name or device.device_id }}</li>
</ol>
</nav>
{% endif %}
<div class="row">
<div class="col-12">
{% if filtered %}
{% if todos %}
<div class="card">
<div class="card-header">
<i class="fas fa-list me-2"></i>待办事项列表
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>标题</th>
<th>描述</th>
<th>截止时间</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for todo in todos %}
<tr>
<td>
<a href="/admin/todos/{{ todo.id }}" class="text-decoration-none">
{{ todo.title }}
</a>
</td>
<td>{{ todo.description or '-' }}</td>
<td>
{% if todo.due_date %}
{{ todo.due_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
-
{% endif %}
</td>
<td>
{% if todo.is_completed %}
<span class="badge bg-success">已完成</span>
{% if todo.completed_at %}
<small class="text-muted d-block">{{ todo.completed_at.strftime('%Y-%m-%d %H:%M') }}</small>
{% endif %}
{% else %}
<span class="badge bg-warning">未完成</span>
{% endif %}
</td>
<td>{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/todos/{{ todo.id }}" class="btn btn-outline-primary" title="查看详情">
<i class="fas fa-eye"></i>
</a>
<form method="post" action="/admin/todos/{{ todo.id }}/toggle" style="display: inline;">
<button type="submit" class="btn {% if todo.is_completed %}btn-outline-warning{% else %}btn-outline-success{% endif %}" title="{% if todo.is_completed %}标记为未完成{% else %}标记为已完成{% endif %}">
<i class="fas {% if todo.is_completed %}fa-undo{% else %}fa-check{% endif %}"></i>
</button>
</form>
<form method="post" action="/admin/todos/{{ todo.id }}/delete" style="display: inline;" onsubmit="return confirm('确定要删除这个待办事项吗?');">
<button type="submit" class="btn btn-outline-danger" title="删除">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<h5 class="text-muted">该设备暂无待办事项</h5>
<p class="text-muted">点击上方按钮添加第一个待办事项</p>
<a href="/admin/todos/add?device_id={{ device.device_id }}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>添加待办事项
</a>
</div>
</div>
{% endif %}
{% else %}
{% if todo_list %}
<div class="card">
<div class="card-header">
<i class="fas fa-list me-2"></i>所有待办事项
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>标题</th>
<th>设备</th>
<th>描述</th>
<th>截止时间</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in todo_list %}
<tr>
<td>
<a href="/admin/todos/{{ item.todo.id }}" class="text-decoration-none">
{{ item.todo.title }}
</a>
</td>
<td>
<a href="/admin/devices/{{ item.device.device_id }}" class="text-decoration-none">
{{ item.device.name or item.device.device_id }}
</a>
</td>
<td>{{ item.todo.description or '-' }}</td>
<td>
{% if item.todo.due_date %}
{{ item.todo.due_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.todo.is_completed %}
<span class="badge bg-success">已完成</span>
{% if item.todo.completed_at %}
<small class="text-muted d-block">{{ item.todo.completed_at.strftime('%Y-%m-%d %H:%M') }}</small>
{% endif %}
{% else %}
<span class="badge bg-warning">未完成</span>
{% endif %}
</td>
<td>{{ item.todo.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/todos/{{ item.todo.id }}" class="btn btn-outline-primary" title="查看详情">
<i class="fas fa-eye"></i>
</a>
<form method="post" action="/admin/todos/{{ item.todo.id }}/toggle" style="display: inline;">
<button type="submit" class="btn {% if item.todo.is_completed %}btn-outline-warning{% else %}btn-outline-success{% endif %}" title="{% if item.todo.is_completed %}标记为未完成{% else %}标记为已完成{% endif %}">
<i class="fas {% if item.todo.is_completed %}fa-undo{% else %}fa-check{% endif %}"></i>
</button>
</form>
<form method="post" action="/admin/todos/{{ item.todo.id }}/delete" style="display: inline;" onsubmit="return confirm('确定要删除这个待办事项吗?');">
<button type="submit" class="btn btn-outline-danger" title="删除">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<h5 class="text-muted">暂无待办事项</h5>
<p class="text-muted">点击上方按钮添加第一个待办事项</p>
<a href="/admin/todos/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>添加待办事项
</a>
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endblock %}