feat: redesign task detail paper flow
This commit is contained in:
+45
-31
@@ -161,6 +161,8 @@ const selectedRepeatError = ref('')
|
||||
const selectedTaskRecurrence = ref<Recurrence | null>(null)
|
||||
const recurrenceLoading = ref(false)
|
||||
const savingSelectedTask = ref(false)
|
||||
const removingSubtaskId = ref<string | null>(null)
|
||||
const taskDetailBusy = computed(() => savingSelectedTask.value || recurrenceLoading.value || removingSubtaskId.value !== null)
|
||||
const defaultRepeatConfig = (): TaskRepeatConfig => ({ frequency: 'daily', interval: 1, weekdays: [], monthDays: [], endMode: 'never', count: 10, until: '' })
|
||||
const composeRepeatConfig = ref<TaskRepeatConfig>(defaultRepeatConfig())
|
||||
const selectedRepeatConfig = ref<TaskRepeatConfig>(defaultRepeatConfig())
|
||||
@@ -943,7 +945,8 @@ async function saveTask(options?: { showSuccess?: boolean, expectedTaskId?: stri
|
||||
if (options?.showSuccess !== false) toast('已保存')
|
||||
return updated
|
||||
} catch (reason) {
|
||||
fail(reason)
|
||||
const selectionMatches = options?.expectedSelectionToken === undefined || recurrenceLoadToken === options.expectedSelectionToken
|
||||
if (selectionMatches && selectedTask.value?.id === task.id) fail(reason)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -979,7 +982,9 @@ async function saveSelectedTaskChanges() {
|
||||
}
|
||||
}
|
||||
async function removeTask(task: Task) {
|
||||
if (taskDetailBusy.value) return
|
||||
if (!(await confirmAction(`把“${task.title}”移到回收站?`, undefined, true))) return
|
||||
if (taskDetailBusy.value || selectedTask.value?.id !== task.id) return
|
||||
try {
|
||||
await api(`/tasks/${task.id}`, { method: 'DELETE' })
|
||||
tasks.value = tasks.value.filter((item) => item.id !== task.id && item.parent_id !== task.id)
|
||||
@@ -1019,7 +1024,25 @@ async function addSubtask() {
|
||||
if (!subtaskTitle) return
|
||||
try { const child = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: subtaskTitle, list_id: selectedTask.value.list_id, parent_id: selectedTask.value.id }) }); if (selectedTask.value) selectedTask.value.subtasks = [...(selectedTask.value.subtasks ?? []), child]; tasks.value.push(child); toast('子任务已添加') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function removeSubtask(subtask: Task) {
|
||||
if (taskDetailBusy.value) return
|
||||
const parent = selectedTask.value
|
||||
if (!parent || subtask.parent_id !== parent.id) return
|
||||
if (!(await confirmAction(`删除子任务“${subtask.title}”?`, '子任务将移到回收站。', true))) return
|
||||
if (taskDetailBusy.value || selectedTask.value?.id !== parent.id) return
|
||||
removingSubtaskId.value = subtask.id
|
||||
try {
|
||||
await api(`/tasks/${subtask.id}`, { method: 'DELETE' })
|
||||
if (selectedTask.value?.id !== parent.id) return
|
||||
parent.subtasks = (parent.subtasks ?? []).filter((item) => item.id !== subtask.id)
|
||||
tasks.value = tasks.value.filter((item) => item.id !== subtask.id)
|
||||
toast('子任务已删除')
|
||||
} catch (reason) { fail(reason) }
|
||||
finally { if (removingSubtaskId.value === subtask.id) removingSubtaskId.value = null }
|
||||
}
|
||||
function closeTaskDetail() {
|
||||
if (taskDetailBusy.value) return
|
||||
recurrenceLoadToken += 1
|
||||
mobileDetail.value = false
|
||||
selectedTask.value = null
|
||||
}
|
||||
@@ -1598,44 +1621,35 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<AppSheet v-if="selectedTask" :open="true" :modal="compactLayout" variant="detail" panel-class="detail" title-id="task-detail-title" :close-on-scrim="compactLayout" @close="closeTaskDetail">
|
||||
<div class="detail-head"><span id="task-detail-title">任务详情</span><button class="icon" aria-label="关闭详情" @click="closeTaskDetail"><X/></button></div>
|
||||
<div class="detail-form">
|
||||
<AppSheet v-if="selectedTask" :open="true" :modal="compactLayout" variant="detail" panel-class="detail" title-id="task-detail-title" :close-on-scrim="compactLayout" :busy="taskDetailBusy" @close="closeTaskDetail" @submit.prevent="saveSelectedTaskChanges">
|
||||
<div class="detail-head"><span id="task-detail-title">任务详情</span><button class="icon" type="button" :disabled="taskDetailBusy" aria-label="关闭详情" @click="closeTaskDetail"><X/></button></div>
|
||||
<fieldset class="detail-form" :disabled="taskDetailBusy">
|
||||
<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>
|
||||
<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>
|
||||
<div class="field markdown">
|
||||
<section class="task-detail-arrangement" aria-label="安排">
|
||||
<label class="task-detail-field"><span class="task-detail-field-label">清单</span><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>
|
||||
<div class="task-detail-date-time">
|
||||
<label class="task-detail-field"><span class="task-detail-field-label">截止日期</span><span class="task-detail-date-control"><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.prevent="clearSelectedDueDate"><X/></button></span></label>
|
||||
<div class="task-detail-field"><span class="task-detail-field-label">时间</span><button v-if="selectedDueDate && !selectedDueHasTime" class="task-compose-time-add task-detail-time-control" type="button" @click="addSelectedDueTime">添加时间</button><label v-else-if="selectedDueDate" class="task-compose-time-chip task-detail-time-control"><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><span v-else class="task-detail-time-empty" aria-hidden="true">—</span></div>
|
||||
</div>
|
||||
<label class="task-detail-field"><span class="task-detail-field-label">重复</span><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>
|
||||
</section>
|
||||
<section class="field markdown task-detail-notes">
|
||||
<div class="field-label"><span>任务备注</span><span class="markdown-mode-switch"><button type="button" :aria-pressed="!markdownPreview" :class="{active:!markdownPreview}" @click="markdownPreview=false">编辑</button><button type="button" :aria-pressed="markdownPreview" :class="{active:markdownPreview}" @click="markdownPreview=true">预览</button></span></div>
|
||||
<div v-if="!markdownPreview" class="markdown-editor-shell">
|
||||
<div class="markdown-toolbar" role="toolbar" aria-label="Markdown 格式">
|
||||
<button type="button" aria-label="标题" title="标题" @click="formatTaskNote('heading')"><Heading2/></button>
|
||||
<button type="button" aria-label="粗体" title="粗体" @click="formatTaskNote('bold')"><Bold/></button>
|
||||
<button type="button" aria-label="斜体" title="斜体" @click="formatTaskNote('italic')"><Italic/></button>
|
||||
<button type="button" aria-label="无序列表" title="无序列表" @click="formatTaskNote('bullet')"><List/></button>
|
||||
<button type="button" aria-label="有序列表" title="有序列表" @click="formatTaskNote('ordered')"><ListOrdered/></button>
|
||||
<button type="button" aria-label="待办" title="待办" @click="formatTaskNote('task')"><ListChecks/></button>
|
||||
<button type="button" aria-label="链接" title="链接" @click="formatTaskNote('link')"><Link/></button>
|
||||
<button type="button" aria-label="行内代码" title="行内代码" @click="formatTaskNote('code')"><Code/></button>
|
||||
<button type="button" aria-label="代码块" title="代码块" class="markdown-codeblock" @click="formatTaskNote('codeblock')">{ }</button>
|
||||
<button type="button" aria-label="引用" title="引用" @click="formatTaskNote('quote')"><Quote/></button>
|
||||
<button type="button" aria-label="标题" title="标题" @click="formatTaskNote('heading')"><Heading2/></button><button type="button" aria-label="粗体" title="粗体" @click="formatTaskNote('bold')"><Bold/></button><button type="button" aria-label="斜体" title="斜体" @click="formatTaskNote('italic')"><Italic/></button><button type="button" aria-label="无序列表" title="无序列表" @click="formatTaskNote('bullet')"><List/></button><button type="button" aria-label="有序列表" title="有序列表" @click="formatTaskNote('ordered')"><ListOrdered/></button><button type="button" aria-label="待办" title="待办" @click="formatTaskNote('task')"><ListChecks/></button><button type="button" aria-label="链接" title="链接" @click="formatTaskNote('link')"><Link/></button><button type="button" aria-label="行内代码" title="行内代码" @click="formatTaskNote('code')"><Code/></button><button type="button" aria-label="代码块" title="代码块" class="markdown-codeblock" @click="formatTaskNote('codeblock')">{ }</button><button type="button" aria-label="引用" title="引用" @click="formatTaskNote('quote')"><Quote/></button>
|
||||
</div>
|
||||
<textarea ref="taskNoteEditor" v-model="selectedTask.description" rows="9" placeholder="写备注,选中文字后可用上方工具栏添加格式…" @keydown="handleTaskNoteShortcut"/>
|
||||
</div>
|
||||
<div v-else class="markdown-preview" :class="{'markdown-preview-empty':!selectedTask.description.trim()}" v-html="selectedTask.description.trim() ? renderMarkdown(selectedTask.description) : '<p>暂无备注,切回编辑开始书写。</p>'"/>
|
||||
</div>
|
||||
<div class="subtasks"><div class="field-label"><span>子任务</span><button class="link" @click="addSubtask"><Plus/>添加</button></div><div v-for="subtask in selectedTaskSubtasks" :key="subtask.id" class="subtask-detail"><button class="task-check subtask-check" type="button" :aria-label="subtask.completed ? `重新打开${subtask.title}` : `完成${subtask.title}`" :aria-pressed="subtask.completed" @click="toggle(subtask)"><span class="task-check-mark" :class="`p${subtask.priority}`"><Check v-if="subtask.completed" /></span></button><span :class="{strike:subtask.completed}">{{subtask.title}}</span></div><span v-if="!selectedTaskSubtasks.length" class="hint">把这件事拆成更小的步骤</span></div>
|
||||
<details class="more-settings" :open="moreSettingsOpen" @toggle="moreSettingsOpen=($event.target as HTMLDetailsElement).open"><summary>更多设置</summary><div class="more-settings-body">
|
||||
<label>优先级<select v-model.number="selectedTask.priority"><option :value="0">无</option><option :value="1">低</option><option :value="2">中</option><option :value="3">高</option></select></label>
|
||||
</div></details>
|
||||
<div class="detail-actions"><button class="secondary" :disabled="savingSelectedTask || recurrenceLoading" @click="saveSelectedTaskChanges">{{savingSelectedTask?'正在保存…':recurrenceLoading?'正在读取…':'保存更改'}}</button><button class="danger-text" @click="removeTask(selectedTask)"><Trash2/>移到回收站</button></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="subtasks task-detail-subtasks"><div class="field-label"><span>子任务</span><button class="link" type="button" :disabled="taskDetailBusy" @click="addSubtask"><Plus/>添加</button></div><div v-for="subtask in selectedTaskSubtasks" :key="subtask.id" class="subtask-detail"><button class="task-check subtask-check" type="button" :disabled="taskDetailBusy" :aria-label="subtask.completed ? `重新打开${subtask.title}` : `完成${subtask.title}`" :aria-pressed="subtask.completed" @click="toggle(subtask)"><span class="task-check-mark" :class="`p${subtask.priority}`"><Check v-if="subtask.completed" /></span></button><span :class="{strike:subtask.completed}">{{subtask.title}}</span><button class="subtask-remove" type="button" :disabled="taskDetailBusy" :aria-label="`删除子任务${subtask.title}`" @click="removeSubtask(subtask)"><Trash2/></button></div><span v-if="!selectedTaskSubtasks.length" class="hint">把这件事拆成更小的步骤</span></section>
|
||||
<details class="more-settings" :open="moreSettingsOpen" @toggle="moreSettingsOpen=($event.target as HTMLDetailsElement).open"><summary>更多设置</summary><div class="more-settings-body"><label class="task-detail-field"><span class="task-detail-field-label">优先级</span><select v-model.number="selectedTask.priority"><option :value="0">无</option><option :value="1">低</option><option :value="2">中</option><option :value="3">高</option></select></label></div></details>
|
||||
</fieldset>
|
||||
<footer class="detail-actions"><button class="danger-text detail-trash" type="button" :disabled="taskDetailBusy" @click="removeTask(selectedTask)"><Trash2/>移到回收站</button><button class="primary detail-save" type="submit" :disabled="taskDetailBusy">{{savingSelectedTask?'正在保存…':recurrenceLoading?'正在读取…':'保存更改'}}</button></footer>
|
||||
</AppSheet>
|
||||
|
||||
<nav class="bottom" :inert="memoBackgroundInert ? true : undefined" aria-label="主要导航"><button :class="{active:activeView==='today'}" :aria-current="activeView==='today' ? 'page' : undefined" @click="switchView('today')"><ListTodo/><span>今天</span></button><button :class="{active:activeView==='habits'}" :aria-current="activeView==='habits' ? 'page' : undefined" @click="switchView('habits')"><Repeat2/><span>习惯</span></button><button :class="{active:activeView==='countdowns'}" :aria-current="activeView==='countdowns' ? 'page' : undefined" @click="switchView('countdowns')"><CalendarHeart/><span>倒数日</span></button><button :class="{active:activeView==='settings'}" :aria-current="activeView==='settings' ? 'page' : undefined" @click="switchView('settings')"><Settings/><span>设置</span></button></nav>
|
||||
|
||||
Reference in New Issue
Block a user