From 0dd67f2824d8e7a7fc671cca3cc916973dec6945 Mon Sep 17 00:00:00 2001 From: bboysoul Date: Tue, 8 Sep 2026 08:53:02 +0800 Subject: [PATCH] feat: add custom task recurrence options --- backend/main.py | 9 ++++++++- backend/schemas.py | 1 + frontend/src/App.vue | 3 ++- frontend/src/style.test.ts | 2 +- tests/test_mvp_backend.py | 23 +++++++++++++++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index 8360b78..5e770ff 100644 --- a/backend/main.py +++ b/backend/main.py @@ -468,7 +468,12 @@ async def create_task( ) if parent is None: raise HTTPException(status_code=400, detail="父任务必须是同一清单的顶层任务") - data = payload.model_dump() + if payload.rrule: + if not payload.due_at: + raise HTTPException(status_code=422, detail="重复任务需要截止时间") + from .mvp import parse_rrule + parse_rrule(payload.rrule) + data = payload.model_dump(exclude={"rrule"}) parent_filter = Task.parent_id == payload.parent_id if payload.parent_id else Task.parent_id.is_(None) max_position = await db.scalar(select(func.max(Task.position)).where( Task.user_id == user.id, @@ -479,6 +484,8 @@ async def create_task( task = Task(user_id=user.id, position=(max_position if max_position is not None else -1) + 1, **data) db.add(task) await db.flush() + if payload.rrule: + db.add(RecurrenceTemplate(user_id=user.id, task_id=task.id, rrule=payload.rrule.upper(), starts_at=task.due_at)) audit(db, user.id, "create", "task", task.id) await db.commit() await db.refresh(task) diff --git a/backend/schemas.py b/backend/schemas.py index 519bbf1..c162995 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -77,6 +77,7 @@ class TaskCreate(BaseModel): priority: int = Field(default=0, ge=0, le=3) due_at: datetime | None = None parent_id: UUID | None = None + rrule: str | None = Field(default=None, min_length=5, max_length=1000) class TaskUpdate(BaseModel): diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 50f6393..5130b0e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -145,14 +145,15 @@ async function submitTaskCompose() { const taskTitle = composeTitle.value.trim() if (!taskTitle || !composeListId.value) return try { + const rrule = composeRepeat.value === 'none' ? null : repeatRrule(composeRepeat.value, composeRepeatConfig.value) const task = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: taskTitle, list_id: composeListId.value, due_at: fromDateTimeLocal(composeDueAt.value), priority: composePriority.value, description: composeDescription.value, + rrule, }) }) - if (composeRepeat.value !== 'none') await api('/recurrences', { method: 'POST', body: JSON.stringify({ task_id: task.id, rrule: repeatRrule(composeRepeat.value, composeRepeatConfig.value) }) }) if (isTaskView(activeView.value)) { tasks.value.push(task) totalTasks.value = nextTotalAfterLocalTaskAdd(totalTasks.value) diff --git a/frontend/src/style.test.ts b/frontend/src/style.test.ts index 44472eb..e7d1d80 100644 --- a/frontend/src/style.test.ts +++ b/frontend/src/style.test.ts @@ -177,7 +177,7 @@ describe('unified floating add interaction', () => { expect(app).toContain('const selectedTaskRepeat = ref') expect(app).toContain('重复