5c028d7952
包含 FastAPI 后端、React 前端、队列/OCR/标签/待办等完整功能。 Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.0 KiB
Python
107 lines
3.0 KiB
Python
"""待办清单接口。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import and_, func, or_, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import db_session
|
|
from app.models.todo import Todo, TodoStatus
|
|
from app.schemas.common import TodoUpdate
|
|
from app.schemas.screenshot import TodoBrief, TodoListResp
|
|
|
|
|
|
router = APIRouter(prefix="/api/todos", tags=["todos"])
|
|
|
|
|
|
def _todo_filters(
|
|
status: Optional[str],
|
|
kind: Optional[str],
|
|
q: Optional[str],
|
|
) -> list:
|
|
"""构建待办筛选条件。"""
|
|
filters = []
|
|
if status:
|
|
filters.append(Todo.status == status)
|
|
if kind:
|
|
filters.append(Todo.kind == kind)
|
|
if q:
|
|
like = f"%{q.strip()}%"
|
|
filters.append(or_(Todo.title.ilike(like), Todo.note.ilike(like)))
|
|
return filters
|
|
|
|
|
|
@router.get("", response_model=TodoListResp)
|
|
def list_todos(
|
|
session: Session = Depends(db_session),
|
|
status: Optional[str] = Query(None),
|
|
kind: Optional[str] = Query(None),
|
|
q: Optional[str] = Query(None, description="标题/备注关键词"),
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(50, ge=1, le=200),
|
|
) -> TodoListResp:
|
|
"""按状态/类型/关键词分页查询。"""
|
|
filters = _todo_filters(status, kind, q)
|
|
base = select(Todo)
|
|
if filters:
|
|
base = base.where(and_(*filters))
|
|
|
|
total = session.scalar(select(func.count()).select_from(base.subquery())) or 0
|
|
rows = session.scalars(
|
|
base.order_by(Todo.created_at.desc()).offset((page - 1) * size).limit(size)
|
|
).all()
|
|
return TodoListResp(
|
|
items=[TodoBrief.model_validate(r) for r in rows],
|
|
total=int(total),
|
|
page=page,
|
|
size=size,
|
|
)
|
|
|
|
|
|
@router.get("/summary")
|
|
def summary(session: Session = Depends(db_session)) -> dict:
|
|
"""各状态待办数量。"""
|
|
return {
|
|
st.value: session.scalar(select(func.count(Todo.id)).where(Todo.status == st.value)) or 0
|
|
for st in TodoStatus
|
|
}
|
|
|
|
|
|
@router.patch("/{todo_id}", response_model=TodoBrief)
|
|
def update_todo(
|
|
todo_id: int,
|
|
payload: TodoUpdate,
|
|
session: Session = Depends(db_session),
|
|
) -> TodoBrief:
|
|
"""更新状态/标题/备注。"""
|
|
todo = session.get(Todo, todo_id)
|
|
if todo is None:
|
|
raise HTTPException(404, "Todo not found")
|
|
if payload.status is not None:
|
|
todo.status = payload.status
|
|
if payload.status == TodoStatus.DONE.value:
|
|
todo.completed_at = datetime.utcnow()
|
|
if payload.title is not None:
|
|
todo.title = payload.title
|
|
if payload.note is not None:
|
|
todo.note = payload.note
|
|
session.commit()
|
|
session.refresh(todo)
|
|
return TodoBrief.model_validate(todo)
|
|
|
|
|
|
@router.delete("/{todo_id}")
|
|
def delete_todo(
|
|
todo_id: int,
|
|
session: Session = Depends(db_session),
|
|
) -> dict:
|
|
todo = session.get(Todo, todo_id)
|
|
if todo is None:
|
|
raise HTTPException(404, "Todo not found")
|
|
session.delete(todo)
|
|
session.commit()
|
|
return {"ok": True}
|