From 9ec91ac16d02a73dddd41cdabf8888c9625e49fb Mon Sep 17 00:00:00 2001 From: vince Date: Fri, 19 Jul 2024 01:26:13 +0800 Subject: [PATCH] feat: add breadcrumb navigation example --- apps/web-antd/src/locales/langs/en-US.json | 7 ++- apps/web-antd/src/locales/langs/zh-CN.json | 7 ++- .../src/router/routes/modules/demos.ts | 58 ++++++++++++++++++- .../features/breadcrumb/lateral-detail.vue | 23 ++++++++ .../demos/features/breadcrumb/lateral.vue | 27 +++++++++ .../features/breadcrumb/level-detail.vue | 13 +++++ .../helpers/src/generate-menus.test.ts | 8 +++ .../forward/helpers/src/generate-menus.ts | 9 ++- .../src/generate-routes-frontend.test.ts | 31 ---------- .../helpers/src/generate-routes-frontend.ts | 12 +--- packages/@core/locales/src/langs/zh-CN.json | 2 +- .../design/src/design-tokens/dark/index.css | 6 +- .../@core/shared/typings/src/vue-router.d.ts | 6 +- packages/@core/ui-kit/layout-ui/package.json | 1 - .../src/components/layout-sidebar.vue | 6 +- .../menu-ui/src/components/menu-item.vue | 1 + .../ui-kit/menu-ui/src/components/menu.vue | 8 +-- .../components/normal-menu/normal-menu.vue | 11 ++-- .../@core/ui-kit/menu-ui/src/sub-menu.vue | 1 + .../src/components/menu-badge/menu-badge.vue | 4 +- .../layouts/src/basic/menu/use-extra-menu.ts | 9 ++- .../layouts/src/basic/menu/use-mixed-menu.ts | 8 ++- .../preferences/blocks/layout/sidebar.vue | 2 +- pnpm-lock.yaml | 3 - 24 files changed, 188 insertions(+), 75 deletions(-) create mode 100644 apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue create mode 100644 apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue create mode 100644 apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue diff --git a/apps/web-antd/src/locales/langs/en-US.json b/apps/web-antd/src/locales/langs/en-US.json index 4bac56fd..8be104a8 100644 --- a/apps/web-antd/src/locales/langs/en-US.json +++ b/apps/web-antd/src/locales/langs/en-US.json @@ -37,7 +37,12 @@ "features": { "title": "Features", "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" } } } diff --git a/apps/web-antd/src/locales/langs/zh-CN.json b/apps/web-antd/src/locales/langs/zh-CN.json index b91679d1..376292d0 100644 --- a/apps/web-antd/src/locales/langs/zh-CN.json +++ b/apps/web-antd/src/locales/langs/zh-CN.json @@ -39,7 +39,12 @@ "features": { "title": "功能", "hideChildrenInMenu": "隐藏子菜单", - "loginExpired": "登录过期" + "loginExpired": "登录过期", + "breadcrumbNavigation": "面包屑导航", + "breadcrumbLateral": "平级模式", + "breadcrumbLevel": "层级模式", + "breadcrumbLevelDetail": "层级模式详情", + "breadcrumbLateralDetail": "平级模式详情" } } } diff --git a/apps/web-antd/src/router/routes/modules/demos.ts b/apps/web-antd/src/router/routes/modules/demos.ts index 66ad37d9..e8bb7736 100644 --- a/apps/web-antd/src/router/routes/modules/demos.ts +++ b/apps/web-antd/src/router/routes/modules/demos.ts @@ -127,6 +127,60 @@ const routes: RouteRecordRaw[] = [ 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: { + badgeType: 'dot', + badgeVariants: 'destructive', icon: 'lucide:circle-dot', title: $t('page.demos.badge.title'), }, @@ -201,7 +257,7 @@ const routes: RouteRecordRaw[] = [ component: () => import('#/views/demos/badge/index.vue'), path: 'text', meta: { - badge: 'New', + badge: '10', icon: 'lucide:square-dot', title: $t('page.demos.badge.text'), }, diff --git a/apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue b/apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue new file mode 100644 index 00000000..47c3e0d0 --- /dev/null +++ b/apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue b/apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue new file mode 100644 index 00000000..6d487431 --- /dev/null +++ b/apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue b/apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue new file mode 100644 index 00000000..bcba562b --- /dev/null +++ b/apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/@core/forward/helpers/src/generate-menus.test.ts b/packages/@core/forward/helpers/src/generate-menus.test.ts index d550fe1e..7a1eeb73 100644 --- a/packages/@core/forward/helpers/src/generate-menus.test.ts +++ b/packages/@core/forward/helpers/src/generate-menus.test.ts @@ -53,6 +53,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/home', + show: true, children: [], }, { @@ -65,6 +66,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/about', + show: true, children: [], }, ]; @@ -94,6 +96,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/profile', + show: true, children: [], }, ]); @@ -120,6 +123,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/users/:userId', + show: true, children: [], }, ]); @@ -155,6 +159,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/old-path', + show: true, children: [], }, { @@ -167,6 +172,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/new-path', + show: true, children: [], }, ]); @@ -203,6 +209,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/about', + show: true, children: [], }, { @@ -215,6 +222,7 @@ describe('generateMenus', () => { parent: undefined, parents: undefined, path: '/', + show: true, children: [], }, ]; diff --git a/packages/@core/forward/helpers/src/generate-menus.ts b/packages/@core/forward/helpers/src/generate-menus.ts index 7fb3aeac..3cb7cfed 100644 --- a/packages/@core/forward/helpers/src/generate-menus.ts +++ b/packages/@core/forward/helpers/src/generate-menus.ts @@ -1,7 +1,7 @@ import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings'; import type { RouteRecordRaw, Router } from 'vue-router'; -import { mapTree } from '@vben-core/toolkit'; +import { filterTree, mapTree } from '@vben-core/toolkit'; /** * 根据 routes 生成菜单列表 @@ -61,13 +61,18 @@ async function generateMenus( parent: route.parent, parents: route.parents, path: resultPath as string, + show: !route?.meta?.hideInMenu, children: resultChildren || [], }; }); // 对菜单进行排序 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 }; diff --git a/packages/@core/forward/helpers/src/generate-routes-frontend.test.ts b/packages/@core/forward/helpers/src/generate-routes-frontend.test.ts index 5e1f56d4..1ca57d39 100644 --- a/packages/@core/forward/helpers/src/generate-routes-frontend.test.ts +++ b/packages/@core/forward/helpers/src/generate-routes-frontend.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from 'vitest'; import { generateRoutesByFrontend, hasAuthority, - hasVisible, } from './generate-routes-frontend'; // 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', () => { - 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 () => { const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [ 'user', diff --git a/packages/@core/forward/helpers/src/generate-routes-frontend.ts b/packages/@core/forward/helpers/src/generate-routes-frontend.ts index 5c471f2a..fed26315 100644 --- a/packages/@core/forward/helpers/src/generate-routes-frontend.ts +++ b/packages/@core/forward/helpers/src/generate-routes-frontend.ts @@ -12,7 +12,7 @@ async function generateRoutesByFrontend( ): Promise { // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限 const finalRoutes = filterTree(routes, (route) => { - return hasVisible(route) && hasAuthority(route, roles); + return hasAuthority(route, roles); }); if (!forbiddenComponent) { @@ -43,14 +43,6 @@ function hasAuthority(route: RouteRecordRaw, access: string[]) { return canAccess || (!canAccess && menuHasVisibleWithForbidden(route)); } -/** - * 判断路由是否需要在菜单中显示 - * @param route - */ -function hasVisible(route?: RouteRecordRaw) { - return !route?.meta?.hideInMenu; -} - /** * 判断路由是否在菜单中显示,但是访问会被重定向到403 * @param route @@ -63,4 +55,4 @@ function menuHasVisibleWithForbidden(route: RouteRecordRaw) { ); } -export { generateRoutesByFrontend, hasAuthority, hasVisible }; +export { generateRoutesByFrontend, hasAuthority }; diff --git a/packages/@core/locales/src/langs/zh-CN.json b/packages/@core/locales/src/langs/zh-CN.json index c99f8a9c..bd5c58c7 100644 --- a/packages/@core/locales/src/langs/zh-CN.json +++ b/packages/@core/locales/src/langs/zh-CN.json @@ -169,7 +169,7 @@ "width": "宽度", "visible": "显示侧边栏", "collapsed": "折叠菜单", - "collapsedShowTitle": "显示菜单名" + "collapsedShowTitle": "折叠显示菜单名" }, "tabbar": { "title": "标签栏", diff --git a/packages/@core/shared/design/src/design-tokens/dark/index.css b/packages/@core/shared/design/src/design-tokens/dark/index.css index 3283e234..66da121f 100644 --- a/packages/@core/shared/design/src/design-tokens/dark/index.css +++ b/packages/@core/shared/design/src/design-tokens/dark/index.css @@ -122,7 +122,7 @@ --background: 20 14.3% 4.1%; --background-deep: var(--background); --foreground: 0 0% 95%; - --card: 24 9.8% 10%; + --card: 0 0% 9%; --card-foreground: 0 0% 95%; --popover: 0 0% 9%; --popover-foreground: 0 0% 95%; @@ -222,7 +222,7 @@ --background: 20 14.3% 4.1%; --background-deep: var(--background); --foreground: 0 0% 95%; - --card: 24 9.8% 10%; + --card: 24 9.8% 6%; --card-foreground: 0 0% 95%; --popover: 0 0% 9%; --popover-foreground: 0 0% 95%; @@ -247,7 +247,7 @@ --background: 20 14.3% 4.1%; --background-deep: var(--background); --foreground: 0 0% 95%; - --card: 24 9.8% 10%; + --card: 24 9.8% 6%; --card-foreground: 0 0% 95%; --popover: 0 0% 9%; --popover-foreground: 0 0% 95%; diff --git a/packages/@core/shared/typings/src/vue-router.d.ts b/packages/@core/shared/typings/src/vue-router.d.ts index 1eb808d5..60d19bb6 100644 --- a/packages/@core/shared/typings/src/vue-router.d.ts +++ b/packages/@core/shared/typings/src/vue-router.d.ts @@ -3,6 +3,11 @@ import type { RouteRecordRaw, Router } from 'vue-router'; import type { Component } from 'vue'; interface RouteMeta { + /** + * 当前激活的菜单,有时候不想激活现有菜单,需要激活父级菜单时使用 + * @default false + */ + activePath?: string; /** * 是否固定标签页 * @default false @@ -88,7 +93,6 @@ interface RouteMeta { * 用于路由->菜单排序 */ order?: number; - /** * 标题名称 */ diff --git a/packages/@core/ui-kit/layout-ui/package.json b/packages/@core/ui-kit/layout-ui/package.json index bfd0e3c4..0e81bf21 100644 --- a/packages/@core/ui-kit/layout-ui/package.json +++ b/packages/@core/ui-kit/layout-ui/package.json @@ -40,7 +40,6 @@ "@vben-core/hooks": "workspace:*", "@vben-core/icons": "workspace:*", "@vben-core/shadcn-ui": "workspace:*", - "@vben-core/toolkit": "workspace:*", "@vben-core/typings": "workspace:*", "@vueuse/core": "^10.11.0", "vue": "^3.4.32" diff --git a/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue b/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue index 6547c2e9..92ad0ca4 100644 --- a/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue +++ b/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue @@ -253,7 +253,7 @@ function handleMouseleave() { }, ]" :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" @mouseleave="handleMouseleave" > @@ -277,10 +277,10 @@ function handleMouseleave() { v-if="isSidebarMixed" ref="asideRef" :class="{ - 'border-r': extraVisible, + 'border-l': extraVisible, }" :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" > {
diff --git a/packages/@core/ui-kit/menu-ui/src/components/menu.vue b/packages/@core/ui-kit/menu-ui/src/components/menu.vue index 92ee1198..57670997 100644 --- a/packages/@core/ui-kit/menu-ui/src/components/menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/components/menu.vue @@ -507,12 +507,12 @@ $namespace: vben; } &.is-light { - --menu-item-active-color: hsl(var(--primary)); - --menu-item-active-background-color: hsl(var(--primary) / 15%); + --menu-item-active-color: hsl(var(--primary-foreground)); + --menu-item-active-background-color: hsl(var(--primary)); --menu-item-hover-background-color: hsl(var(--accent)); --menu-item-hover-color: hsl(var(--primary)); - --menu-submenu-active-color: hsl(var(--primary)); - --menu-submenu-active-background-color: hsl(var(--primary) / 15%); + --menu-submenu-active-color: hsl(var(--primary-foreground)); + --menu-submenu-active-background-color: hsl(var(--primary)); --menu-submenu-hover-color: hsl(var(--primary)); --menu-submenu-hover-background-color: hsl(var(--accent)); } diff --git a/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue b/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue index 40233ea8..8efb018d 100644 --- a/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue @@ -69,16 +69,17 @@ $namespace: vben; &.is-dark { .#{$namespace}-normal-menu__item { - color: hsl(var(--foreground) / 80%); + @apply text-foreground/80; + // color: hsl(var(--foreground) / 80%); &:not(.is-active):hover { - color: hsl(var(--primary-foreground)); + @apply text-foreground; } &.is-active { .#{$namespace}-normal-menu__name, .#{$namespace}-normal-menu__icon { - color: hsl(var(--primary-foreground)); + @apply text-foreground; } } } @@ -117,11 +118,11 @@ $namespace: vben; border-color 0.15s ease; &.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__icon { - @apply text-primary font-semibold; + @apply text-primary-foreground font-semibold; } } diff --git a/packages/@core/ui-kit/menu-ui/src/sub-menu.vue b/packages/@core/ui-kit/menu-ui/src/sub-menu.vue index 99719ba1..f65320d1 100644 --- a/packages/@core/ui-kit/menu-ui/src/sub-menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/sub-menu.vue @@ -56,6 +56,7 @@ const hasChildren = computed(() => { :badge="menu.badge" :badge-type="menu.badgeType" :badge-variants="menu.badgeVariants" + class="right-6" /> diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/menu-badge/menu-badge.vue b/packages/@core/ui-kit/shadcn-ui/src/components/menu-badge/menu-badge.vue index 3ee22041..db07c197 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/menu-badge/menu-badge.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/menu-badge/menu-badge.vue @@ -43,13 +43,13 @@ const badgeStyle = computed(() => { });