You've already forked agentic-coding-workflow
feat(web): project list and home views
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<NSpin :show="store.loading">
|
||||
<div v-if="store.current" class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-2xl font-semibold">{{ store.current.name }}</h1>
|
||||
<NTag :type="store.current.visibility === 'private' ? 'warning' : 'info'" size="small">
|
||||
{{ store.current.visibility === 'private' ? '私有' : '内部' }}
|
||||
</NTag>
|
||||
<NTag v-if="store.current.archived_at" size="small">已归档</NTag>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm font-mono">{{ store.current.slug }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p v-if="store.current.description" class="text-gray-600">{{ store.current.description }}</p>
|
||||
|
||||
<!-- Nav buttons -->
|
||||
<div class="flex gap-3">
|
||||
<NButton type="primary" @click="goRequirements">需求列表</NButton>
|
||||
<NButton @click="goIssues">Issue 列表</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!store.loading" class="text-gray-400 text-sm">项目不存在或无权访问。</div>
|
||||
</NSpin>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { NButton, NTag, NSpin } from 'naive-ui'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useProjectsStore } from '@/stores/projects'
|
||||
|
||||
const store = useProjectsStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const slug = route.params.slug as string
|
||||
|
||||
onMounted(() => store.load(slug))
|
||||
|
||||
function goRequirements() {
|
||||
router.push({ name: 'requirement-list', params: { slug } })
|
||||
}
|
||||
|
||||
function goIssues() {
|
||||
router.push({ name: 'issue-list', params: { slug } })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<NSpin :show="store.loading">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">项目</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-500">显示已归档</span>
|
||||
<NSwitch v-model:value="store.includeArchived" @update:value="store.refresh()" />
|
||||
</div>
|
||||
<NButton type="primary" @click="showCreate = true">新建项目</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active projects grid -->
|
||||
<div v-if="store.activeItems.length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<NCard
|
||||
v-for="p in store.activeItems"
|
||||
:key="p.id"
|
||||
class="cursor-pointer hover:shadow-md transition-shadow"
|
||||
@click="goProject(p.slug)"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">{{ p.name }}</span>
|
||||
<NTag :type="p.visibility === 'private' ? 'warning' : 'info'" size="small">
|
||||
{{ p.visibility === 'private' ? '私有' : '内部' }}
|
||||
</NTag>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 line-clamp-2">{{ p.description || '暂无描述' }}</p>
|
||||
<p class="text-xs text-gray-400 font-mono">{{ p.slug }}</p>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
<div v-else-if="!store.loading" class="text-gray-400 text-sm">暂无活跃项目。</div>
|
||||
|
||||
<!-- Archived projects -->
|
||||
<template v-if="store.includeArchived && store.archivedItems.length > 0">
|
||||
<h2 class="text-base font-medium text-gray-500">已归档</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<NCard
|
||||
v-for="p in store.archivedItems"
|
||||
:key="p.id"
|
||||
class="opacity-60 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
@click="goProject(p.slug)"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">{{ p.name }}</span>
|
||||
<NTag size="small">已归档</NTag>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 line-clamp-2">{{ p.description || '暂无描述' }}</p>
|
||||
<p class="text-xs text-gray-400 font-mono">{{ p.slug }}</p>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</NSpin>
|
||||
|
||||
<!-- Create project modal -->
|
||||
<NModal v-model:show="showCreate" preset="card" title="新建项目" class="w-full max-w-md">
|
||||
<NForm @submit.prevent="onSubmit">
|
||||
<NFormItem label="Slug(URL 标识)">
|
||||
<NInput v-model:value="form.slug" placeholder="my-project" />
|
||||
</NFormItem>
|
||||
<NFormItem label="名称">
|
||||
<NInput v-model:value="form.name" placeholder="My Project" />
|
||||
</NFormItem>
|
||||
<NFormItem label="描述">
|
||||
<NInput v-model:value="form.description" type="textarea" :rows="3" placeholder="(可选)" />
|
||||
</NFormItem>
|
||||
<NFormItem label="可见性">
|
||||
<NRadioGroup v-model:value="form.visibility">
|
||||
<NRadio value="private">私有</NRadio>
|
||||
<NRadio value="internal">内部</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<NButton @click="showCreate = false">取消</NButton>
|
||||
<NButton type="primary" attr-type="submit" :loading="creating">创建</NButton>
|
||||
</div>
|
||||
</NForm>
|
||||
<p v-if="createError" class="text-red-500 text-sm mt-2">{{ createError }}</p>
|
||||
</NModal>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
NTag,
|
||||
NModal,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NSwitch,
|
||||
NSpin,
|
||||
} from 'naive-ui'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useProjectsStore } from '@/stores/projects'
|
||||
import type { Visibility } from '@/api/projects'
|
||||
|
||||
const store = useProjectsStore()
|
||||
const router = useRouter()
|
||||
|
||||
const showCreate = ref(false)
|
||||
const creating = ref(false)
|
||||
const createError = ref('')
|
||||
|
||||
const form = ref<{ slug: string; name: string; description: string; visibility: Visibility }>({
|
||||
slug: '',
|
||||
name: '',
|
||||
description: '',
|
||||
visibility: 'private',
|
||||
})
|
||||
|
||||
onMounted(() => store.refresh())
|
||||
|
||||
function goProject(slug: string) {
|
||||
router.push({ name: 'project-home', params: { slug } })
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
createError.value = ''
|
||||
if (!form.value.slug || !form.value.name) {
|
||||
createError.value = 'Slug 和名称不能为空'
|
||||
return
|
||||
}
|
||||
creating.value = true
|
||||
try {
|
||||
const p = await store.create({
|
||||
slug: form.value.slug,
|
||||
name: form.value.name,
|
||||
description: form.value.description || undefined,
|
||||
visibility: form.value.visibility,
|
||||
})
|
||||
showCreate.value = false
|
||||
form.value = { slug: '', name: '', description: '', visibility: 'private' }
|
||||
router.push({ name: 'project-home', params: { slug: p.slug } })
|
||||
} catch (e) {
|
||||
createError.value = e instanceof Error ? e.message : '创建失败'
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user