新增: 灵感归档箱与任务归档动线

- ideasStore/taskStore: archiveIdea/archiveTask IPC 封装(本地移除/本地更新,对齐 delete 语义)
- project.ts 组合 store 显式透传 archiveIdea/archiveTask
- Ideas.vue: 筛选栏归档箱 Tab(独立数据源 ideaApi.listArchived,主查询不受扰);archiveCurrentIdea 处理器(归档确认+归档箱内取消归档自动清选中);IdeaDetail 传 in-archive-box prop
- IdeaDetail.vue: inArchiveBox prop + archive emit;操作区归档/取消归档按钮,归档箱模式隐藏删除(防误删归档记忆)
- i18n zh-CN/en ideas 补 filterArchived/archiveBoxEmpty/quickArchive/quickUnarchive/confirmArchive/toast 键(前批遗留)
验收: vue-tsc --noEmit EXIT 0
This commit is contained in:
lxy
2026-10-05 00:35:02 +08:00
parent c6ebf15407
commit 83e86cd6b7
13 changed files with 201 additions and 9 deletions
+50 -3
View File
@@ -60,6 +60,8 @@
<div class="filter-group view-toggle">
<button class="seg-btn" :class="{ 'is-active': viewMode === 'list' }" @click="viewMode = 'list'">{{ $t('tasks.kanban.toggleList') }}</button>
<button class="seg-btn" :class="{ 'is-active': viewMode === 'kanban' }" @click="viewMode = 'kanban'">{{ $t('tasks.kanban.toggleKanban') }}</button>
<!-- 归档箱 P1:开关态独立于 list/kanban,开启后整页只渲染归档任务 -->
<button class="seg-btn" :class="{ 'is-active': archiveBoxMode }" @click="toggleArchiveBox">{{ $t('tasks.archiveBox') }}</button>
</div>
<!-- 列表模式全选(看板无批量栏,隐藏) -->
<label v-if="viewMode === 'list'" class="select-all" :title="$t('tasks.batch.selectHint')">
@@ -77,7 +79,8 @@
<div class="task-groups" v-if="viewMode === 'list'">
<div v-if="loading" class="empty-state">{{ $t('common.loading') }}</div>
<div v-else-if="filteredGroups.length === 0" class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-icon">{{ archiveBoxMode ? '📦' : '📋' }}</div>
<p v-if="archiveBoxMode">{{ $t('tasks.archiveBoxEmpty') }}</p>
<div>{{ $t('tasks.group.empty') }}</div>
<button class="btn btn-primary btn-sm" @click="openCreateModal()">{{ $t('tasks.create') }}</button>
</div>
@@ -258,6 +261,7 @@
</button>
</div>
<div class="quick-menu-divider"></div>
<button class="quick-menu-item" @click="quickArchive(row.task, true)">{{ $t('tasks.quickArchive') }}</button>
<button class="quick-menu-item quick-menu-danger" @click="quickDelete(row.task)">{{ $t('tasks.quickDelete') }}</button>
</div>
</div>
@@ -434,6 +438,10 @@ const activeProject = usePersistedRef('tasks.activeProject', 'all')
const activeStatuses = usePersistedRef<string[]>('tasks.activeStatuses', [])
// 列表/看板视图切换(持久化;看板模式隐藏状态多选与排序,列即状态)
const viewMode = usePersistedRef<'list' | 'kanban'>('tasks.viewMode', 'list')
// 归档功能 P1:归档箱开关(持久化)。开启后整页只渲染归档任务(独立数据源,不经 buildTaskQuery)。
const archiveBoxMode = usePersistedRef('tasks.archiveBoxMode', false)
// 归档箱数据源(仅归档箱模式拉取;列表态保持 null 不请求)
const archivedTasks = ref<TaskRecord[]>([])
// 状态多选切换:点击「全部」清空数组(空=未筛选=显示全部);
// 点击具体状态时移除「全部」残留(已有具体筛选则「全部」芯片不应保留激活)。
@@ -547,6 +555,43 @@ async function quickDelete(task: TaskRecord, childCount = 0) {
showToast(e?.toString() ?? t('tasks.err.deleteFailed'), 'error')
}
}
// 归档功能 P1:快捷归档(archive=true 归档,false=归档箱内取消归档)。
// 归档后任务退出主视图(后端活跃查询恒过滤),本地从当前数据源移除;归档箱内取消则从归档箱移除并回主列表。
async function quickArchive(task: TaskRecord, archived: boolean) {
quickMenuId.value = null
if (archived && !await confirmDialog(t('tasks.confirmArchive', { title: task.title }))) return
try {
const ok = await store.archiveTask(task.id, archived)
if (!ok) { showToast(t('tasks.err.archiveFailed'), 'error'); return }
showToast(archived ? t('tasks.archivedToast') : t('tasks.unarchivedToast'))
// 归档箱模式:取消归档后从归档箱移除;列表模式无需动 store(store.archiveTask 已移除)
if (archiveBoxMode.value) archivedTasks.value = archivedTasks.value.filter(x => x.id !== task.id)
} catch (e: any) {
showToast(e?.toString() ?? t('tasks.err.archiveFailed'), 'error')
}
}
// 归档箱开关:开启时拉归档数据源;关闭时按当前筛选重拉主列表。
// 看板模式进入归档箱自动切回列表(归档箱走列表渲染,看板列语义=状态,与归档正交)。
async function toggleArchiveBox() {
archiveBoxMode.value = !archiveBoxMode.value
quickMenuId.value = null
if (archiveBoxMode.value) {
if (viewMode.value === 'kanban') viewMode.value = 'list'
loading.value = true
try {
const pid = activeProject.value === 'all' ? undefined : activeProject.value
archivedTasks.value = await taskApi.listArchived(pid)
} catch (e: any) {
showToast(e?.toString() ?? t('tasks.err.loadFailed'), 'error')
} finally {
loading.value = false
}
} else {
loading.value = true
try { await store.loadTasks(buildTaskQuery()) } finally { loading.value = false }
}
}
// 点击外部关闭快捷菜单
function closeQuickMenu() { quickMenuId.value = null }
function toggleGroup(name: string) {
@@ -631,10 +676,12 @@ interface TaskGroup {
}
// 树组装 — 顶层 = 无 parent_id 的任务,每个顶层挂 children;父进度前端计算(D6,全量已在前端)。
// 归档功能 P1:归档箱模式数据源切 archivedTasks(独立拉取,不经 buildTaskQuery)。
const listSource = computed<TaskRecord[]>(() => (archiveBoxMode.value ? archivedTasks.value : store.tasks))
const taskRows = computed<TaskRow[]>(() => {
// 子任务按 parent_id 分组(1 级嵌套)
const childMap = new Map<string, TaskRecord[]>()
for (const t of store.tasks) {
for (const t of listSource.value) {
if (!t.parent_id) continue
const arr = childMap.get(t.parent_id) ?? []
arr.push(t)
@@ -648,7 +695,7 @@ const taskRows = computed<TaskRow[]>(() => {
const bv = (b as any)[sortBy.value] ?? ''
return String(bv).localeCompare(String(av))
}
const tops = store.tasks.filter(t => !t.parent_id).sort(cmp)
const tops = listSource.value.filter(t => !t.parent_id).sort(cmp)
const rows: TaskRow[] = []
for (const t of tops) {
const children = (childMap.get(t.id) ?? []).sort(cmp)