新增: 灵感归档箱与任务归档动线
- 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:
+48
-4
@@ -54,7 +54,7 @@
|
||||
<div class="idea-list">
|
||||
<!-- 三态:加载/空(错误改走顶部 error-banner,WK-7 不整表替换列表) -->
|
||||
<div v-if="store.loading" class="empty-state">{{ $t('common.loading') }}</div>
|
||||
<div v-else-if="filteredIdeas.length === 0" class="empty-state">{{ $t('ideas.listEmpty') }}</div>
|
||||
<div v-else-if="filteredIdeas.length === 0" class="empty-state">{{ activeFilter === 'archived' ? $t('ideas.archiveBoxEmpty') : $t('ideas.listEmpty') }}</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="idea in pagedIdeas"
|
||||
@@ -93,9 +93,11 @@
|
||||
:eval-error="evalError"
|
||||
:promoting="promoting"
|
||||
:deleting="deleting"
|
||||
:in-archive-box="activeFilter === 'archived'"
|
||||
@evaluate="evaluateCurrentIdea"
|
||||
@promote="promoteToProject"
|
||||
@delete="deleteCurrentIdea"
|
||||
@archive="archiveCurrentIdea"
|
||||
@status-change="onStatusChange"
|
||||
@update-desc="onUpdateDesc"
|
||||
@update-related="onUpdateRelated"
|
||||
@@ -160,7 +162,8 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import type { IdeaStatus, IdeaQuery } from '../api/types'
|
||||
import type { IdeaRecord, IdeaStatus, IdeaQuery } from '../api/types'
|
||||
import { ideaApi } from '../api'
|
||||
import { formatDate } from '../utils/time'
|
||||
import { stripMd } from '../utils/markdown'
|
||||
import { scoreTier } from '../utils/ideaEval'
|
||||
@@ -180,7 +183,7 @@ const store = useProjectStore()
|
||||
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted'
|
||||
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted' | 'archived'
|
||||
|
||||
// ⑤ 列表筛选/排序/分页状态持久化 localStorage(对齐 Tasks.vue:193-201 用 usePersistedRef),
|
||||
// key 前缀 'ideas.',刷新页面/重开恢复用户的筛选视图。selectedId 不持久化(随路由恢复)。
|
||||
@@ -212,6 +215,8 @@ const filters: { key: FilterKey; labelKey: string; icon: string }[] = [
|
||||
{ key: 'hot', labelKey: 'ideas.filter.hot', icon: '🔥' },
|
||||
{ key: 'pending', labelKey: 'ideas.filter.pending', icon: '⏳' },
|
||||
{ key: 'promoted', labelKey: 'ideas.filter.promoted', icon: '🚀' },
|
||||
// 归档功能 P1:归档箱(独立数据源 listArchived,不走主查询)
|
||||
{ key: 'archived', labelKey: 'ideas.filterArchived', icon: '📦' },
|
||||
]
|
||||
|
||||
// ── 状态映射(单一数据源在 @/constants/project,列表/详情/下拉共用) ──
|
||||
@@ -237,9 +242,21 @@ function buildIdeaQuery(): IdeaQuery {
|
||||
return q
|
||||
}
|
||||
|
||||
// 归档功能 P1:归档箱独立数据源(切到归档箱 filter 时拉取;其余 filter 保持 null 不请求)
|
||||
const archivedIdeas = ref<IdeaRecord[]>([])
|
||||
|
||||
// 按当前视图查询加载(登记 setActiveIdeaQuery 供 df-data-changed 联动刷新复用同一筛选,
|
||||
// 避免 promote/relate/监听触发无参 loadIdeas 全量冲掉当前筛选视图)
|
||||
function loadCurrentView() {
|
||||
// 归档箱模式:拉归档数据源,主查询不动。
|
||||
async function loadCurrentView() {
|
||||
if (activeFilter.value === 'archived') {
|
||||
try {
|
||||
archivedIdeas.value = await ideaApi.listArchived()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.toString() ?? t('ideas.err.loadFailed'))
|
||||
}
|
||||
return
|
||||
}
|
||||
store.setActiveIdeaQuery(buildIdeaQuery())
|
||||
return store.loadIdeas(buildIdeaQuery())
|
||||
}
|
||||
@@ -272,6 +289,9 @@ watch(sortMode, () => { page.value = 1; void reloadIdeas(true) })
|
||||
watch(searchQuery, () => { page.value = 1; void reloadIdeas(false) })
|
||||
|
||||
const filteredIdeas = computed(() => {
|
||||
// 归档箱模式:整列表切归档数据源
|
||||
if (activeFilter.value === 'archived') return archivedIdeas.value
|
||||
|
||||
let ideas = store.ideas
|
||||
|
||||
// hot(按 score≥80)/pending(多状态 draft+pending_review)保留前端 filter:
|
||||
@@ -380,6 +400,30 @@ async function deleteCurrentIdea() {
|
||||
}
|
||||
}
|
||||
|
||||
// 归档功能 P1:归档/取消归档当前灵感(归档箱内取消归档后自动切回 all 重载)。
|
||||
async function archiveCurrentIdea(archived: boolean) {
|
||||
if (!currentIdea.value || deleting.value) return
|
||||
if (archived && !await confirmDialog(t('ideas.confirmArchive', { title: currentIdea.value.title }))) return
|
||||
deleting.value = true
|
||||
try {
|
||||
const ok = await store.archiveIdea(currentIdea.value.id, archived)
|
||||
if (!ok) { Message.error(t('ideas.err.archiveFailed')); return }
|
||||
Message.success(archived ? t('ideas.archivedToast') : t('ideas.unarchivedToast'))
|
||||
if (activeFilter.value === 'archived') {
|
||||
// 归档箱内:本地移除 + 选中态清理
|
||||
archivedIdeas.value = archivedIdeas.value.filter(i => i.id !== currentIdea.value?.id)
|
||||
if (selectedId.value === currentIdea.value?.id) selectedId.value = null
|
||||
} else {
|
||||
// 主列表归档成功:store 已本地移除,清选中(归档项不再可见)
|
||||
selectedId.value = null
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e?.toString() ?? t('ideas.err.archiveFailed'))
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function promoteToProject() {
|
||||
if (!currentIdea.value || promoting.value) return
|
||||
promoting.value = true
|
||||
|
||||
+50
-3
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user