feat: add breadcrumb navigation example
parent
0c245665a9
commit
9ec91ac16d
|
@ -37,7 +37,12 @@
|
||||||
"features": {
|
"features": {
|
||||||
"title": "Features",
|
"title": "Features",
|
||||||
"hideChildrenInMenu": "Hide Menu Children",
|
"hideChildrenInMenu": "Hide Menu Children",
|
||||||
"loginExpired": "Login Expired"
|
"loginExpired": "Login Expired",
|
||||||
|
"breadcrumbNavigation": "Breadcrumb Navigation",
|
||||||
|
"breadcrumbLateral": "Lateral Mode",
|
||||||
|
"breadcrumbLateralDetail": "Lateral Mode Detail",
|
||||||
|
"breadcrumbLevel": "Level Mode",
|
||||||
|
"breadcrumbLevelDetail": "Level Mode Detail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,12 @@
|
||||||
"features": {
|
"features": {
|
||||||
"title": "功能",
|
"title": "功能",
|
||||||
"hideChildrenInMenu": "隐藏子菜单",
|
"hideChildrenInMenu": "隐藏子菜单",
|
||||||
"loginExpired": "登录过期"
|
"loginExpired": "登录过期",
|
||||||
|
"breadcrumbNavigation": "面包屑导航",
|
||||||
|
"breadcrumbLateral": "平级模式",
|
||||||
|
"breadcrumbLevel": "层级模式",
|
||||||
|
"breadcrumbLevelDetail": "层级模式详情",
|
||||||
|
"breadcrumbLateralDetail": "平级模式详情"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,60 @@ const routes: RouteRecordRaw[] = [
|
||||||
title: $t('page.demos.features.loginExpired'),
|
title: $t('page.demos.features.loginExpired'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'BreadcrumbDemos',
|
||||||
|
path: 'breadcrumb',
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:navigation',
|
||||||
|
title: $t('page.demos.features.breadcrumbNavigation'),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'BreadcrumbLateral',
|
||||||
|
path: 'lateral',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/demos/features/breadcrumb/lateral.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:navigation',
|
||||||
|
title: $t('page.demos.features.breadcrumbLateral'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BreadcrumbLateralDetail',
|
||||||
|
path: 'lateral-detail',
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
'#/views/demos/features/breadcrumb/lateral-detail.vue'
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
activePath: '/demos/features/breadcrumb/lateral',
|
||||||
|
hideInMenu: true,
|
||||||
|
title: $t('page.demos.features.breadcrumbLateralDetail'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BreadcrumbLevel',
|
||||||
|
path: 'level',
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:navigation',
|
||||||
|
title: $t('page.demos.features.breadcrumbLevel'),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'BreadcrumbLevelDetail',
|
||||||
|
path: 'detail',
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
'#/views/demos/features/breadcrumb/level-detail.vue'
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.demos.features.breadcrumbLevelDetail'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -179,6 +233,8 @@ const routes: RouteRecordRaw[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
badgeVariants: 'destructive',
|
||||||
icon: 'lucide:circle-dot',
|
icon: 'lucide:circle-dot',
|
||||||
title: $t('page.demos.badge.title'),
|
title: $t('page.demos.badge.title'),
|
||||||
},
|
},
|
||||||
|
@ -201,7 +257,7 @@ const routes: RouteRecordRaw[] = [
|
||||||
component: () => import('#/views/demos/badge/index.vue'),
|
component: () => import('#/views/demos/badge/index.vue'),
|
||||||
path: 'text',
|
path: 'text',
|
||||||
meta: {
|
meta: {
|
||||||
badge: 'New',
|
badge: '10',
|
||||||
icon: 'lucide:square-dot',
|
icon: 'lucide:square-dot',
|
||||||
title: $t('page.demos.badge.text'),
|
title: $t('page.demos.badge.text'),
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BreadcrumbLateralDetail' });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback
|
||||||
|
description="面包屑导航-平级模式-详情页"
|
||||||
|
status="coming-soon"
|
||||||
|
title="注意观察面包屑导航变化"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<Button @click="router.go(-1)">返回</Button>
|
||||||
|
</template>
|
||||||
|
</Fallback>
|
||||||
|
</template>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BreadcrumbLateral' });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function details() {
|
||||||
|
router.push({ name: 'BreadcrumbLateralDetail' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback
|
||||||
|
description="点击查看详情,并观察面包屑导航变化"
|
||||||
|
status="coming-soon"
|
||||||
|
title="面包屑导航-平级模式"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<Button type="primary" @click="details">点击查看详情</Button>
|
||||||
|
</template>
|
||||||
|
</Fallback>
|
||||||
|
</template>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BreadcrumbLevelDetail' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback
|
||||||
|
description="面包屑导航-层级模式-详情页"
|
||||||
|
status="coming-soon"
|
||||||
|
title="注意观察面包屑导航变化"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -53,6 +53,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/home',
|
path: '/home',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -65,6 +66,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/about',
|
path: '/about',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -94,6 +96,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -120,6 +123,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/users/:userId',
|
path: '/users/:userId',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -155,6 +159,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/old-path',
|
path: '/old-path',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -167,6 +172,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/new-path',
|
path: '/new-path',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -203,6 +209,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/about',
|
path: '/about',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -215,6 +222,7 @@ describe('generateMenus', () => {
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
parents: undefined,
|
parents: undefined,
|
||||||
path: '/',
|
path: '/',
|
||||||
|
show: true,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
||||||
import type { RouteRecordRaw, Router } from 'vue-router';
|
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||||
|
|
||||||
import { mapTree } from '@vben-core/toolkit';
|
import { filterTree, mapTree } from '@vben-core/toolkit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 routes 生成菜单列表
|
* 根据 routes 生成菜单列表
|
||||||
|
@ -61,13 +61,18 @@ async function generateMenus(
|
||||||
parent: route.parent,
|
parent: route.parent,
|
||||||
parents: route.parents,
|
parents: route.parents,
|
||||||
path: resultPath as string,
|
path: resultPath as string,
|
||||||
|
show: !route?.meta?.hideInMenu,
|
||||||
children: resultChildren || [],
|
children: resultChildren || [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 对菜单进行排序
|
// 对菜单进行排序
|
||||||
menus = menus.sort((a, b) => (a.order || 999) - (b.order || 999));
|
menus = menus.sort((a, b) => (a.order || 999) - (b.order || 999));
|
||||||
return menus;
|
|
||||||
|
const finalMenus = filterTree(menus, (menu) => {
|
||||||
|
return !!menu.show;
|
||||||
|
});
|
||||||
|
return finalMenus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generateMenus };
|
export { generateMenus };
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
generateRoutesByFrontend,
|
generateRoutesByFrontend,
|
||||||
hasAuthority,
|
hasAuthority,
|
||||||
hasVisible,
|
|
||||||
} from './generate-routes-frontend';
|
} from './generate-routes-frontend';
|
||||||
|
|
||||||
// Mock 路由数据
|
// Mock 路由数据
|
||||||
|
@ -51,37 +50,7 @@ describe('hasAuthority', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hasVisible', () => {
|
|
||||||
it('should return true if hideInMenu is not set or false', () => {
|
|
||||||
expect(hasVisible(mockRoutes[0])).toBe(true);
|
|
||||||
expect(hasVisible(mockRoutes[2])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if hideInMenu is true', () => {
|
|
||||||
expect(hasVisible(mockRoutes[0].children?.[1])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('generateRoutesByFrontend', () => {
|
describe('generateRoutesByFrontend', () => {
|
||||||
it('should filter routes based on authority and visibility', async () => {
|
|
||||||
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
|
|
||||||
'user',
|
|
||||||
]);
|
|
||||||
// The user should have access to /dashboard/stats, but it should be filtered out because it's not visible
|
|
||||||
expect(generatedRoutes).toEqual([
|
|
||||||
{
|
|
||||||
meta: { authority: ['admin', 'user'], hideInMenu: false },
|
|
||||||
path: '/dashboard',
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
// Note: We expect /settings to be filtered out because the user does not have 'admin' authority
|
|
||||||
{
|
|
||||||
meta: { hideInMenu: false },
|
|
||||||
path: '/profile',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle routes without children', async () => {
|
it('should handle routes without children', async () => {
|
||||||
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
|
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
|
||||||
'user',
|
'user',
|
||||||
|
|
|
@ -12,7 +12,7 @@ async function generateRoutesByFrontend(
|
||||||
): Promise<RouteRecordRaw[]> {
|
): Promise<RouteRecordRaw[]> {
|
||||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||||
const finalRoutes = filterTree(routes, (route) => {
|
const finalRoutes = filterTree(routes, (route) => {
|
||||||
return hasVisible(route) && hasAuthority(route, roles);
|
return hasAuthority(route, roles);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!forbiddenComponent) {
|
if (!forbiddenComponent) {
|
||||||
|
@ -43,14 +43,6 @@ function hasAuthority(route: RouteRecordRaw, access: string[]) {
|
||||||
return canAccess || (!canAccess && menuHasVisibleWithForbidden(route));
|
return canAccess || (!canAccess && menuHasVisibleWithForbidden(route));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断路由是否需要在菜单中显示
|
|
||||||
* @param route
|
|
||||||
*/
|
|
||||||
function hasVisible(route?: RouteRecordRaw) {
|
|
||||||
return !route?.meta?.hideInMenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断路由是否在菜单中显示,但是访问会被重定向到403
|
* 判断路由是否在菜单中显示,但是访问会被重定向到403
|
||||||
* @param route
|
* @param route
|
||||||
|
@ -63,4 +55,4 @@ function menuHasVisibleWithForbidden(route: RouteRecordRaw) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generateRoutesByFrontend, hasAuthority, hasVisible };
|
export { generateRoutesByFrontend, hasAuthority };
|
||||||
|
|
|
@ -169,7 +169,7 @@
|
||||||
"width": "宽度",
|
"width": "宽度",
|
||||||
"visible": "显示侧边栏",
|
"visible": "显示侧边栏",
|
||||||
"collapsed": "折叠菜单",
|
"collapsed": "折叠菜单",
|
||||||
"collapsedShowTitle": "显示菜单名"
|
"collapsedShowTitle": "折叠显示菜单名"
|
||||||
},
|
},
|
||||||
"tabbar": {
|
"tabbar": {
|
||||||
"title": "标签栏",
|
"title": "标签栏",
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
--background: 20 14.3% 4.1%;
|
--background: 20 14.3% 4.1%;
|
||||||
--background-deep: var(--background);
|
--background-deep: var(--background);
|
||||||
--foreground: 0 0% 95%;
|
--foreground: 0 0% 95%;
|
||||||
--card: 24 9.8% 10%;
|
--card: 0 0% 9%;
|
||||||
--card-foreground: 0 0% 95%;
|
--card-foreground: 0 0% 95%;
|
||||||
--popover: 0 0% 9%;
|
--popover: 0 0% 9%;
|
||||||
--popover-foreground: 0 0% 95%;
|
--popover-foreground: 0 0% 95%;
|
||||||
|
@ -222,7 +222,7 @@
|
||||||
--background: 20 14.3% 4.1%;
|
--background: 20 14.3% 4.1%;
|
||||||
--background-deep: var(--background);
|
--background-deep: var(--background);
|
||||||
--foreground: 0 0% 95%;
|
--foreground: 0 0% 95%;
|
||||||
--card: 24 9.8% 10%;
|
--card: 24 9.8% 6%;
|
||||||
--card-foreground: 0 0% 95%;
|
--card-foreground: 0 0% 95%;
|
||||||
--popover: 0 0% 9%;
|
--popover: 0 0% 9%;
|
||||||
--popover-foreground: 0 0% 95%;
|
--popover-foreground: 0 0% 95%;
|
||||||
|
@ -247,7 +247,7 @@
|
||||||
--background: 20 14.3% 4.1%;
|
--background: 20 14.3% 4.1%;
|
||||||
--background-deep: var(--background);
|
--background-deep: var(--background);
|
||||||
--foreground: 0 0% 95%;
|
--foreground: 0 0% 95%;
|
||||||
--card: 24 9.8% 10%;
|
--card: 24 9.8% 6%;
|
||||||
--card-foreground: 0 0% 95%;
|
--card-foreground: 0 0% 95%;
|
||||||
--popover: 0 0% 9%;
|
--popover: 0 0% 9%;
|
||||||
--popover-foreground: 0 0% 95%;
|
--popover-foreground: 0 0% 95%;
|
||||||
|
|
|
@ -3,6 +3,11 @@ import type { RouteRecordRaw, Router } from 'vue-router';
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
|
/**
|
||||||
|
* 当前激活的菜单,有时候不想激活现有菜单,需要激活父级菜单时使用
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
activePath?: string;
|
||||||
/**
|
/**
|
||||||
* 是否固定标签页
|
* 是否固定标签页
|
||||||
* @default false
|
* @default false
|
||||||
|
@ -88,7 +93,6 @@ interface RouteMeta {
|
||||||
* 用于路由->菜单排序
|
* 用于路由->菜单排序
|
||||||
*/
|
*/
|
||||||
order?: number;
|
order?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标题名称
|
* 标题名称
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
"@vben-core/hooks": "workspace:*",
|
"@vben-core/hooks": "workspace:*",
|
||||||
"@vben-core/icons": "workspace:*",
|
"@vben-core/icons": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/toolkit": "workspace:*",
|
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"vue": "^3.4.32"
|
"vue": "^3.4.32"
|
||||||
|
|
|
@ -253,7 +253,7 @@ function handleMouseleave() {
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:style="style"
|
:style="style"
|
||||||
class="border-border fixed left-0 top-0 h-full border-r transition-all duration-150"
|
class="fixed left-0 top-0 h-full transition-all duration-150"
|
||||||
@mouseenter="handleMouseenter"
|
@mouseenter="handleMouseenter"
|
||||||
@mouseleave="handleMouseleave"
|
@mouseleave="handleMouseleave"
|
||||||
>
|
>
|
||||||
|
@ -277,10 +277,10 @@ function handleMouseleave() {
|
||||||
v-if="isSidebarMixed"
|
v-if="isSidebarMixed"
|
||||||
ref="asideRef"
|
ref="asideRef"
|
||||||
:class="{
|
:class="{
|
||||||
'border-r': extraVisible,
|
'border-l': extraVisible,
|
||||||
}"
|
}"
|
||||||
:style="extraStyle"
|
:style="extraStyle"
|
||||||
class="border-border bg-sidebar fixed top-0 h-full overflow-hidden transition-all duration-200"
|
class="border-border bg-sidebar fixed top-0 h-full overflow-hidden border-r transition-all duration-200"
|
||||||
>
|
>
|
||||||
<SidebarCollapseButton
|
<SidebarCollapseButton
|
||||||
v-if="isSidebarMixed && expandOnHover"
|
v-if="isSidebarMixed && expandOnHover"
|
||||||
|
|
|
@ -106,6 +106,7 @@ onBeforeUnmount(() => {
|
||||||
<div v-show="!showTooltip" :class="[e('content')]">
|
<div v-show="!showTooltip" :class="[e('content')]">
|
||||||
<VbenMenuBadge
|
<VbenMenuBadge
|
||||||
v-if="rootMenu.props.mode !== 'horizontal'"
|
v-if="rootMenu.props.mode !== 'horizontal'"
|
||||||
|
class="right-2"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
/>
|
/>
|
||||||
<VbenIcon :class="nsMenu.e('icon')" :icon="icon" fallback />
|
<VbenIcon :class="nsMenu.e('icon')" :icon="icon" fallback />
|
||||||
|
|
|
@ -507,12 +507,12 @@ $namespace: vben;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-light {
|
&.is-light {
|
||||||
--menu-item-active-color: hsl(var(--primary));
|
--menu-item-active-color: hsl(var(--primary-foreground));
|
||||||
--menu-item-active-background-color: hsl(var(--primary) / 15%);
|
--menu-item-active-background-color: hsl(var(--primary));
|
||||||
--menu-item-hover-background-color: hsl(var(--accent));
|
--menu-item-hover-background-color: hsl(var(--accent));
|
||||||
--menu-item-hover-color: hsl(var(--primary));
|
--menu-item-hover-color: hsl(var(--primary));
|
||||||
--menu-submenu-active-color: hsl(var(--primary));
|
--menu-submenu-active-color: hsl(var(--primary-foreground));
|
||||||
--menu-submenu-active-background-color: hsl(var(--primary) / 15%);
|
--menu-submenu-active-background-color: hsl(var(--primary));
|
||||||
--menu-submenu-hover-color: hsl(var(--primary));
|
--menu-submenu-hover-color: hsl(var(--primary));
|
||||||
--menu-submenu-hover-background-color: hsl(var(--accent));
|
--menu-submenu-hover-background-color: hsl(var(--accent));
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,16 +69,17 @@ $namespace: vben;
|
||||||
|
|
||||||
&.is-dark {
|
&.is-dark {
|
||||||
.#{$namespace}-normal-menu__item {
|
.#{$namespace}-normal-menu__item {
|
||||||
color: hsl(var(--foreground) / 80%);
|
@apply text-foreground/80;
|
||||||
|
// color: hsl(var(--foreground) / 80%);
|
||||||
|
|
||||||
&:not(.is-active):hover {
|
&:not(.is-active):hover {
|
||||||
color: hsl(var(--primary-foreground));
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
.#{$namespace}-normal-menu__name,
|
.#{$namespace}-normal-menu__name,
|
||||||
.#{$namespace}-normal-menu__icon {
|
.#{$namespace}-normal-menu__icon {
|
||||||
color: hsl(var(--primary-foreground));
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,11 +118,11 @@ $namespace: vben;
|
||||||
border-color 0.15s ease;
|
border-color 0.15s ease;
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
@apply text-primary bg-primary/15 dark:bg-accent;
|
@apply text-primary bg-primary dark:bg-accent;
|
||||||
|
|
||||||
.#{$namespace}-normal-menu__name,
|
.#{$namespace}-normal-menu__name,
|
||||||
.#{$namespace}-normal-menu__icon {
|
.#{$namespace}-normal-menu__icon {
|
||||||
@apply text-primary font-semibold;
|
@apply text-primary-foreground font-semibold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ const hasChildren = computed(() => {
|
||||||
:badge="menu.badge"
|
:badge="menu.badge"
|
||||||
:badge-type="menu.badgeType"
|
:badge-type="menu.badgeType"
|
||||||
:badge-variants="menu.badgeVariants"
|
:badge-variants="menu.badgeVariants"
|
||||||
|
class="right-6"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #title>{{ menu.name }}</template>
|
<template #title>{{ menu.name }}</template>
|
||||||
|
|
|
@ -43,13 +43,13 @@ const badgeStyle = computed(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<span v-if="isDot || badge" :class="$attrs.class" class="absolute right-6">
|
<span v-if="isDot || badge" :class="$attrs.class" class="absolute">
|
||||||
<BadgeDot v-if="isDot" :dot-class="badgeClass" :dot-style="badgeStyle" />
|
<BadgeDot v-if="isDot" :dot-class="badgeClass" :dot-style="badgeStyle" />
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
:class="badgeClass"
|
:class="badgeClass"
|
||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
class="text-primary-foreground rounded-xl px-1.5 py-0.5 text-xs"
|
class="text-primary-foreground flex-center rounded-xl px-1.5 py-0.5 text-[10px]"
|
||||||
>
|
>
|
||||||
{{ badge }}
|
{{ badge }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -80,14 +80,19 @@ function useExtraMenu() {
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
() => {
|
(path) => {
|
||||||
|
const currentPath = path;
|
||||||
|
// if (preferences.sidebar.expandOnHover) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
||||||
menus.value,
|
menus.value,
|
||||||
route.path,
|
currentPath,
|
||||||
);
|
);
|
||||||
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
|
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
|
||||||
extraMenus.value = rootMenu?.children ?? [];
|
extraMenus.value = rootMenu?.children ?? [];
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -57,7 +57,7 @@ function useMixedMenu() {
|
||||||
* 侧边菜单激活路径
|
* 侧边菜单激活路径
|
||||||
*/
|
*/
|
||||||
const sidebarActive = computed(() => {
|
const sidebarActive = computed(() => {
|
||||||
return route.path;
|
return route?.meta?.activePath ?? route.path;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,9 +104,11 @@ function useMixedMenu() {
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
(path: string) => {
|
(path) => {
|
||||||
calcSideMenus(path);
|
const currentPath = (route?.meta?.activePath as string) ?? path;
|
||||||
|
calcSideMenus(currentPath);
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化计算侧边菜单
|
// 初始化计算侧边菜单
|
||||||
|
|
|
@ -27,7 +27,7 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
<SwitchItem
|
<SwitchItem
|
||||||
v-model="sidebarCollapsedShowTitle"
|
v-model="sidebarCollapsedShowTitle"
|
||||||
:disabled="!sidebarEnable || disabled"
|
:disabled="!sidebarEnable || disabled || !sidebarCollapsed"
|
||||||
>
|
>
|
||||||
{{ $t('preferences.sidebar.collapsedShowTitle') }}
|
{{ $t('preferences.sidebar.collapsedShowTitle') }}
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
|
|
|
@ -741,9 +741,6 @@ importers:
|
||||||
'@vben-core/shadcn-ui':
|
'@vben-core/shadcn-ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../shadcn-ui
|
version: link:../shadcn-ui
|
||||||
'@vben-core/toolkit':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../shared/toolkit
|
|
||||||
'@vben-core/typings':
|
'@vben-core/typings':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../shared/typings
|
version: link:../../shared/typings
|
||||||
|
|
Loading…
Reference in New Issue