frontend
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<!-- Breadcrumb and Title -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-lg font-medium">{{ displayTitle }}</h1>
|
||||
<div class="text-xs breadcrumbs">
|
||||
<ul>
|
||||
<li v-for="(item, index) in breadcrumbItems" :key="index">
|
||||
<template v-if="item.path">
|
||||
<a
|
||||
:href="item.path"
|
||||
class="text-gray-500"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-gray-500">{{ item.name }}</span>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: null // 默认为null,将从路由中获取
|
||||
},
|
||||
// Optional custom breadcrumb items
|
||||
customBreadcrumbs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// Generate breadcrumb items based on current route
|
||||
// 生成面包屑项
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (props.customBreadcrumbs.length > 0) {
|
||||
return props.customBreadcrumbs;
|
||||
}
|
||||
|
||||
// 获取当前路径并分割成段
|
||||
const pathSegments = route.path.split('/').filter(segment => segment);
|
||||
|
||||
return pathSegments.map((segment, index) => {
|
||||
const name = segment.charAt(0).toUpperCase() + segment.slice(1);
|
||||
|
||||
// 对于最后一段,不设置链接
|
||||
if (index === pathSegments.length - 1) {
|
||||
return { name, path: '' };
|
||||
}
|
||||
|
||||
// 创建到此段的路径
|
||||
const path = '/' + pathSegments.slice(0, index + 1).join('/');
|
||||
return { name, path };
|
||||
});
|
||||
});
|
||||
|
||||
// 显示的标题:如果提供了自定义标题则使用,否则使用当前路径的最后一段
|
||||
const displayTitle = computed(() => {
|
||||
if (props.title) {
|
||||
return props.title;
|
||||
}
|
||||
|
||||
const pathSegments = route.path.split('/').filter(segment => segment);
|
||||
if (pathSegments.length > 0) {
|
||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||
return lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
|
||||
}
|
||||
|
||||
return 'Dashboard';
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<aside class="w-60 bg-base-100 border-r border-base-200">
|
||||
<div class="flex items-center p-4 outline-none select-none">
|
||||
<div class="w-12 h-12 rounded-full">
|
||||
<img src='@/assets/logo.svg' class="">
|
||||
</div>
|
||||
<div class="p-0 text-2xl font-bold text-center">OpenTeam</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="menu p-4 w-60 min-h-full bg-base-100 text-base-content">
|
||||
<li v-for="item in menuItems" :key="item.label">
|
||||
<template v-if="item.type === 'link'">
|
||||
<router-link :to="item.to" :class="{ 'active': isActive(item.to) }">
|
||||
<component :is="item.icon" class="w-4" />
|
||||
{{ item.label }}
|
||||
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'title'">
|
||||
<span class="menu-title">
|
||||
<span>{{ item.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'submenu'">
|
||||
<details :open="item.open">
|
||||
<summary>
|
||||
<component :is="item.icon" class="w-4" />
|
||||
{{ item.label }}
|
||||
<div v-if="item.badge" class="badge badge-sm">{{ item.badge }}</div>
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="subItem in item.children" :key="subItem.label">
|
||||
<router-link :to="subItem.to" :class="{ 'active': isActive(subItem.to) }">
|
||||
<component :is="subItem.icon" class="w-4" />
|
||||
{{ subItem.label }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
LayoutDashboardIcon,
|
||||
ShieldPlus,
|
||||
UsersRoundIcon,
|
||||
KeyRoundIcon,
|
||||
MessageSquareIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
CommandIcon,
|
||||
BracesIcon,
|
||||
} from 'lucide-vue-next'
|
||||
import { ref, reactive, onMounted ,computed} from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {routes,generateMenuItemsFromRoutes}from '@/utils/router_menu.js'
|
||||
import { useAuthStore } from '@/stores/auth.js';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const userrole = computed(() => {
|
||||
const user = authStore.user;
|
||||
return user ? user.role : 0;
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
// 判断当前路由是否激活菜单项
|
||||
const isActive = (path) => {
|
||||
return route.path === path;
|
||||
// return router.currentRoute.value.fullPath.startsWith(path);
|
||||
};
|
||||
|
||||
let menuItems = reactive([
|
||||
{ type: 'link', label: 'Overview', to: '/dashboard/overview', icon: LayoutDashboardIcon },
|
||||
{ type: 'title', label: 'Apps' },
|
||||
{ type: 'link', label: 'Tokens', to: '/dashboard/tokens', icon: BracesIcon },
|
||||
{
|
||||
type: 'submenu', label: 'Manager', icon: CommandIcon, open: true, badge: 'Admin',
|
||||
children: [
|
||||
{ label: 'Users', to: '/dashboard/manager/users', icon: UsersRoundIcon },
|
||||
{ label: 'ApiKeys', to: '/dashboard/manager/keys', icon: KeyRoundIcon },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'submenu', label: 'Settings', icon: SettingsIcon,open: false,
|
||||
children: [
|
||||
{ label: 'Profile', to: '/dashboard/settings/profile', icon: UserIcon },
|
||||
]
|
||||
},
|
||||
]);
|
||||
menuItems = computed(() => generateMenuItemsFromRoutes(routes, userrole.value));
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user