Files
tori/web/src/components/AppLayout.vue
Fam Zheng 1aa81896b5 Add project soft-delete with workspace archival
- Add delete button (×) to sidebar project list, shown on hover
- Soft-delete: mark projects as deleted in DB instead of hard delete
- Move workspace files to /app/data/deleted/ folder on deletion
- Filter deleted projects from list query
- Auto-select next project after deleting current one
- Also includes agent prompt improvements for reverse proxy paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 07:56:37 +00:00

168 lines
4.3 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import Sidebar from './Sidebar.vue'
import WorkflowView from './WorkflowView.vue'
import ReportView from './ReportView.vue'
import CreateForm from './CreateForm.vue'
import { api } from '../api'
import type { Project } from '../types'
const projects = ref<Project[]>([])
const selectedProjectId = ref('')
const reportWorkflowId = ref('')
const error = ref('')
const creating = ref(false)
const isReportPage = computed(() => !!reportWorkflowId.value)
function parseUrl(): { projectId: string; reportId: string } {
const reportMatch = location.pathname.match(/^\/report\/([^/]+)/)
if (reportMatch) return { projectId: '', reportId: reportMatch[1] ?? '' }
const projectMatch = location.pathname.match(/^\/projects\/([^/]+)/)
return { projectId: projectMatch?.[1] ?? '', reportId: '' }
}
function onPopState() {
const { projectId, reportId } = parseUrl()
selectedProjectId.value = projectId
reportWorkflowId.value = reportId
}
onMounted(async () => {
try {
projects.value = await api.listProjects()
const { projectId, reportId } = parseUrl()
if (reportId) {
reportWorkflowId.value = reportId
} else if (projectId && projects.value.some(p => p.id === projectId)) {
selectedProjectId.value = projectId
} else if (projects.value[0]) {
selectedProjectId.value = projects.value[0].id
history.replaceState(null, '', `/projects/${projects.value[0].id}`)
}
} catch (e: any) {
error.value = e.message
}
window.addEventListener('popstate', onPopState)
})
onUnmounted(() => {
window.removeEventListener('popstate', onPopState)
})
function onSelectProject(id: string) {
selectedProjectId.value = id
reportWorkflowId.value = ''
creating.value = false
history.pushState(null, '', `/projects/${id}`)
}
function onStartCreate() {
creating.value = true
selectedProjectId.value = ''
history.pushState(null, '', '/')
}
async function onConfirmCreate(req: string) {
try {
const project = await api.createProject('新项目')
projects.value.unshift(project)
await api.createWorkflow(project.id, req)
creating.value = false
selectedProjectId.value = project.id
history.pushState(null, '', `/projects/${project.id}`)
} catch (e: any) {
error.value = e.message
}
}
function onProjectUpdate(projectId: string, name: string) {
const p = projects.value.find(p => p.id === projectId)
if (p) p.name = name
}
async function onDeleteProject(id: string) {
try {
await api.deleteProject(id)
projects.value = projects.value.filter(p => p.id !== id)
if (selectedProjectId.value === id) {
selectedProjectId.value = projects.value[0]?.id ?? ''
if (selectedProjectId.value) {
history.replaceState(null, '', `/projects/${selectedProjectId.value}`)
} else {
history.replaceState(null, '', '/')
}
}
} catch (e: any) {
error.value = e.message
}
}
</script>
<template>
<div v-if="isReportPage" class="report-fullpage">
<ReportView :workflowId="reportWorkflowId" :key="reportWorkflowId" />
</div>
<div v-else class="app-layout">
<Sidebar
:projects="projects"
:selectedId="selectedProjectId"
@select="onSelectProject"
@create="onStartCreate"
@delete="onDeleteProject"
/>
<main class="main-content">
<div v-if="error" class="error-banner" @click="error = ''">{{ error }}</div>
<div v-if="creating" class="empty-state">
<CreateForm @submit="onConfirmCreate" @cancel="creating = false" />
</div>
<div v-else-if="!selectedProjectId" class="empty-state">
选择或创建一个项目开始
</div>
<WorkflowView
v-else
:projectId="selectedProjectId"
:key="selectedProjectId"
@projectUpdate="onProjectUpdate"
/>
</main>
</div>
</template>
<style scoped>
.report-fullpage {
height: 100vh;
overflow-y: auto;
}
.app-layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.main-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 16px;
}
.error-banner {
background: var(--error);
color: #fff;
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
}
</style>