feat: refine task and countdown interactions
This commit is contained in:
+42
-12
@@ -5,7 +5,7 @@ import {
|
||||
Ellipsis, GripVertical, Heading2, Inbox, Italic, Link, List, ListChecks, ListOrdered, ListTodo, Menu, Pencil, Plus, Quote, Search,
|
||||
Settings, Trash2, X, Repeat2, RefreshCw, StickyNote,
|
||||
} from 'lucide-vue-next'
|
||||
import { applyMarkdownFormat, buildTaskRecurrencePayload, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskRecurrence, parseTaskRrule, renderMarkdown, toDateTimeLocal, type MarkdownFormat, type TaskRepeatConfig, type TaskRepeatOption } from './lib/task-utils'
|
||||
import { applyMarkdownFormat, buildTaskDueDraft, buildTaskRecurrencePayload, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskDueDraft, parseTaskRecurrence, parseTaskRrule, renderMarkdown, type MarkdownFormat, type TaskRepeatConfig, type TaskRepeatOption } from './lib/task-utils'
|
||||
import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, normalizeRequiredName, performTrashMutation, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
import { createCompletionPulse, shouldAnimateCompletionExit, waitForCompletionExit } from './lib/completion-motion'
|
||||
@@ -143,6 +143,10 @@ const composeTime = ref('12:00')
|
||||
const composeCalendarOpen = ref(false)
|
||||
const composeDateButton = ref<HTMLButtonElement | null>(null)
|
||||
const composeTimePicker = ref<HTMLInputElement | null>(null)
|
||||
const selectedDueDate = ref('')
|
||||
const selectedDueHasTime = ref(false)
|
||||
const selectedDueTime = ref('12:00')
|
||||
const selectedDueTimePicker = ref<HTMLInputElement | null>(null)
|
||||
const composePriority = ref(0)
|
||||
const composeDescription = ref('')
|
||||
const composeRepeat = ref<RepeatOption>('none')
|
||||
@@ -917,11 +921,14 @@ async function saveTask(options?: { showSuccess?: boolean, expectedTaskId?: stri
|
||||
return false
|
||||
}
|
||||
task.title = normalized.value
|
||||
const due = buildTaskDueDraft({ date: selectedDueDate.value, hasTime: selectedDueHasTime.value, time: selectedDueTime.value })
|
||||
try {
|
||||
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<Task>, false)
|
||||
const updated = await patchTask(task, { title: task.title.trim(), description: task.description, priority: Number(task.priority), ...due, list_id: task.list_id } as Partial<Task>, false)
|
||||
const selectionMatches = options?.expectedSelectionToken === undefined || recurrenceLoadToken === options.expectedSelectionToken
|
||||
if (selectedTask.value?.id === task.id && selectionMatches) selectedTask.value = { ...selectedTask.value, ...updated }
|
||||
if (selectedTask.value?.id === task.id && selectionMatches) {
|
||||
selectedTask.value = { ...selectedTask.value, ...updated }
|
||||
selectedDueHasTime.value = Boolean(updated.due_has_time)
|
||||
}
|
||||
await refreshTodayAfterTaskSave()
|
||||
if (options?.showSuccess !== false) toast('已保存')
|
||||
return updated
|
||||
@@ -936,18 +943,22 @@ async function saveSelectedTaskChanges() {
|
||||
if (!taskId) return
|
||||
savingSelectedTask.value = true
|
||||
const selectionToken = recurrenceLoadToken
|
||||
const repeatValue = selectedTask.value?.due_at ? selectedTaskRepeat.value : 'none'
|
||||
const repeatConfig = structuredClone(selectedRepeatConfig.value)
|
||||
const repeatValue = selectedDueDate.value ? selectedTaskRepeat.value : 'none'
|
||||
const repeatConfig = JSON.parse(JSON.stringify(selectedRepeatConfig.value)) as TaskRepeatConfig
|
||||
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
|
||||
if (!taskSaved.due_at) {
|
||||
selectedTaskRecurrence.value = null
|
||||
selectedTaskRepeat.value = 'none'
|
||||
} else {
|
||||
const updatedRecurrence = await saveRepeat(taskSaved, repeatValue, repeatConfig, afterCompletionDays, recurrence)
|
||||
if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return
|
||||
selectedTaskRecurrence.value = updatedRecurrence
|
||||
}
|
||||
toast('已保存')
|
||||
} catch (reason) {
|
||||
if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return
|
||||
@@ -1002,8 +1013,24 @@ function closeTaskDetail() {
|
||||
mobileDetail.value = false
|
||||
selectedTask.value = null
|
||||
}
|
||||
function addSelectedDueTime() {
|
||||
selectedDueHasTime.value = true
|
||||
nextTick(() => {
|
||||
const picker = selectedDueTimePicker.value as (HTMLInputElement & { showPicker?: () => void }) | null
|
||||
try { picker?.showPicker?.() } catch { picker?.focus() }
|
||||
})
|
||||
}
|
||||
function clearSelectedDueDate() {
|
||||
selectedDueDate.value = ''
|
||||
selectedDueHasTime.value = false
|
||||
selectedTaskRepeat.value = 'none'
|
||||
}
|
||||
function selectTask(task: Task) {
|
||||
selectedTask.value = { ...task, subtasks: task.subtasks ? [...task.subtasks] : [] }
|
||||
const due = parseTaskDueDraft(task.due_at, task.due_has_time)
|
||||
selectedDueDate.value = due.date
|
||||
selectedDueHasTime.value = due.hasTime
|
||||
selectedDueTime.value = due.time
|
||||
markdownPreview.value = false; moreSettingsOpen.value = false; mobileDetail.value = true
|
||||
void loadTaskRecurrence(task)
|
||||
}
|
||||
@@ -1543,8 +1570,11 @@ onUnmounted(() => {
|
||||
<div class="detail-form">
|
||||
<div class="detail-title"><button class="task-check detail-task-check" type="button" :aria-label="selectedTask.completed ? `重新打开${selectedTask.title}` : `完成${selectedTask.title}`" :aria-pressed="selectedTask.completed" @click="toggle(selectedTask)"><span class="task-check-mark" :class="`p${selectedTask.priority}`"><Check v-if="selectedTask.completed" /></span></button><textarea v-model="selectedTask.title" rows="2" aria-label="任务标题"/></div>
|
||||
<label>清单<select v-model="selectedTask.list_id" class="task-detail-field-input"><option v-for="list in lists" :key="list.id" :value="list.id">{{list.name}}</option></select></label>
|
||||
<label class="task-detail-due-row">截止时间<input class="task-detail-due-input task-detail-field-input" :value="toDateTimeLocal(selectedTask.due_at)" type="datetime-local" @change="selectedTask!.due_at=($event.target as HTMLInputElement).value"></label>
|
||||
<label>重复<select v-model="selectedTaskRepeat" class="task-detail-field-input" :disabled="!selectedTask.due_at"><option value="none">不重复</option><option value="daily">每天</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option><option value="after_completion">完成后重复</option><option value="custom">自定义…</option></select><small v-if="!selectedTask.due_at" class="field-hint">请先设置截止时间,才能开启重复</small></label>
|
||||
<div class="task-detail-due-row">
|
||||
<span>截止时间</span>
|
||||
<div class="task-detail-due-controls"><input v-model="selectedDueDate" class="task-detail-due-input task-detail-field-input" type="date" aria-label="截止日期"><button v-if="selectedDueDate" class="task-compose-date-clear" type="button" aria-label="清除截止日期" @click="clearSelectedDueDate"><X/></button><button v-if="selectedDueDate && !selectedDueHasTime" class="task-compose-time-add" type="button" @click="addSelectedDueTime">添加时间</button><label v-else-if="selectedDueDate" class="task-compose-time-chip"><span>时间</span><input ref="selectedDueTimePicker" v-model="selectedDueTime" type="time" aria-label="选择截止时间"><button class="task-compose-time-remove" type="button" aria-label="移除时间" @click.prevent="selectedDueHasTime=false"><X/></button></label></div>
|
||||
</div>
|
||||
<label>重复<select v-model="selectedTaskRepeat" class="task-detail-field-input" :disabled="!selectedDueDate"><option value="none">不重复</option><option value="daily">每天</option><option value="weekly">每周</option><option value="monthly">每月</option><option value="yearly">每年</option><option value="after_completion">完成后重复</option><option value="custom">自定义…</option></select><small v-if="!selectedDueDate" class="field-hint">请先设置截止时间,才能开启重复</small></label>
|
||||
<section v-if="selectedTaskRepeat==='after_completion'" class="after-completion-fields"><div>完成后 <input v-model="selectedAfterCompletionDays" type="number" min="1" max="3650" step="1" inputmode="numeric" aria-label="完成后重复天数"> 天重复</div><small>每次完成后,将截止时间顺延对应天数;首版永不结束</small></section>
|
||||
<section v-if="selectedTaskRepeat==='custom'" class="repeat-custom-fields"><div><span>每隔</span><input v-model.number="selectedRepeatConfig.interval" type="number" min="1"><select v-model="selectedRepeatConfig.frequency"><option value="daily">天</option><option value="weekly">周</option><option value="monthly">月</option><option value="yearly">年</option></select></div><label v-if="selectedRepeatConfig.frequency==='weekly'">重复日期<span class="weekday-picker"><label v-for="day in weekdayOptions" :key="day.value"><input v-model="selectedRepeatConfig.weekdays" type="checkbox" :value="day.value">{{day.label}}</label></span></label><label v-if="selectedRepeatConfig.frequency==='monthly'">每月日期<input v-model="selectedRepeatConfig.monthDays" placeholder="例如 1,15,31" @change="selectedRepeatConfig.monthDays=String(($event.target as HTMLInputElement).value).split(',').map(Number).filter(Boolean)"></label><label>结束方式<select v-model="selectedRepeatConfig.endMode"><option value="never">永不结束</option><option value="date">指定日期</option><option value="count">重复次数</option></select></label><label v-if="selectedRepeatConfig.endMode==='date'">结束日期<input v-model="selectedRepeatConfig.until" type="date"></label><label v-if="selectedRepeatConfig.endMode==='count'">重复次数<input v-model.number="selectedRepeatConfig.count" type="number" min="1"></label></section>
|
||||
<small v-if="selectedRepeatError" role="alert" class="field-error">{{selectedRepeatError}}</small>
|
||||
|
||||
Reference in New Issue
Block a user