新增: 灵感归档箱与任务归档动线
- 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:
@@ -49,4 +49,14 @@ export const ideaApi = {
|
|||||||
listEvaluations(ideaId: string): Promise<IdeaEvaluationRecord[]> {
|
listEvaluations(ideaId: string): Promise<IdeaEvaluationRecord[]> {
|
||||||
return invoke('list_idea_evaluations', { ideaId })
|
return invoke('list_idea_evaluations', { ideaId })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 归档/取消归档灵感(归档功能 P1,V47)。返回是否命中(灵感不存在/已软删为 false)。 */
|
||||||
|
archive(id: string, archived: boolean): Promise<boolean> {
|
||||||
|
return invoke('archive_idea', { id, archived })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 列出归档灵感(archived = 1 且未删,updated_at DESC)。 */
|
||||||
|
listArchived(): Promise<IdeaRecord[]> {
|
||||||
|
return invoke('list_archived_ideas')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,14 @@ export const taskApi = {
|
|||||||
advance(id: string, targetStatus: string): Promise<TaskRecord> {
|
advance(id: string, targetStatus: string): Promise<TaskRecord> {
|
||||||
return invoke('advance_task', { id, targetStatus })
|
return invoke('advance_task', { id, targetStatus })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 归档/取消归档任务(归档功能 P1,V46)。返回是否命中(任务不存在/已软删为 false)。 */
|
||||||
|
archive(id: string, archived: boolean): Promise<boolean> {
|
||||||
|
return invoke('archive_task', { id, archived })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 列出归档任务(archived = 1 且未删,updated_at DESC)。projectId 可选过滤。 */
|
||||||
|
listArchived(projectId?: string): Promise<TaskRecord[]> {
|
||||||
|
return invoke('list_archived_tasks', { projectId: projectId ?? null })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export interface IdeaRecord {
|
|||||||
scores: string | null
|
scores: string | null
|
||||||
/** 关联灵感 id JSON 数组字符串(对齐后端 related_ids Option<String>),null/空串=无关联 */
|
/** 关联灵感 id JSON 数组字符串(对齐后端 related_ids Option<String>),null/空串=无关联 */
|
||||||
related_ids: string | null
|
related_ids: string | null
|
||||||
|
/** 归档标记(归档功能 P1,V47 列)。归档灵感退出主视图,仅在归档箱可见。 */
|
||||||
|
archived: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -189,6 +191,12 @@ export interface TaskRecord {
|
|||||||
* 老任务无结构化规格时为 undefined。
|
* 老任务无结构化规格时为 undefined。
|
||||||
*/
|
*/
|
||||||
content_json?: string
|
content_json?: string
|
||||||
|
/**
|
||||||
|
* 归档标记(归档功能 P1,V46 列)。与 status/queue 正交的纯元数据:
|
||||||
|
* archived=true 的任务退出主视图(后端活跃查询恒过滤),仅在归档视图可见。
|
||||||
|
* 老任务后端 DEFAULT 0 保证该字段始终存在。
|
||||||
|
*/
|
||||||
|
archived: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,10 +286,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作(P0-② 立项按钮已上移至标题行,此处仅留删除) -->
|
<!-- 操作(P0-② 立项按钮已上移至标题行) -->
|
||||||
<div class="detail-section" style="margin-top:8px">
|
<div class="detail-section" style="margin-top:8px">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn btn-ghost" :disabled="deleting" @click="$emit('delete')">
|
<!-- 归档功能 P1:主列表=归档动线;归档箱=取消归档动线(删除隐藏,防误删归档记忆) -->
|
||||||
|
<button class="btn btn-ghost" :disabled="deleting" @click="$emit('archive', !inArchiveBox)">
|
||||||
|
{{ inArchiveBox ? $t('ideas.quickUnarchive') : $t('ideas.quickArchive') }}
|
||||||
|
</button>
|
||||||
|
<button v-if="!inArchiveBox" class="btn btn-ghost" :disabled="deleting" @click="$emit('delete')">
|
||||||
{{ deleting ? $t('ideas.deleting') : $t('ideas.deleteIdea') }}
|
{{ deleting ? $t('ideas.deleting') : $t('ideas.deleteIdea') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,12 +319,15 @@ const props = defineProps<{
|
|||||||
evalError: string
|
evalError: string
|
||||||
promoting: boolean
|
promoting: boolean
|
||||||
deleting: boolean
|
deleting: boolean
|
||||||
|
/** 归档功能 P1:父视图是否处于归档箱模式(决定操作按钮语义:归档↔取消归档) */
|
||||||
|
inArchiveBox?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'evaluate'): void
|
(e: 'evaluate'): void
|
||||||
(e: 'promote'): void
|
(e: 'promote'): void
|
||||||
(e: 'delete'): void
|
(e: 'delete'): void
|
||||||
|
(e: 'archive', archived: boolean): void
|
||||||
(e: 'status-change', value: IdeaStatus): void
|
(e: 'status-change', value: IdeaStatus): void
|
||||||
// 返 Promise<boolean> 让子组件 await 知成功/失败。
|
// 返 Promise<boolean> 让子组件 await 知成功/失败。
|
||||||
// true=落库成功(关编辑态);false=失败(保持编辑态,toast 由父组件处理)。
|
// true=落库成功(关编辑态);false=失败(保持编辑态,toast 由父组件处理)。
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
createFailed: 'Failed to create idea',
|
createFailed: 'Failed to create idea',
|
||||||
deleteFailed: 'Failed to delete idea',
|
deleteFailed: 'Failed to delete idea',
|
||||||
relateFailed: 'Failed to update relations',
|
relateFailed: 'Failed to update relations',
|
||||||
|
archiveFailed: 'Archive operation failed',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Action feedback toasts (WK-16/18: success/failure light toast, aligns Knowledge.createdToast)
|
// Action feedback toasts (WK-16/18: success/failure light toast, aligns Knowledge.createdToast)
|
||||||
@@ -147,6 +148,14 @@ export default {
|
|||||||
// ⑥ Status change confirm (terminal promoted/rejected/archived or leaving approved)
|
// ⑥ Status change confirm (terminal promoted/rejected/archived or leaving approved)
|
||||||
confirmStatus: 'Change idea "{title}" status from {from} to {to}?',
|
confirmStatus: 'Change idea "{title}" status from {from} to {to}?',
|
||||||
promoteFailed: 'Promotion failed',
|
promoteFailed: 'Promotion failed',
|
||||||
|
// Archive P1: idea archive box
|
||||||
|
filterArchived: '📦 Archive',
|
||||||
|
archiveBoxEmpty: 'No archived ideas',
|
||||||
|
quickArchive: '📦 Archive',
|
||||||
|
quickUnarchive: '📤 Unarchive',
|
||||||
|
confirmArchive: 'Archive idea "{title}"? It will leave the main view; find it in the Archive box.',
|
||||||
|
archivedToast: 'Archived',
|
||||||
|
unarchivedToast: 'Unarchived',
|
||||||
evalFailed: 'Evaluation failed',
|
evalFailed: 'Evaluation failed',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ export default {
|
|||||||
quickStatus: 'Status',
|
quickStatus: 'Status',
|
||||||
quickPriority: 'Priority',
|
quickPriority: 'Priority',
|
||||||
quickDelete: '🗑 Delete',
|
quickDelete: '🗑 Delete',
|
||||||
|
// Archive P1: archive moves out of main view, keep searchable; delete moves to trash
|
||||||
|
quickArchive: '📦 Archive',
|
||||||
|
quickUnarchive: '📤 Unarchive',
|
||||||
|
confirmArchive: 'Archive task "{title}"? It will leave the main view; find it in the Archive box.',
|
||||||
|
archivedToast: 'Archived',
|
||||||
|
unarchivedToast: 'Unarchived',
|
||||||
|
archiveBox: '📦 Archive',
|
||||||
|
archiveBoxEmpty: 'No archived tasks',
|
||||||
|
|
||||||
// Batch action bar (shared by Tasks / ProjectDetail / TaskDetail subtasks)
|
// Batch action bar (shared by Tasks / ProjectDetail / TaskDetail subtasks)
|
||||||
batch: {
|
batch: {
|
||||||
@@ -129,6 +137,7 @@ export default {
|
|||||||
createFailed: 'Failed to create task',
|
createFailed: 'Failed to create task',
|
||||||
deleteFailed: 'Failed to delete task',
|
deleteFailed: 'Failed to delete task',
|
||||||
updateFailed: 'Failed to update task',
|
updateFailed: 'Failed to update task',
|
||||||
|
archiveFailed: 'Archive operation failed',
|
||||||
selectProject: 'Please select a project',
|
selectProject: 'Please select a project',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
createFailed: '创建灵感失败',
|
createFailed: '创建灵感失败',
|
||||||
deleteFailed: '删除灵感失败',
|
deleteFailed: '删除灵感失败',
|
||||||
relateFailed: '更新关联关系失败',
|
relateFailed: '更新关联关系失败',
|
||||||
|
archiveFailed: '归档操作失败',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 操作反馈 toast(WK-16/18:成功/失败轻提示,对齐 Knowledge.createdToast)
|
// 操作反馈 toast(WK-16/18:成功/失败轻提示,对齐 Knowledge.createdToast)
|
||||||
@@ -147,6 +148,14 @@ export default {
|
|||||||
// ⑥ 状态切换确认(终态 promoted/rejected/archived 或离开 approved 失去立项入口)
|
// ⑥ 状态切换确认(终态 promoted/rejected/archived 或离开 approved 失去立项入口)
|
||||||
confirmStatus: '将灵感「{title}」状态从 {from} 改为 {to}?',
|
confirmStatus: '将灵感「{title}」状态从 {from} 改为 {to}?',
|
||||||
promoteFailed: '立项失败',
|
promoteFailed: '立项失败',
|
||||||
|
// 归档功能 P1(灵感归档箱)
|
||||||
|
filterArchived: '📦 归档箱',
|
||||||
|
archiveBoxEmpty: '暂无归档灵感',
|
||||||
|
quickArchive: '📦 归档到归档箱',
|
||||||
|
quickUnarchive: '📤 取消归档',
|
||||||
|
confirmArchive: '归档灵感「{title}」?归档后移出主视图,可在归档箱查看。',
|
||||||
|
archivedToast: '已归档',
|
||||||
|
unarchivedToast: '已取消归档',
|
||||||
evalFailed: '评估失败',
|
evalFailed: '评估失败',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ export default {
|
|||||||
quickStatus: '状态',
|
quickStatus: '状态',
|
||||||
quickPriority: '优先级',
|
quickPriority: '优先级',
|
||||||
quickDelete: '🗑 删除',
|
quickDelete: '🗑 删除',
|
||||||
|
// 归档功能 P1(归档=移出主视图保留检索,区别于删除=进回收站)
|
||||||
|
quickArchive: '📦 归档',
|
||||||
|
quickUnarchive: '📤 取消归档',
|
||||||
|
confirmArchive: '归档任务「{title}」?归档后移出主视图,可在归档箱查看。',
|
||||||
|
archivedToast: '已归档',
|
||||||
|
unarchivedToast: '已取消归档',
|
||||||
|
archiveBox: '📦 归档箱',
|
||||||
|
archiveBoxEmpty: '暂无归档任务',
|
||||||
|
|
||||||
// 批量操作栏(三处任务列表共用:Tasks / ProjectDetail / TaskDetail 子任务)
|
// 批量操作栏(三处任务列表共用:Tasks / ProjectDetail / TaskDetail 子任务)
|
||||||
batch: {
|
batch: {
|
||||||
@@ -129,6 +137,7 @@ export default {
|
|||||||
createFailed: '创建任务失败',
|
createFailed: '创建任务失败',
|
||||||
deleteFailed: '删除任务失败',
|
deleteFailed: '删除任务失败',
|
||||||
updateFailed: '更新任务失败',
|
updateFailed: '更新任务失败',
|
||||||
|
archiveFailed: '归档操作失败',
|
||||||
selectProject: '请选择项目',
|
selectProject: '请选择项目',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ function createStore() {
|
|||||||
createTask: tasksStore.createTask,
|
createTask: tasksStore.createTask,
|
||||||
updateTask: tasksStore.updateTask,
|
updateTask: tasksStore.updateTask,
|
||||||
deleteTask: tasksStore.deleteTask,
|
deleteTask: tasksStore.deleteTask,
|
||||||
|
archiveTask: tasksStore.archiveTask,
|
||||||
// P1-b A0:统计独立通道(Dashboard 卡片 + ActiveProjectsPanel 各项目任务数)
|
// P1-b A0:统计独立通道(Dashboard 卡片 + ActiveProjectsPanel 各项目任务数)
|
||||||
loadStats,
|
loadStats,
|
||||||
// idea actions
|
// idea actions
|
||||||
@@ -186,6 +187,7 @@ function createStore() {
|
|||||||
createIdea: ideasStore.createIdea,
|
createIdea: ideasStore.createIdea,
|
||||||
updateIdea: ideasStore.updateIdea,
|
updateIdea: ideasStore.updateIdea,
|
||||||
deleteIdea: ideasStore.deleteIdea,
|
deleteIdea: ideasStore.deleteIdea,
|
||||||
|
archiveIdea: ideasStore.archiveIdea,
|
||||||
evaluateIdea: ideasStore.evaluateIdea,
|
evaluateIdea: ideasStore.evaluateIdea,
|
||||||
promoteIdea: ideasStore.promoteIdea,
|
promoteIdea: ideasStore.promoteIdea,
|
||||||
relateIdeas: ideasStore.relateIdeas,
|
relateIdeas: ideasStore.relateIdeas,
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ export function createIdeasStore() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归档/取消归档灵感(归档功能 P1)。归档=移出主视图(list_by_query 恒带 archived=0),
|
||||||
|
* 本地同步从列表移除(取消归档时调 loadIdeas 重拉)。返回后端命中布尔。
|
||||||
|
*/
|
||||||
|
async function archiveIdea(id: string, archived: boolean): Promise<boolean> {
|
||||||
|
const ok = await runWithCatch(state, t('ideas.err.archiveFailed'), async () => {
|
||||||
|
const hit = await ideaApi.archive(id, archived)
|
||||||
|
if (hit && archived) state.ideas = state.ideas.filter(x => x.id !== id)
|
||||||
|
return hit
|
||||||
|
})
|
||||||
|
return ok === true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadIdeas,
|
loadIdeas,
|
||||||
createIdea,
|
createIdea,
|
||||||
@@ -78,5 +91,6 @@ export function createIdeasStore() {
|
|||||||
evaluateIdea,
|
evaluateIdea,
|
||||||
promoteIdea,
|
promoteIdea,
|
||||||
relateIdeas,
|
relateIdeas,
|
||||||
|
archiveIdea,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,24 @@ export function createTasksStore() {
|
|||||||
return ok === true
|
return ok === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归档/取消归档任务(归档功能 P1)。归档=移出主视图(后端活跃查询恒过滤),
|
||||||
|
* 本地同步从列表移除(取消归档时调 loadTasks 重拉)。返回后端命中布尔。
|
||||||
|
*/
|
||||||
|
async function archiveTask(id: string, archived: boolean): Promise<boolean> {
|
||||||
|
const ok = await runWithCatch(state, t('tasks.err.archiveFailed'), async () => {
|
||||||
|
const hit = await taskApi.archive(id, archived)
|
||||||
|
if (hit && archived) state.tasks = state.tasks.filter(x => x.id !== id)
|
||||||
|
return hit
|
||||||
|
})
|
||||||
|
return ok === true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadTasks,
|
loadTasks,
|
||||||
createTask,
|
createTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
|
archiveTask,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-4
@@ -54,7 +54,7 @@
|
|||||||
<div class="idea-list">
|
<div class="idea-list">
|
||||||
<!-- 三态:加载/空(错误改走顶部 error-banner,WK-7 不整表替换列表) -->
|
<!-- 三态:加载/空(错误改走顶部 error-banner,WK-7 不整表替换列表) -->
|
||||||
<div v-if="store.loading" class="empty-state">{{ $t('common.loading') }}</div>
|
<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>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="idea in pagedIdeas"
|
v-for="idea in pagedIdeas"
|
||||||
@@ -93,9 +93,11 @@
|
|||||||
:eval-error="evalError"
|
:eval-error="evalError"
|
||||||
:promoting="promoting"
|
:promoting="promoting"
|
||||||
:deleting="deleting"
|
:deleting="deleting"
|
||||||
|
:in-archive-box="activeFilter === 'archived'"
|
||||||
@evaluate="evaluateCurrentIdea"
|
@evaluate="evaluateCurrentIdea"
|
||||||
@promote="promoteToProject"
|
@promote="promoteToProject"
|
||||||
@delete="deleteCurrentIdea"
|
@delete="deleteCurrentIdea"
|
||||||
|
@archive="archiveCurrentIdea"
|
||||||
@status-change="onStatusChange"
|
@status-change="onStatusChange"
|
||||||
@update-desc="onUpdateDesc"
|
@update-desc="onUpdateDesc"
|
||||||
@update-related="onUpdateRelated"
|
@update-related="onUpdateRelated"
|
||||||
@@ -160,7 +162,8 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { useProjectStore } from '../stores/project'
|
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 { formatDate } from '../utils/time'
|
||||||
import { stripMd } from '../utils/markdown'
|
import { stripMd } from '../utils/markdown'
|
||||||
import { scoreTier } from '../utils/ideaEval'
|
import { scoreTier } from '../utils/ideaEval'
|
||||||
@@ -180,7 +183,7 @@ const store = useProjectStore()
|
|||||||
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
||||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||||
|
|
||||||
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted'
|
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted' | 'archived'
|
||||||
|
|
||||||
// ⑤ 列表筛选/排序/分页状态持久化 localStorage(对齐 Tasks.vue:193-201 用 usePersistedRef),
|
// ⑤ 列表筛选/排序/分页状态持久化 localStorage(对齐 Tasks.vue:193-201 用 usePersistedRef),
|
||||||
// key 前缀 'ideas.',刷新页面/重开恢复用户的筛选视图。selectedId 不持久化(随路由恢复)。
|
// key 前缀 'ideas.',刷新页面/重开恢复用户的筛选视图。selectedId 不持久化(随路由恢复)。
|
||||||
@@ -212,6 +215,8 @@ const filters: { key: FilterKey; labelKey: string; icon: string }[] = [
|
|||||||
{ key: 'hot', labelKey: 'ideas.filter.hot', icon: '🔥' },
|
{ key: 'hot', labelKey: 'ideas.filter.hot', icon: '🔥' },
|
||||||
{ key: 'pending', labelKey: 'ideas.filter.pending', icon: '⏳' },
|
{ key: 'pending', labelKey: 'ideas.filter.pending', icon: '⏳' },
|
||||||
{ key: 'promoted', labelKey: 'ideas.filter.promoted', icon: '🚀' },
|
{ key: 'promoted', labelKey: 'ideas.filter.promoted', icon: '🚀' },
|
||||||
|
// 归档功能 P1:归档箱(独立数据源 listArchived,不走主查询)
|
||||||
|
{ key: 'archived', labelKey: 'ideas.filterArchived', icon: '📦' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// ── 状态映射(单一数据源在 @/constants/project,列表/详情/下拉共用) ──
|
// ── 状态映射(单一数据源在 @/constants/project,列表/详情/下拉共用) ──
|
||||||
@@ -237,9 +242,21 @@ function buildIdeaQuery(): IdeaQuery {
|
|||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 归档功能 P1:归档箱独立数据源(切到归档箱 filter 时拉取;其余 filter 保持 null 不请求)
|
||||||
|
const archivedIdeas = ref<IdeaRecord[]>([])
|
||||||
|
|
||||||
// 按当前视图查询加载(登记 setActiveIdeaQuery 供 df-data-changed 联动刷新复用同一筛选,
|
// 按当前视图查询加载(登记 setActiveIdeaQuery 供 df-data-changed 联动刷新复用同一筛选,
|
||||||
// 避免 promote/relate/监听触发无参 loadIdeas 全量冲掉当前筛选视图)
|
// 避免 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())
|
store.setActiveIdeaQuery(buildIdeaQuery())
|
||||||
return store.loadIdeas(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) })
|
watch(searchQuery, () => { page.value = 1; void reloadIdeas(false) })
|
||||||
|
|
||||||
const filteredIdeas = computed(() => {
|
const filteredIdeas = computed(() => {
|
||||||
|
// 归档箱模式:整列表切归档数据源
|
||||||
|
if (activeFilter.value === 'archived') return archivedIdeas.value
|
||||||
|
|
||||||
let ideas = store.ideas
|
let ideas = store.ideas
|
||||||
|
|
||||||
// hot(按 score≥80)/pending(多状态 draft+pending_review)保留前端 filter:
|
// 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() {
|
async function promoteToProject() {
|
||||||
if (!currentIdea.value || promoting.value) return
|
if (!currentIdea.value || promoting.value) return
|
||||||
promoting.value = true
|
promoting.value = true
|
||||||
|
|||||||
+50
-3
@@ -60,6 +60,8 @@
|
|||||||
<div class="filter-group view-toggle">
|
<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 === '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>
|
<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>
|
</div>
|
||||||
<!-- 列表模式全选(看板无批量栏,隐藏) -->
|
<!-- 列表模式全选(看板无批量栏,隐藏) -->
|
||||||
<label v-if="viewMode === 'list'" class="select-all" :title="$t('tasks.batch.selectHint')">
|
<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 class="task-groups" v-if="viewMode === 'list'">
|
||||||
<div v-if="loading" class="empty-state">{{ $t('common.loading') }}</div>
|
<div v-if="loading" class="empty-state">{{ $t('common.loading') }}</div>
|
||||||
<div v-else-if="filteredGroups.length === 0" class="empty-state">
|
<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>
|
<div>{{ $t('tasks.group.empty') }}</div>
|
||||||
<button class="btn btn-primary btn-sm" @click="openCreateModal()">{{ $t('tasks.create') }}</button>
|
<button class="btn btn-primary btn-sm" @click="openCreateModal()">{{ $t('tasks.create') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,6 +261,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-menu-divider"></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>
|
<button class="quick-menu-item quick-menu-danger" @click="quickDelete(row.task)">{{ $t('tasks.quickDelete') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,6 +438,10 @@ const activeProject = usePersistedRef('tasks.activeProject', 'all')
|
|||||||
const activeStatuses = usePersistedRef<string[]>('tasks.activeStatuses', [])
|
const activeStatuses = usePersistedRef<string[]>('tasks.activeStatuses', [])
|
||||||
// 列表/看板视图切换(持久化;看板模式隐藏状态多选与排序,列即状态)
|
// 列表/看板视图切换(持久化;看板模式隐藏状态多选与排序,列即状态)
|
||||||
const viewMode = usePersistedRef<'list' | 'kanban'>('tasks.viewMode', 'list')
|
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')
|
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 closeQuickMenu() { quickMenuId.value = null }
|
||||||
function toggleGroup(name: string) {
|
function toggleGroup(name: string) {
|
||||||
@@ -631,10 +676,12 @@ interface TaskGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 树组装 — 顶层 = 无 parent_id 的任务,每个顶层挂 children;父进度前端计算(D6,全量已在前端)。
|
// 树组装 — 顶层 = 无 parent_id 的任务,每个顶层挂 children;父进度前端计算(D6,全量已在前端)。
|
||||||
|
// 归档功能 P1:归档箱模式数据源切 archivedTasks(独立拉取,不经 buildTaskQuery)。
|
||||||
|
const listSource = computed<TaskRecord[]>(() => (archiveBoxMode.value ? archivedTasks.value : store.tasks))
|
||||||
const taskRows = computed<TaskRow[]>(() => {
|
const taskRows = computed<TaskRow[]>(() => {
|
||||||
// 子任务按 parent_id 分组(1 级嵌套)
|
// 子任务按 parent_id 分组(1 级嵌套)
|
||||||
const childMap = new Map<string, TaskRecord[]>()
|
const childMap = new Map<string, TaskRecord[]>()
|
||||||
for (const t of store.tasks) {
|
for (const t of listSource.value) {
|
||||||
if (!t.parent_id) continue
|
if (!t.parent_id) continue
|
||||||
const arr = childMap.get(t.parent_id) ?? []
|
const arr = childMap.get(t.parent_id) ?? []
|
||||||
arr.push(t)
|
arr.push(t)
|
||||||
@@ -648,7 +695,7 @@ const taskRows = computed<TaskRow[]>(() => {
|
|||||||
const bv = (b as any)[sortBy.value] ?? ''
|
const bv = (b as any)[sortBy.value] ?? ''
|
||||||
return String(bv).localeCompare(String(av))
|
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[] = []
|
const rows: TaskRow[] = []
|
||||||
for (const t of tops) {
|
for (const t of tops) {
|
||||||
const children = (childMap.get(t.id) ?? []).sort(cmp)
|
const children = (childMap.get(t.id) ?? []).sort(cmp)
|
||||||
|
|||||||
Reference in New Issue
Block a user