zyc 44464dd334 feat: DevPerf Dashboard 研发人效看板 v1.0
- 后端:Bun + Hono + Drizzle ORM + SQLite
- 前端:Vue 3 + Naive UI + ECharts
- 项目管理:创建项目 + 绑定 Git 仓库
- OKR 系统:目标/关键结果 CRUD + 进度追踪
- Git 同步:Gitea API 自动同步 commit/PR + 作者关联
- 数据看板:项目 OKR 进度 + KR 状态分布 + 代码活动
- 权限体系:admin/manager/developer/viewer 四级
- Docker 部署:docker-compose + nginx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:57:14 +08:00

97 lines
2.6 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { NBreadcrumb, NBreadcrumbItem } from 'naive-ui';
const route = useRoute();
/**
* Map each route name to a human-readable page title.
* Every named route MUST have an explicit entry here to avoid the fallback
* 'Dashboard' label, which would cause "Dashboard / Dashboard" in breadcrumbs.
*/
const pageTitle = computed(() => {
switch (route.name) {
case 'Overview': return '团队总览';
case 'ProjectList': return '项目列表';
case 'ProjectDetail': return '项目明细';
case 'MemberList': return '团队成员';
case 'MemberDetail': return '个人产出';
case 'OKR': return 'OKR 看板';
case 'GitActivity': return 'Git 活动统计';
case 'Admin': return '管理后台';
default: return String(route.name || '首页');
}
});
const breadcrumbs = computed(() => {
const items = [{ label: '首页', path: '/' }];
if (route.name === 'ProjectDetail') {
items.push({ label: '项目', path: '/projects' });
items.push({ label: '项目明细', path: route.fullPath });
} else if (route.name === 'MemberDetail') {
const fromProject = route.query.from as string;
if (fromProject) {
items.push({ label: '项目明细', path: `/projects/${fromProject}` });
} else {
items.push({ label: '成员', path: '/members' });
}
items.push({ label: '个人产出', path: route.fullPath });
} else if (route.name !== 'Overview') {
items.push({ label: pageTitle.value, path: route.fullPath });
}
return items;
});
</script>
<template>
<header class="app-header">
<div class="header-left">
<NBreadcrumb>
<NBreadcrumbItem v-for="(item, idx) in breadcrumbs" :key="idx">
<router-link v-if="idx < breadcrumbs.length - 1" :to="item.path">
{{ item.label }}
</router-link>
<span v-else>{{ item.label }}</span>
</NBreadcrumbItem>
</NBreadcrumb>
<h1 class="page-title">{{ pageTitle }}</h1>
</div>
</header>
</template>
<style scoped>
.app-header {
height: 64px;
background: var(--color-bg-card);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
padding: 0 var(--space-6);
}
.header-left {
display: flex;
flex-direction: column;
gap: 2px;
}
.page-title {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
margin: 0;
}
/* On mobile, leave space for the hamburger button */
@media (max-width: 768px) {
.app-header {
padding-left: 60px;
}
.page-title {
font-size: 16px;
}
}
</style>