feat: 回收站、批量编辑、合并导入与标签治理

- 数据:Task 增加 deletedAt,schema v2→v3;删除改为软删除,保留 30 天后自动清理
- 回收站:支持恢复、永久删除、二次确认清空;导航栏入口带数量角标
- 批量编辑:多选后统一移动列、改优先级、标签增删、设置截止日、归档
- 合并导入:按 id 匹配、updatedAt 新者胜;覆盖/合并二选一,执行前自动快照
- 标签治理:全局重命名、合并、删除;回收站任务不参与改写
- 看板语义读取统一改用 activeTasks,避免已删除任务泄漏到统计、今日与标签下拉
- UI:回收站图标与备份统一尺寸;批量编辑条统一控件高度并分组
- 测试:705 个单测通过,补充迁移、回收站、批量编辑、合并与标签用例
This commit is contained in:
chenshoulin
2026-10-19 18:32:42 +08:00
parent 43e813b93a
commit 65e98199ba
39 changed files with 2734 additions and 143 deletions
+11 -5
View File
@@ -12,7 +12,12 @@ import TagFilter from './TagFilter.vue'
defineProps<{ hiddenNote?: string; selecting?: boolean }>()
const emit = defineEmits<{ archive: []; 'batch-add': []; 'toggle-select': [] }>()
const emit = defineEmits<{
archive: []
'batch-add': []
'toggle-select': []
'manage-tags': []
}>()
const store = useTaskStore()
const filters = useFilterStore()
@@ -24,15 +29,15 @@ const searchRef = ref<HTMLInputElement | null>(null)
const PRIORITY_ORDER: readonly Priority[] = [3, 2, 1]
const dueTodayCount = computed(
() => store.tasks.filter((task) => isDueToday(task.dueDate, task.status)).length,
() => store.activeTasks.filter((task) => isDueToday(task.dueDate, task.status)).length,
)
const overdueCount = computed(
() => store.tasks.filter((task) => isOverdue(task.dueDate, task.status)).length,
() => store.activeTasks.filter((task) => isOverdue(task.dueDate, task.status)).length,
)
/** 当前可归档(已完成且超过保留天数)的任务数量 */
const archivableCount = computed(() => findArchivableTasks(store.tasks).length)
const archivableCount = computed(() => findArchivableTasks(store.activeTasks).length)
function focusSearch(): void {
searchRef.value?.focus()
@@ -91,6 +96,7 @@ defineExpose({ focusSearch })
:selected="tags"
@toggle="filters.toggleTag($event)"
@clear="filters.clearTags()"
@manage="emit('manage-tags')"
/>
<div class="ml-auto flex items-center gap-1.5">
@@ -129,7 +135,7 @@ defineExpose({ focusSearch })
</div>
<div class="flex flex-wrap items-center gap-x-2 gap-y-1.5 text-xs text-slate-500 dark:text-zinc-400">
<span>共 {{ store.tasks.length }} 个任务</span>
<span>共 {{ store.activeTasks.length }} 个任务</span>
<span v-if="hiddenNote" class="text-slate-400 dark:text-zinc-500">{{ hiddenNote }}</span>
<div class="ml-auto flex items-center gap-1.5">