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