feat: implement full arch design dashboard #1

Open
openclaw wants to merge 38 commits from feat/full-implementation into main
6 changed files with 226 additions and 0 deletions
Showing only changes of commit d8071bc9f3 - Show all commits

View File

@ -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}`)
}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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 }
})

View File

@ -0,0 +1 @@
export type { Project } from '@/shared/types/api'