新增: 项目归档
This commit is contained in:
@@ -96,6 +96,16 @@ export const projectApi = {
|
||||
return invoke('list_deleted_projects')
|
||||
},
|
||||
|
||||
/** 归档/取消归档项目(归档功能 P2,V48)。归档后退出主列表,归档箱内可见可恢复 */
|
||||
archive(id: string, archived: boolean): Promise<boolean> {
|
||||
return invoke('archive_project', { id, archived })
|
||||
},
|
||||
|
||||
/** 列出归档项目(归档箱数据源) */
|
||||
listArchived(): Promise<ProjectRecord[]> {
|
||||
return invoke('list_archived_projects')
|
||||
},
|
||||
|
||||
restore(id: string): Promise<boolean> {
|
||||
return invoke('restore_project', { id })
|
||||
},
|
||||
|
||||
@@ -430,6 +430,12 @@ export type AiChatEvent = ({
|
||||
// 技能注入反馈闭环:/ 技能 resolve 成功(ok=true)/失败(ok=false,未找到或读失败)通知,
|
||||
// 前端轻 toast(成功「已注入技能《name》」/失败「技能 name 未找到」)。仅提示,不改消息流。
|
||||
type: 'AiSkillInjected'; name: string; ok: boolean
|
||||
} | {
|
||||
// 工具执行实时流式输出(run_command shell 行级 / ask_uclaw 进度块级):
|
||||
// 后端经 task-local sink(command_stream / tools/uclaw)emit,前端按 id 路由到对应
|
||||
// 工具卡,在 Started→Completed 间实时展示。合批在后端(50ms/4KB 或 u-claw 2s 节流)。
|
||||
// stream: "stdout"|"stderr"(来源,前端差异化着色);line:累积文本块(多行 '\n' 连接)。
|
||||
type: 'AiCommandOutput'; id: string; stream: string; line: string
|
||||
} | {
|
||||
// path_auth 审批链阶段3b(统一审批模型):AiApprovalRequired 携带 kind 字段区分
|
||||
// 'path'(F-260619-03 Phase B 路径授权挂起,走 once/session/always/deny)vs 'risk'(普通 RiskLevel 审批,
|
||||
@@ -744,6 +750,13 @@ export interface AiToolCallInfo {
|
||||
startedAt?: number
|
||||
/** 工具执行完成时间(前端在 AiToolCallCompleted 时记录,毫秒)。语义同 startedAt。 */
|
||||
completedAt?: number
|
||||
/**
|
||||
* 执行中实时流式输出(AiCommandOutput 事件累积):run_command shell 行级 /
|
||||
* ask_uclaw 进度块级。数组按到达序追加,ToolCard running 态实时渲染(尾部 N 行),
|
||||
* Completed 时清空(真实结果由 result 字段渲染,防双显)。
|
||||
* undefined = 无流式输出的工具或历史消息(DB 回载不含此内存态)。
|
||||
*/
|
||||
streamLines?: string[]
|
||||
/**
|
||||
* A2-B10 conv-scoped 审批收尾:挂起审批所属的会话 id(前端判定用,仅挂起项设置)。
|
||||
* 来源:AiApprovalRequired / AiDirAuthRequired(统一审批归一)事件透传 conversation_id
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</div>
|
||||
<div class="card-top-actions">
|
||||
<span class="stage-badge" :class="stageClass(project.status)">{{ $t(statusLabel(project.status)) }}</span>
|
||||
<button class="card-del-btn" :title="$t('projects.archiveTitle')" @click.stop="emit('archive', project)">📦</button>
|
||||
<button class="card-del-btn" :title="$t('projects.deleteTitle')" @click.stop="emit('delete', project)">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,6 +69,7 @@ const props = withDefaults(defineProps<{
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete', project: ProjectRecord): void
|
||||
(e: 'archive', project: ProjectRecord): void
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -23,6 +23,16 @@ export default {
|
||||
purge: 'Delete Permanently',
|
||||
// Project card
|
||||
deleteTitle: 'Delete',
|
||||
// Archive box (archive P2, V48; aligned with trash flow, reversible, no hard delete)
|
||||
archiveBox: '📦 Archive',
|
||||
archiveBoxTitle: '📦 Archive Box',
|
||||
archiveBoxEmpty: 'Archive box is empty',
|
||||
archiveTitle: 'Archive',
|
||||
unarchive: '↩ Unarchive',
|
||||
archivedAt: 'archived at',
|
||||
confirmArchive: 'Archive project "{name}"? It will be hidden from the main list and can be restored from the archive box anytime.',
|
||||
archivedToast: 'Archived',
|
||||
unarchivedToast: 'Restored to main list',
|
||||
// Empty state
|
||||
empty: 'No projects yet, click the top-right button to create your first project',
|
||||
// Confirm prompts
|
||||
@@ -80,6 +90,8 @@ export default {
|
||||
loadTrashFailed: 'Failed to load trash',
|
||||
restoreFailed: 'Failed to restore project',
|
||||
purgeFailed: 'Failed to delete permanently',
|
||||
archiveFailed: 'Archive operation failed',
|
||||
loadArchivedFailed: 'Failed to load archive box',
|
||||
loadWorkflowFailed: 'Failed to load workflow records',
|
||||
approveFailed: 'Approval operation failed',
|
||||
cancelFailed: 'Cancel approval failed',
|
||||
|
||||
@@ -54,6 +54,16 @@ export default {
|
||||
purge: '彻底删除',
|
||||
// 项目卡片
|
||||
deleteTitle: '删除',
|
||||
// 归档箱(归档功能 P2,V48;对齐回收站动线,可逆无彻底删除)
|
||||
archiveBox: '📦 归档箱',
|
||||
archiveBoxTitle: '📦 归档箱',
|
||||
archiveBoxEmpty: '归档箱为空',
|
||||
archiveTitle: '归档',
|
||||
unarchive: '↩ 取消归档',
|
||||
archivedAt: '归档于',
|
||||
confirmArchive: '确定归档项目「{name}」?将从主列表隐藏,可随时从归档箱恢复。',
|
||||
archivedToast: '已归档',
|
||||
unarchivedToast: '已恢复到主列表',
|
||||
// 空状态
|
||||
empty: '暂无项目,点击右上角创建第一个项目',
|
||||
// 确认文案
|
||||
@@ -79,6 +89,8 @@ export default {
|
||||
loadTrashFailed: '加载回收站失败',
|
||||
restoreFailed: '恢复项目失败',
|
||||
purgeFailed: '彻底删除失败',
|
||||
archiveFailed: '归档操作失败',
|
||||
loadArchivedFailed: '加载归档箱失败',
|
||||
loadWorkflowFailed: '加载工作流记录失败',
|
||||
approveFailed: '审批操作失败',
|
||||
cancelFailed: '取消审批失败',
|
||||
|
||||
@@ -163,6 +163,7 @@ function createStore() {
|
||||
get loading() { return state.loading },
|
||||
get error() { return state.error },
|
||||
get deletedProjects() { return state.deletedProjects },
|
||||
get archivedProjects() { return state.archivedProjects },
|
||||
clearError,
|
||||
// project actions
|
||||
loadProjects: projectsStore.loadProjects,
|
||||
@@ -174,6 +175,9 @@ function createStore() {
|
||||
loadDeletedProjects: projectsStore.loadDeletedProjects,
|
||||
restoreProject: projectsStore.restoreProject,
|
||||
purgeProject: projectsStore.purgeProject,
|
||||
archiveProject: projectsStore.archiveProject,
|
||||
loadArchivedProjects: projectsStore.loadArchivedProjects,
|
||||
unarchiveProject: projectsStore.unarchiveProject,
|
||||
// task actions
|
||||
loadTasks: tasksStore.loadTasks,
|
||||
createTask: tasksStore.createTask,
|
||||
|
||||
@@ -77,6 +77,34 @@ export function createProjectsStore() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 归档功能 P2(V48):归档箱三动作,对标 tasks/ideas 子 store 同款 ──
|
||||
|
||||
/** 归档/取消归档。归档=退出主列表;取消归档=从归档箱移回主列表(两端各刷一份) */
|
||||
async function archiveProject(id: string, archived: boolean) {
|
||||
await runWithCatch(state, t('projects.err.archiveFailed'), async () => {
|
||||
await projectApi.archive(id, archived)
|
||||
if (archived) {
|
||||
// 从主列表移出,归档箱按需懒加载(打开归档箱时 loadArchivedProjects)
|
||||
state.projects = state.projects.filter(p => p.id !== id)
|
||||
} else {
|
||||
state.archivedProjects = state.archivedProjects.filter(p => p.id !== id)
|
||||
await loadProjects() // 取消归档回主列表
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载归档项目(归档箱 Tab 数据源) */
|
||||
async function loadArchivedProjects() {
|
||||
await runWithCatch(state, t('projects.err.loadArchivedFailed'), async () => {
|
||||
state.archivedProjects = await projectApi.listArchived()
|
||||
})
|
||||
}
|
||||
|
||||
/** 归档箱内恢复到主列表(取消归档语义的 UI 别名,动线用词更自然) */
|
||||
async function unarchiveProject(id: string) {
|
||||
await archiveProject(id, false)
|
||||
}
|
||||
|
||||
async function purgeProject(id: string) {
|
||||
try {
|
||||
await projectApi.purge(id) // 彻底删(级联物理删,不可恢复)
|
||||
@@ -96,6 +124,9 @@ export function createProjectsStore() {
|
||||
loadDeletedProjects,
|
||||
restoreProject,
|
||||
purgeProject,
|
||||
archiveProject,
|
||||
loadArchivedProjects,
|
||||
unarchiveProject,
|
||||
// clearError 来自 state(供 barrel re-export)
|
||||
clearError,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface PendingApproval {
|
||||
export const state = reactive({
|
||||
projects: [] as ProjectRecord[],
|
||||
deletedProjects: [] as ProjectRecord[],
|
||||
// 归档箱数据源(归档功能 P2,V48;独立于 projects——主列表后端已默认排除 archived)
|
||||
archivedProjects: [] as ProjectRecord[],
|
||||
tasks: [] as TaskRecord[],
|
||||
// 统计缓存,与 tasks 数组解耦,供 Dashboard,不被 Tasks 分页/筛选污染(P1-b A0)
|
||||
activeTaskCount: 0 as number,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
>▦</button>
|
||||
</div>
|
||||
<button class="btn btn-ghost" @click="openImportModal">{{ $t('projects.importHistory') }}</button>
|
||||
<button class="btn btn-ghost" @click="openArchiveBox">{{ $t('projects.archiveBox') }}</button>
|
||||
<button class="btn btn-ghost" @click="openTrash">{{ $t('projects.trash') }}</button>
|
||||
<button class="btn btn-primary" @click="showCreateModal = true">{{ $t('projects.create') }}</button>
|
||||
</div>
|
||||
@@ -91,6 +92,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 归档箱模态框(归档功能 P2,V48;对齐回收站 Modal 模式:列表+恢复,无彻底删除——归档可逆) -->
|
||||
<div v-if="showArchiveModal" class="modal-overlay" @click.self="showArchiveModal = false">
|
||||
<div class="modal-box" style="width: 520px">
|
||||
<h3 class="modal-title">{{ $t('projects.archiveBoxTitle') }}</h3>
|
||||
<div v-if="store.archivedProjects.length === 0" class="empty-state" style="padding: 30px 0">{{ $t('projects.archiveBoxEmpty') }}</div>
|
||||
<div v-else class="trash-list">
|
||||
<div class="trash-item" v-for="p in store.archivedProjects" :key="p.id">
|
||||
<div class="trash-info">
|
||||
<span class="trash-name">{{ p.name }}</span>
|
||||
<span class="trash-date">{{ $t('projects.archivedAt') }} {{ formatDate(p.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="trash-actions">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="!!unarchivingId" @click="handleUnarchive(p.id)">{{ $t('projects.unarchive') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-ghost" @click="showArchiveModal = false">{{ $t('common.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入历史项目模态框(F-260614-06 scan 第二步) -->
|
||||
<div v-if="showImportModal" class="modal-overlay" @click.self="closeImportModal">
|
||||
<div class="modal-box import-box">
|
||||
@@ -162,6 +185,7 @@
|
||||
:project="project"
|
||||
:view="viewMode"
|
||||
@delete="handleDelete"
|
||||
@archive="handleArchive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -348,6 +372,34 @@ async function handlePurge(project: ProjectRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 归档箱(归档功能 P2,V48;卡片 📦 归档 + 归档箱 Modal 恢复,对齐回收站动线) ──
|
||||
const showArchiveModal = ref(false)
|
||||
const unarchivingId = ref<string | null>(null)
|
||||
|
||||
/** 归档项目:确认后退出主列表(可从归档箱恢复) */
|
||||
async function handleArchive(project: ProjectRecord) {
|
||||
if (!await confirmDialog(t('projects.confirmArchive', { name: project.name }))) return
|
||||
await store.archiveProject(project.id, true)
|
||||
showToast(t('projects.archivedToast'), 'info')
|
||||
}
|
||||
|
||||
async function openArchiveBox() {
|
||||
await store.loadArchivedProjects()
|
||||
showArchiveModal.value = true
|
||||
}
|
||||
|
||||
/** 归档箱内恢复:回主列表 */
|
||||
async function handleUnarchive(id: string) {
|
||||
if (unarchivingId.value) return
|
||||
unarchivingId.value = id
|
||||
try {
|
||||
await store.unarchiveProject(id)
|
||||
showToast(t('projects.unarchivedToast'), 'success')
|
||||
} finally {
|
||||
unarchivingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// ── 加载 ──
|
||||
onMounted(() => {
|
||||
store.loadProjects()
|
||||
|
||||
Reference in New Issue
Block a user