- 后端: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>
97 lines
2.6 KiB
Vue
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>
|