diff --git a/backend/models.py b/backend/models.py index f23fd1a..877967c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -95,6 +95,7 @@ class Task(Base): priority: Mapped[int] = mapped_column(Integer, default=0) completed: Mapped[bool] = mapped_column(Boolean, default=False, index=True) due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + due_has_time: Mapped[bool] = mapped_column(Boolean, default=False) version: Mapped[int] = mapped_column(Integer, default=1) position: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/backend/mvp.py b/backend/mvp.py index 8085544..880526b 100644 --- a/backend/mvp.py +++ b/backend/mvp.py @@ -809,7 +809,7 @@ async def export_json(user: User = Depends(current_user), db: AsyncSession = Dep "exported_at": utcnow(), "folders": [serialize(x, ["id", "name", "position", "deleted_at"]) for x in folders], "lists": [serialize(x, ["id", "folder_id", "name", "is_inbox", "position", "deleted_at"]) for x in lists], - "tasks": [serialize(x, ["id", "list_id", "parent_id", "title", "description", "priority", "completed", "due_at", "external_id", "deleted_at"]) for x in tasks], + "tasks": [serialize(x, ["id", "list_id", "parent_id", "title", "description", "priority", "completed", "due_at", "due_has_time", "external_id", "deleted_at"]) for x in tasks], "recurrences": [serialize(x, ["id", "task_id", "rrule", "starts_at", "ends_at"]) for x in recurrences], "habits": [serialize(x, ["id", "name", "kind", "target", "max_value", "schedule_type", "weekdays", "month_days", "interval_days", "start_date", "archived_at", "position"]) for x in habits], "countdowns": [serialize(x, ["id", "title", "event_date", "calendar_mode", "lunar_month", "lunar_day", "ignore_year", "kind", "repeat_rule", "icon", "pinned", "archived_at", "created_at", "updated_at"]) for x in countdowns], @@ -868,6 +868,7 @@ async def restore_json(payload: dict, mode: str = Query("merge", pattern="^(merg priority=raw.get("priority", 0), completed=raw.get("completed", False), due_at=datetime.fromisoformat(raw["due_at"]) if raw.get("due_at") else None, + due_has_time=raw.get("due_has_time", True), external_id=ext, ) db.add(row) diff --git a/backend/schemas.py b/backend/schemas.py index c162995..a8ad84e 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -76,6 +76,7 @@ class TaskCreate(BaseModel): description: str = "" priority: int = Field(default=0, ge=0, le=3) due_at: datetime | None = None + due_has_time: bool = False parent_id: UUID | None = None rrule: str | None = Field(default=None, min_length=5, max_length=1000) @@ -85,13 +86,14 @@ class TaskUpdate(BaseModel): description: str | None = None priority: int | None = Field(default=None, ge=0, le=3) due_at: datetime | None = None + due_has_time: bool | None = None completed: bool | None = None list_id: UUID | None = None version: int = Field(ge=1) @model_validator(mode="after") def reject_null_non_nullable_fields(self): - for field in ("title", "description", "priority", "completed", "list_id"): + for field in ("title", "description", "priority", "completed", "list_id", "due_has_time"): if field in self.model_fields_set and getattr(self, field) is None: raise ValueError(f"{field} cannot be null") return self @@ -117,6 +119,7 @@ class TaskOut(BaseModel): priority: int completed: bool due_at: datetime | None + due_has_time: bool version: int diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a32676b..0a00843 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,7 +14,7 @@ import FloatingAddButton from './components/FloatingAddButton.vue' type FolderItem = { id: string; name: string } type TaskList = { id: string; folder_id: string | null; name: string; is_inbox: boolean } -type Task = { id: string; list_id: string; parent_id: string | null; title: string; description: string; priority: number; completed: boolean; version: number; due_at: string | null; subtasks?: Task[] } +type Task = { id: string; list_id: string; parent_id: string | null; title: string; description: string; priority: number; completed: boolean; version: number; due_at: string | null; due_has_time: boolean; subtasks?: Task[] } type RepeatOption = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom' type Recurrence = { id: string; task_id: string; rrule: string } type View = 'tasks' | 'today' | 'upcoming' | 'trash' | 'habits' | 'countdowns' | 'settings' @@ -59,6 +59,8 @@ const taskComposeOpen = ref(false) const composeTitle = ref('') const composeListId = ref('') const composeDueAt = ref('') +const composeHasTime = ref(false) +const composeTime = ref('12:00') const composePriority = ref(0) const composeDescription = ref('') const composeRepeat = ref('none') @@ -81,6 +83,8 @@ function openTaskCompose() { composeTitle.value = '' composeListId.value = activeView.value === 'tasks' && activeList.value ? activeList.value : inboxId composeDueAt.value = defaultTaskDueAt() + composeHasTime.value = false + composeTime.value = '12:00' composePriority.value = 0 composeDescription.value = '' composeRepeat.value = 'none' @@ -147,10 +151,12 @@ async function submitTaskCompose() { if (!taskTitle || !composeListId.value) return try { const rrule = composeRepeat.value === 'none' ? null : repeatRrule(composeRepeat.value, composeRepeatConfig.value) + const dueValue = composeDueAt.value ? `${composeDueAt.value}T${composeHasTime.value ? composeTime.value : '23:59'}` : '' const task = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: taskTitle, list_id: composeListId.value, - due_at: fromDateTimeLocal(composeDueAt.value), + due_at: fromDateTimeLocal(dueValue), + due_has_time: composeHasTime.value, priority: composePriority.value, description: composeDescription.value, rrule, @@ -627,7 +633,13 @@ async function restoreList(item: TaskList) { try { await api(`/lists/${item.id}/restore`, { method: 'POST' }); await loadArchivedLists(); await refreshAll(); toast('清单已恢复') } catch (reason) { fail(reason) } } function toggleFolder(id: string) { const next = new Set(expandedFolders.value); next.has(id) ? next.delete(id) : next.add(id); expandedFolders.value = next } -function formatDue(value: string | null) { return value ? new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)) : '' } +function formatDue(value: string | null, hasTime = true) { + if (!value) return '' + const date = new Date(value) + return hasTime + ? new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date) + : new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date) +} function previousPage() { if (page.value <= 1 || loading.value) return page.value -= 1 @@ -701,7 +713,7 @@ onMounted(bootstrap)

已过期 {{overdueTaskTree.length}}

@@ -714,7 +726,7 @@ onMounted(bootstrap)
-
{{node.task.title}}{{formatDue(node.task.due_at)}}{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}
+
{{node.task.title}}{{formatDue(node.task.due_at,node.task.due_has_time)}}{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}
{{['','低','中','高'][node.task.priority]}} @@ -759,7 +771,9 @@ onMounted(bootstrap)
NEW TASK

{{ taskComposeTitle }}

- + + +
每隔