diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4713e38..2680927 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -137,6 +137,8 @@ const selectedTaskRepeat = ref('none') const selectedAfterCompletionDays = ref('1') const selectedRepeatError = ref('') const selectedTaskRecurrence = ref(null) +const recurrenceLoading = ref(false) +const savingSelectedTask = ref(false) const defaultRepeatConfig = (): TaskRepeatConfig => ({ frequency: 'daily', interval: 1, weekdays: [], monthDays: [], endMode: 'never', count: 10, until: '' }) const composeRepeatConfig = ref(defaultRepeatConfig()) const selectedRepeatConfig = ref(defaultRepeatConfig()) @@ -194,23 +196,21 @@ function activateFloatingAdd(origin: { x: number; y: number }) { else if (activeView.value === 'countdowns') countdownComposer.value?.openCountdownComposer(origin) else if (['tasks', 'today', 'upcoming'].includes(activeView.value)) openTaskCompose() } -async function saveRepeat(task: Task, value: RepeatOption, config = selectedRepeatConfig.value) { +async function saveRepeat(task: Task, value: RepeatOption, config: TaskRepeatConfig, afterCompletionDays: string, recurrence: Recurrence | null) { if (value !== 'none' && !task.due_at) throw new Error('请先设置截止时间') if (value === 'none') { - if (selectedTaskRecurrence.value) await api(`/recurrences/${selectedTaskRecurrence.value.id}`, { method: 'DELETE' }) - selectedTaskRecurrence.value = null - return + if (recurrence) await api(`/recurrences/${recurrence.id}`, { method: 'DELETE' }) + return null } - const recurrencePayload = buildTaskRecurrencePayload(value, { afterCompletionDays: selectedAfterCompletionDays.value, repeatConfig: config }) - if (selectedTaskRecurrence.value) { - const updated = await api(`/recurrences/${selectedTaskRecurrence.value.id}`, { method: 'PATCH', body: JSON.stringify(recurrencePayload) }) as Recurrence - selectedTaskRecurrence.value = updated - } else { - selectedTaskRecurrence.value = await api('/recurrences', { method: 'POST', body: JSON.stringify({ task_id: task.id, ...recurrencePayload }) }) as Recurrence + const recurrencePayload = buildTaskRecurrencePayload(value, { afterCompletionDays, repeatConfig: config }) + if (recurrence) { + return await api(`/recurrences/${recurrence.id}`, { method: 'PATCH', body: JSON.stringify(recurrencePayload) }) as Recurrence } + return await api('/recurrences', { method: 'POST', body: JSON.stringify({ task_id: task.id, ...recurrencePayload }) }) as Recurrence } async function loadTaskRecurrence(task: Task) { const token = ++recurrenceLoadToken + recurrenceLoading.value = true selectedTaskRecurrence.value = null selectedTaskRepeat.value = 'none' selectedAfterCompletionDays.value = '1' @@ -223,14 +223,12 @@ async function loadTaskRecurrence(task: Task) { selectedTaskRepeat.value = parsed.option selectedAfterCompletionDays.value = String(parsed.afterCompletionDays) selectedRepeatConfig.value = recurrence?.rrule ? parseTaskRrule(recurrence.rrule) : defaultRepeatConfig() - } catch (reason) { if (token === recurrenceLoadToken) fail(reason) } + } catch (reason) { + if (token === recurrenceLoadToken) fail(reason) + } finally { + if (token === recurrenceLoadToken) recurrenceLoading.value = false + } } -async function updateSelectedTaskRepeat() { - if (!selectedTask.value) return - try { selectedRepeatError.value = ''; await saveRepeat(selectedTask.value, selectedTaskRepeat.value); toast('重复设置已保存') } - catch (reason) { selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败'; fail(reason) } -} - async function submitTaskCompose() { const normalized = normalizeRequiredName(composeTitle.value) if (normalized.error) { @@ -619,7 +617,7 @@ async function switchView(view: View, listId?: string) { async function loadTodayView() { await loadAll() } -function applyTaskUpdate(task: Task, updated: Task) { +function applyTaskUpdate(task: Task, updated: Task, updateSelected = true) { const index = tasks.value.findIndex((item) => item.id === task.id) if (index >= 0) tasks.value[index] = { ...tasks.value[index], ...updated } const overdueIndex = overdueTasks.value.findIndex((item) => item.id === task.id) @@ -627,11 +625,11 @@ function applyTaskUpdate(task: Task, updated: Task) { if (updated.completed && !completionExitingTaskIds.value.has(task.id)) overdueTasks.value.splice(overdueIndex, 1) else overdueTasks.value[overdueIndex] = { ...overdueTasks.value[overdueIndex], ...updated } } - if (selectedTask.value?.id === task.id) selectedTask.value = { ...selectedTask.value, ...updated } + if (updateSelected && selectedTask.value?.id === task.id) selectedTask.value = { ...selectedTask.value, ...updated } } -async function patchTask(task: Task, patch: Partial) { +async function patchTask(task: Task, patch: Partial, updateSelected = true) { const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) }) as Task - applyTaskUpdate(task, updated) + applyTaskUpdate(task, updated, updateSelected) return updated } const taskMutationNavigation = ref(0) @@ -842,30 +840,54 @@ async function refreshTodayAfterTaskSave() { loadTodayTaskSummary(), ]) } -async function saveTask() { - if (!selectedTask.value) return - const normalized = normalizeRequiredName(selectedTask.value.title) +async function saveTask(options?: { showSuccess?: boolean, expectedTaskId?: string, expectedSelectionToken?: number }) { + const task = selectedTask.value + if (!task || (options?.expectedTaskId && task.id !== options.expectedTaskId)) return false + const normalized = normalizeRequiredName(task.title) if (normalized.error) { error.value = normalized.error - return + return false } - selectedTask.value.title = normalized.value + task.title = normalized.value try { - const task = selectedTask.value const dueAt = fromDateTimeLocal(toDateTimeLocal(task.due_at)) - const updated = await patchTask(task, { title: task.title.trim(), description: task.description, priority: Number(task.priority), due_at: dueAt, list_id: task.list_id } as Partial) - if (selectedTaskRecurrence.value) { - if (dueAt) { - await api(`/recurrences/${selectedTaskRecurrence.value.id}`, { method: 'PATCH', body: JSON.stringify({ due_at: dueAt }) }) - } else { - selectedTaskRecurrence.value = null - selectedTaskRepeat.value = 'none' - } - } - if (selectedTask.value) selectedTask.value = { ...selectedTask.value, ...updated } + const updated = await patchTask(task, { title: task.title.trim(), description: task.description, priority: Number(task.priority), due_at: dueAt, list_id: task.list_id } as Partial, false) + const selectionMatches = options?.expectedSelectionToken === undefined || recurrenceLoadToken === options.expectedSelectionToken + if (selectedTask.value?.id === task.id && selectionMatches) selectedTask.value = { ...selectedTask.value, ...updated } await refreshTodayAfterTaskSave() + if (options?.showSuccess !== false) toast('已保存') + return updated + } catch (reason) { + fail(reason) + return false + } +} +async function saveSelectedTaskChanges() { + if (savingSelectedTask.value || recurrenceLoading.value) return + const taskId = selectedTask.value?.id + if (!taskId) return + savingSelectedTask.value = true + const selectionToken = recurrenceLoadToken + const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none' + const repeatConfig = structuredClone(selectedRepeatConfig.value) + const afterCompletionDays = selectedAfterCompletionDays.value + const recurrence = selectedTaskRecurrence.value + selectedRepeatError.value = '' + try { + const taskSaved = await saveTask({ showSuccess: false, expectedTaskId: taskId, expectedSelectionToken: selectionToken }) + if (!taskSaved || recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return + const repeatRecurrence = taskSaved.due_at ? recurrence : null + const updatedRecurrence = await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, repeatRecurrence) + if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return + selectedTaskRecurrence.value = updatedRecurrence toast('已保存') - } catch (reason) { fail(reason) } + } catch (reason) { + if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return + selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败' + fail(reason) + } finally { + savingSelectedTask.value = false + } } async function removeTask(task: Task) { if (!window.confirm(`把“${task.title}”移到回收站?`)) return @@ -1384,19 +1406,19 @@ onUnmounted(() => {