From bb04bd8fa50a1d9f5cf4c075cec82cad047f75b4 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Sun, 16 Nov 2025 17:36:42 +0800 Subject: [PATCH] todo_list --- __pycache__/admin_routes.cpython-312.pyc | Bin 16310 -> 28619 bytes __pycache__/config.cpython-312.pyc | Bin 1565 -> 1565 bytes __pycache__/database.cpython-312.pyc | Bin 3150 -> 3935 bytes __pycache__/image_processor.cpython-312.pyc | Bin 5679 -> 5679 bytes __pycache__/main.cpython-312.pyc | Bin 4483 -> 4483 bytes __pycache__/models.cpython-312.pyc | Bin 325 -> 328 bytes __pycache__/mqtt_manager.cpython-312.pyc | Bin 9196 -> 10828 bytes __pycache__/schemas.cpython-312.pyc | Bin 5461 -> 6949 bytes admin_routes.py | 286 +++++++++++++++++++- api/__init__.py | 5 +- api/__pycache__/__init__.cpython-312.pyc | Bin 459 -> 550 bytes api/__pycache__/contents.cpython-312.pyc | Bin 13120 -> 13120 bytes api/__pycache__/devices.cpython-312.pyc | Bin 8111 -> 8111 bytes api/__pycache__/todos.cpython-312.pyc | Bin 0 -> 7407 bytes api/todos.py | 168 ++++++++++++ database.py | 16 ++ models.py | 4 +- mqtt_manager.py | 37 +++ schemas.py | 28 +- templates/admin/base.html | 5 + templates/admin/dashboard.html | 75 +++-- templates/admin/todo_add.html | 108 ++++++++ templates/admin/todo_detail.html | 161 +++++++++++ templates/admin/todo_edit.html | 121 +++++++++ templates/admin/todos.html | 218 +++++++++++++++ 25 files changed, 1198 insertions(+), 34 deletions(-) create mode 100644 api/__pycache__/todos.cpython-312.pyc create mode 100644 api/todos.py create mode 100644 templates/admin/todo_add.html create mode 100644 templates/admin/todo_detail.html create mode 100644 templates/admin/todo_edit.html create mode 100644 templates/admin/todos.html diff --git a/__pycache__/admin_routes.cpython-312.pyc b/__pycache__/admin_routes.cpython-312.pyc index 4bd126b7ca89285af46fd30627201a13717161ae..703d6796a742ee5aa0d9496cb614a46559ec4d5b 100644 GIT binary patch literal 28619 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 Af&c&j delta 3358 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 delta 19 ZcmZoxZdT?x&CAQh00bxdHgZ)80st@R1mXYy diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index abf0787496d42b61bdd1ad72f7c083bf6c3e1ba2..3ed9b869791dd4d450973eb1b76e017f966ce4a8 100644 GIT binary patch delta 128 zcmX@gbb^WZG%qg~0}#~Akjy+ckynz@VxqdXP%c{(8zVzHLkjaE#whkm7ERWP36j#x zx7b`#%QBNwZ?QY)=ar=9mE2+p$xq4m)8v>qLyxJ5ed6ITEk2-Oj6hs00VF;!Gcq#X bWl*}yAo-n*fl>P+gVrN1^+xt09-t%ubLko zx%YXV`(B^-dG3C0`6gic+Gx~caD6w+3{1H*rWbI%4(;K(^8twBsja+D(DPRKvk7H_ zUNH0#3=HxH!Q7|g?I+2@ju8h(&#Q=~cnIlCTG~zCV z_W5?#_iFqgsAYqACzxV?VuH}0(}a4Zfmk$0Nijaw&4;8=F9~kC>_im-Qnn5c$*U7Y zLM3Tova%$qbQRWt3~2*5D6RP-UUc%Bv*egM0cJ=?RoGZcd|(r#WG?N(7BFL~*mjX9 zI>s~!P1srrKCqI#bRDKa$nL`#aC4K5(I)US7_UnZXRtTP(&KS#gaR4+BV~FeT80#X zMeDT^K1_0zbT(t+<742Goi!RHPsy3k0^G{f8^^Tc+7W_R>@Yrzy{hWOhH=gmCt!(s z_WsIGr`C_3$bb4;e&XEP$CK+9J_1LW)`P{fY+&;f>9s3of5Oc8y!juhx8AsPYwCTl?x+L5GgczL9n{hl$$vdMpSrTPIM*yT z!&*kwPavrri3kT^AM}ac5IGX{R@{&FEf8@x1p9CuMX?4pAU=<>YW^cjh01-8b|+aF z{6B^KT;|8iZ#S^*oLRKMJaG#`HNpXe00No?u@zw_L{81eBgclsTI4{(Bko1mits1` z2f^t!hB{fSOneE&b|BOvv>`kOF@#>J`we47G)B2iLqe4ARt~#27KwzS{HVvjFA(sD zqM?34-2PDbU8M61#Br?P#4Psd@rm(OgLS&?^)~qsvuDN7nzcC#q>W;-bj57v?B4Tn z*-c=7fXMcyez`W&N_SD>_)a9mAEd>)syG{pF!RwCdYmVx)<1+c7 zKBHfFdilx5Wk(bEv%D%P*9YW|pd2_NSG>4l>B^S7*DUmDL&`8anOxh^F4ynMaq z=(V1sa%fQYL{==(Y16>_P=t2;{J#Quc zIdD}0{~gStZpI6neQGBCQ~`r#L8X`_a_NA)zhmjpS0-gI2fKemVUb1>We^-)90BcM ztiOLC+Ajvcw^i|7eJHL>4ob=yBCkkNH*6bfmKa7sMM3GUZf7LM#|MRHMF|Gb2WhAa Zob>sbB!M%xF@;SU%;KILJ7G}n`x}T_qPzeA delta 324 zcmX>T^2VL-G%qg~0}yERNM>e=ZR87M@r%evj zn5^`LL4ehb5uykruPIh!1Eg-T=j5lSXXd3B1x+^8EOm$lX;lRgAVs%$atljJ;&T)8 z64O(QiV{KMaBFUH*yQG?l;)(`6=ee@7=gGrVe&IgU+E|&MmNTf3=pb{g+V~NyS9-B GtP=n^$w>n3g`YBUw8e?cu>XQ1`3WzsHo zU$(n$*q{%lo`n_^Ta|)8>`$pkRdiureNhrI&BMaN8n(~I_r2#}t-gVQk}ju^MOBGB*5P>}O+5#=7|dBD%;OP^bo3sfE0mpOd~JVmmv+ZU^1-EofdJ z%y|AG+u=v~kl2Ba4)|McBONeIb=c@#SRF^wfzX1m1HlQKXh4ji83U~@PGS(R)nU-D zK+IbY({N&!gHz#&P=Tfv z!_BwVh3&=gp7C9|f&BRw-Ze$K$JSU9yNCDwd(Gy02|NATwpc|`dkgw_XDC_9-in&( z2arYRM`IfTPjUe73poK7X|mfSY009N`<;>wt|T$UGS7c-7&}gJy|^wm_E`1|X8KMJ zogF@7jaa7PfhTT5S#BQBIEYY>6{x*vBA9Gt@XBTdSWrUYaAc*o0*#c|>)w3x)Rz;{ zB8@B;*^8Z$0l494J!jH%t>v^_yhv*DA}yCiO%coGRxMh5x}0pfT=!P7SN5c(G0Lt5 zMX~HgMl9bcEHPE~tn|>=KVpZ`+r$7iimeVeegP-su%1R9*9HVGpusG|4OitR@d%O* zcr1t752J~2imeRFofS$>_k~2M&VM0Q@@oaEl_D*?)3z7>rUGp#DZv5_Uh!(R_+82f zLQw6A@|oxJ9b>G`Cz8(=9|C^0_^;*a9)=&(kaU~{61Fn0;I?|Jvnn{q+Nx4Rk|Z}) j6>zUggtX;@Re^0)k&@=TUKQ9@^|(kwuI;VBHs0O;pb0ZW delta 404 zcmZ2#c2$e-G%qg~0}yERNM`Qhn#d=?XtPn>mXW1WMpJh40Y)}XCQbIq>xD%p-{sO~ zEZ)q?oy5py4pd)cKDm&mjM04Z27bxOKUh^KtMP7Tw3&R1H*9h?59ee{KGVt1c?2id z^Bokh2PtO-5!xWaVR8V!1e+a*X*W5K-;vP{XvRTCtI0(Iv5d}>F9@i!Ie{ddCjS-4 zW^{sDATYUxOK$Q8!3akC$(%x}@@^m{!XQEfM4;K>KKUT8Fq{>>SU@@=~%~en1&UATC}y*-dIO*H;EsMx`P#pa1~Z C9bNGN diff --git a/admin_routes.py b/admin_routes.py index 2f2a4a8..8f324cd 100644 --- a/admin_routes.py +++ b/admin_routes.py @@ -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)}" - }) \ No newline at end of file + }) + +# 待办事项管理路由 +@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) \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py index dd27b23..abb40a1 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -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) \ No newline at end of file +api_router.include_router(contents.router) +api_router.include_router(todos.router) \ No newline at end of file diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc index bc30392ee17bbf70e98b8d9feecaf067212aed5c..6178234fd96ec5ee7da34693def2423bd58c2aac 100644 GIT binary patch delta 220 zcmX@jyo`nSG%qg~0}!m9A({DkBCjOlm5J)|dbzAotc(my45^H(L6Sfa#g@vJ#SdX4 zlc~&E0uu{lnSqiMCx|kF*pk>4vVs)KOgtnm$9#)DCABOwIkor}M{<5%Norn6@h#Sp z{FMA+KTXbwA7q(|*d}u^mP>=|V+7)2Um)>;nURt4E`#iS291jh8k1)-YDryT(7M6E Zb)7--B7@`%)%iLzb*?g~7x4lW0{{r%Fo^&F delta 172 zcmZ3+a+;aYofZm79#@_Ln`BHkOUA!v8J+S@j}?hWGZtO z-^2$pQaEH;K(fZ-Ot;umQp+-vQ;TnLB diff --git a/api/__pycache__/contents.cpython-312.pyc b/api/__pycache__/contents.cpython-312.pyc index 6d35d264f29b3934f822ee7fa6d72d7431096472..cc16a335563477e9226fe1e73b87560a0b5aad4b 100644 GIT binary patch delta 19 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 literal 0 HcmV?d00001 diff --git a/api/todos.py b/api/todos.py new file mode 100644 index 0000000..fbeb654 --- /dev/null +++ b/api/todos.py @@ -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 \ No newline at end of file diff --git a/database.py b/database.py index cd5fd0d..c1b4ea1 100644 --- a/database.py +++ b/database.py @@ -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) diff --git a/models.py b/models.py index c180430..bc29898 100644 --- a/models.py +++ b/models.py @@ -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"] \ No newline at end of file +__all__ = ["Device", "Content", "Todo"] \ No newline at end of file diff --git a/mqtt_manager.py b/mqtt_manager.py index d1306fb..d8eb282 100644 --- a/mqtt_manager.py +++ b/mqtt_manager.py @@ -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() \ No newline at end of file diff --git a/schemas.py b/schemas.py index 9bd92a5..982caed 100644 --- a/schemas.py +++ b/schemas.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/templates/admin/base.html b/templates/admin/base.html index 1b6a294..06a0394 100644 --- a/templates/admin/base.html +++ b/templates/admin/base.html @@ -35,6 +35,11 @@ 内容管理 +