You've already forked agentic-coding-workflow
146 lines
4.4 KiB
Vue
146 lines
4.4 KiB
Vue
<template>
|
|
<AppShell>
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-semibold">欢迎,{{ auth.user?.display_name }}</h2>
|
|
<NButton type="primary" @click="goToProjects"> 新建 / 管理项目 </NButton>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
<NGrid :cols="3" :x-gap="12">
|
|
<NGi>
|
|
<NCard>
|
|
<NStatistic label="活跃项目" :value="activeCount" />
|
|
</NCard>
|
|
</NGi>
|
|
<NGi>
|
|
<NCard>
|
|
<NStatistic label="归档项目" :value="archivedCount" />
|
|
</NCard>
|
|
</NGi>
|
|
<NGi>
|
|
<NCard>
|
|
<NStatistic label="分配给我的待办议题" :value="myIssues.length" />
|
|
</NCard>
|
|
</NGi>
|
|
</NGrid>
|
|
|
|
<!-- 分配给我的议题 -->
|
|
<NCard title="分配给我的议题(未关闭)">
|
|
<NSpin :show="loading">
|
|
<div v-if="myIssues.length === 0" class="text-sm text-gray-400 py-2">
|
|
暂无分配给你的未关闭议题。
|
|
</div>
|
|
<ul v-else class="divide-y">
|
|
<li
|
|
v-for="row in myIssues"
|
|
:key="row.issue.id"
|
|
class="py-2 flex items-center justify-between cursor-pointer hover:bg-gray-50 px-1"
|
|
@click="goIssue(row)"
|
|
>
|
|
<span class="text-sm">
|
|
<span class="font-mono text-gray-500 mr-2"
|
|
>{{ row.projectSlug }}#{{ row.issue.number }}</span
|
|
>
|
|
{{ row.issue.title }}
|
|
</span>
|
|
<span class="text-xs text-gray-400">{{ row.projectName }}</span>
|
|
</li>
|
|
</ul>
|
|
</NSpin>
|
|
</NCard>
|
|
|
|
<!-- 最近项目 -->
|
|
<NCard title="项目">
|
|
<NSpin :show="loading">
|
|
<div v-if="activeProjects.length === 0" class="text-sm text-gray-400 py-2">
|
|
还没有项目,去创建一个吧。
|
|
</div>
|
|
<ul v-else class="divide-y">
|
|
<li
|
|
v-for="p in activeProjects"
|
|
:key="p.id"
|
|
class="py-2 flex items-center justify-between cursor-pointer hover:bg-gray-50 px-1"
|
|
@click="goToProject(p)"
|
|
>
|
|
<span class="text-sm font-medium">{{ p.name }}</span>
|
|
<span class="font-mono text-xs text-gray-400">{{ p.slug }}</span>
|
|
</li>
|
|
</ul>
|
|
</NSpin>
|
|
</NCard>
|
|
</div>
|
|
</AppShell>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { NButton, NCard, NGrid, NGi, NStatistic, NSpin } from 'naive-ui'
|
|
|
|
import AppShell from '@/layouts/AppShell.vue'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { projectsApi, type Project, type Issue } from '@/api/projects'
|
|
|
|
const auth = useAuthStore()
|
|
const router = useRouter()
|
|
|
|
const projects = ref<Project[]>([])
|
|
const loading = ref(false)
|
|
|
|
interface MyIssueRow {
|
|
issue: Issue
|
|
projectSlug: string
|
|
projectName: string
|
|
}
|
|
const myIssues = ref<MyIssueRow[]>([])
|
|
|
|
const activeProjects = computed(() => projects.value.filter((p) => !p.archived_at))
|
|
const activeCount = computed(() => activeProjects.value.length)
|
|
const archivedCount = computed(() => projects.value.filter((p) => p.archived_at).length)
|
|
|
|
onMounted(async () => {
|
|
loading.value = true
|
|
try {
|
|
const out = await projectsApi.list(true)
|
|
projects.value = out.items
|
|
const uid = auth.user?.id
|
|
if (uid) {
|
|
// 跨活跃项目聚合“分配给我且未关闭”的议题。项目数通常较小,直接并发查询。
|
|
const lists = await Promise.all(
|
|
activeProjects.value.map(async (p) => {
|
|
try {
|
|
const res = await projectsApi.listIssues(p.slug, { assignee: uid, status: 'open' })
|
|
return res.items.map((issue) => ({
|
|
issue,
|
|
projectSlug: p.slug,
|
|
projectName: p.name,
|
|
}))
|
|
} catch {
|
|
return []
|
|
}
|
|
}),
|
|
)
|
|
myIssues.value = lists.flat()
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
function goIssue(row: MyIssueRow) {
|
|
router.push({
|
|
name: 'issue-detail',
|
|
params: { slug: row.projectSlug, number: row.issue.number },
|
|
})
|
|
}
|
|
|
|
function goToProjects() {
|
|
router.push({ name: 'project-list' })
|
|
}
|
|
|
|
function goToProject(p: Project) {
|
|
router.push({ name: 'project-home', params: { slug: p.slug } })
|
|
}
|
|
</script>
|