- 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>
168 lines
4.3 KiB
Vue
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>
|