feat(fe-project): add project management UI — list, create, delete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7328057e7d
commit
d8071bc9f3
|
|
@ -0,0 +1,25 @@
|
||||||
|
import api from '@/shared/api'
|
||||||
|
import type { Project } from '@/shared/types/api'
|
||||||
|
|
||||||
|
export async function listProjects(): Promise<Project[]> {
|
||||||
|
const { data } = await api.get<Project[]>('/projects')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProject(name: string, designDir: string, codeDir?: string): Promise<Project> {
|
||||||
|
const { data } = await api.post<Project>('/projects', {
|
||||||
|
name,
|
||||||
|
design_dir: designDir,
|
||||||
|
code_dir: codeDir || null,
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProject(id: string): Promise<Project> {
|
||||||
|
const { data } = await api.get<Project>(`/projects/${id}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProject(id: string): Promise<void> {
|
||||||
|
await api.delete(`/projects/${id}`)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="project-list">
|
||||||
|
<h1>项目管理</h1>
|
||||||
|
|
||||||
|
<div class="add-section">
|
||||||
|
<button class="primary" @click="showForm = !showForm">
|
||||||
|
{{ showForm ? '取消' : '添加项目' }}
|
||||||
|
</button>
|
||||||
|
<form v-if="showForm" class="card add-form" @submit.prevent="handleCreate">
|
||||||
|
<input v-model="form.name" placeholder="项目名称" required />
|
||||||
|
<input v-model="form.designDir" placeholder="设计目录路径" required />
|
||||||
|
<input v-model="form.codeDir" placeholder="代码目录路径(可选)" />
|
||||||
|
<button type="submit" class="primary" :disabled="loading">创建</button>
|
||||||
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading && projects.length === 0" class="loading">加载中...</div>
|
||||||
|
<div v-if="!loading && projects.length === 0" class="empty">暂无项目,点击"添加项目"开始</div>
|
||||||
|
|
||||||
|
<ProjectOverview
|
||||||
|
v-for="p in projects"
|
||||||
|
:key="p.id"
|
||||||
|
:project="p"
|
||||||
|
@click="goToProject(p.id)"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useProjectStore } from '../composables/useProject'
|
||||||
|
import ProjectOverview from './ProjectOverview.vue'
|
||||||
|
|
||||||
|
const store = useProjectStore()
|
||||||
|
const { projects, loading, error } = storeToRefs(store)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const showForm = ref(false)
|
||||||
|
const form = ref({ name: '', designDir: '', codeDir: '' })
|
||||||
|
|
||||||
|
onMounted(() => { store.fetchProjects() })
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
try {
|
||||||
|
const project = await store.createProject(form.value.name, form.value.designDir, form.value.codeDir || undefined)
|
||||||
|
showForm.value = false
|
||||||
|
form.value = { name: '', designDir: '', codeDir: '' }
|
||||||
|
router.push(`/projects/${project.id}`)
|
||||||
|
} catch { /* error handled in store */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToProject(id: string) {
|
||||||
|
store.selectProject(id)
|
||||||
|
router.push(`/projects/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id: string) {
|
||||||
|
if (confirm('确认删除该项目?')) {
|
||||||
|
await store.removeProject(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 { margin-bottom: 16px; }
|
||||||
|
.add-section { margin-bottom: 16px; }
|
||||||
|
.add-form { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.empty { color: #999; padding: 24px; text-align: center; }
|
||||||
|
.loading { color: #999; padding: 24px; text-align: center; }
|
||||||
|
.error { color: #F44336; font-size: 13px; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="card project-card">
|
||||||
|
<div class="project-header">
|
||||||
|
<h3>{{ project.name }}</h3>
|
||||||
|
<button class="danger" @click.stop="$emit('delete', project.id)">删除</button>
|
||||||
|
</div>
|
||||||
|
<p class="meta">{{ project.design_dir }}</p>
|
||||||
|
<p class="meta">创建于 {{ new Date(project.created_at).toLocaleDateString() }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Project } from '@/shared/types/api'
|
||||||
|
|
||||||
|
defineProps<{ project: Project }>()
|
||||||
|
defineEmits<{ delete: [id: string] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.project-card { cursor: pointer; }
|
||||||
|
.project-card:hover { border-color: #1976D2; }
|
||||||
|
.project-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.project-header h3 { font-size: 16px; }
|
||||||
|
.meta { font-size: 13px; color: #666; margin-top: 4px; }
|
||||||
|
</style>
|
||||||
41
frontend/src/modules/project/components/ProjectSidebar.vue
Normal file
41
frontend/src/modules/project/components/ProjectSidebar.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div class="project-sidebar">
|
||||||
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
|
<div v-for="p in projects" :key="p.id" class="project-item" :class="{ active: currentProject?.id === p.id }" @click="goToProject(p.id)">
|
||||||
|
<span class="project-name">{{ p.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && projects.length === 0" class="empty">暂无项目</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useProjectStore } from '../composables/useProject'
|
||||||
|
|
||||||
|
const store = useProjectStore()
|
||||||
|
const { projects, currentProject, loading } = storeToRefs(store)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => { store.fetchProjects() })
|
||||||
|
|
||||||
|
function goToProject(id: string) {
|
||||||
|
store.selectProject(id)
|
||||||
|
router.push(`/projects/${id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.project-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.project-item:hover { background: #f0f0f0; }
|
||||||
|
.project-item.active { background: #e3f2fd; color: #1976D2; }
|
||||||
|
.project-name { font-size: 14px; }
|
||||||
|
.empty { color: #999; font-size: 13px; padding: 8px; }
|
||||||
|
.loading { color: #999; font-size: 13px; padding: 8px; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { Project } from '@/shared/types/api'
|
||||||
|
import * as projectApi from '../api'
|
||||||
|
|
||||||
|
export const useProjectStore = defineStore('project', () => {
|
||||||
|
const projects = ref<Project[]>([])
|
||||||
|
const currentProject = ref<Project | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function fetchProjects() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
projects.value = await projectApi.listProjects()
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProject(name: string, designDir: string, codeDir?: string) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const project = await projectApi.createProject(name, designDir, codeDir)
|
||||||
|
projects.value.push(project)
|
||||||
|
return project
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.response?.data?.detail || e.message
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectProject(id: string) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
currentProject.value = await projectApi.getProject(id)
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeProject(id: string) {
|
||||||
|
await projectApi.deleteProject(id)
|
||||||
|
projects.value = projects.value.filter(p => p.id !== id)
|
||||||
|
if (currentProject.value?.id === id) {
|
||||||
|
currentProject.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { projects, currentProject, loading, error, fetchProjects, createProject, selectProject, removeProject }
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export type { Project } from '@/shared/types/api'
|
||||||
Loading…
Reference in New Issue
Block a user