Browse Source

feat: add breadcrumb navigation example

vince 8 months ago
parent
commit
9ec91ac16d
24 changed files with 188 additions and 75 deletions
  1. 6 1
      apps/web-antd/src/locales/langs/en-US.json
  2. 6 1
      apps/web-antd/src/locales/langs/zh-CN.json
  3. 57 1
      apps/web-antd/src/router/routes/modules/demos.ts
  4. 23 0
      apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue
  5. 27 0
      apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue
  6. 13 0
      apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue
  7. 8 0
      packages/@core/forward/helpers/src/generate-menus.test.ts
  8. 7 2
      packages/@core/forward/helpers/src/generate-menus.ts
  9. 0 31
      packages/@core/forward/helpers/src/generate-routes-frontend.test.ts
  10. 2 10
      packages/@core/forward/helpers/src/generate-routes-frontend.ts
  11. 1 1
      packages/@core/locales/src/langs/zh-CN.json
  12. 3 3
      packages/@core/shared/design/src/design-tokens/dark/index.css
  13. 5 1
      packages/@core/shared/typings/src/vue-router.d.ts
  14. 0 1
      packages/@core/ui-kit/layout-ui/package.json
  15. 3 3
      packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue
  16. 1 0
      packages/@core/ui-kit/menu-ui/src/components/menu-item.vue
  17. 4 4
      packages/@core/ui-kit/menu-ui/src/components/menu.vue
  18. 6 5
      packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue
  19. 1 0
      packages/@core/ui-kit/menu-ui/src/sub-menu.vue
  20. 2 2
      packages/@core/ui-kit/shadcn-ui/src/components/menu-badge/menu-badge.vue
  21. 7 2
      packages/effects/layouts/src/basic/menu/use-extra-menu.ts
  22. 5 3
      packages/effects/layouts/src/basic/menu/use-mixed-menu.ts
  23. 1 1
      packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue
  24. 0 3
      pnpm-lock.yaml

+ 6 - 1
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"
       }
     }
   }

+ 6 - 1
apps/web-antd/src/locales/langs/zh-CN.json

@@ -39,7 +39,12 @@
       "features": {
         "title": "功能",
         "hideChildrenInMenu": "隐藏子菜单",
-        "loginExpired": "登录过期"
+        "loginExpired": "登录过期",
+        "breadcrumbNavigation": "面包屑导航",
+        "breadcrumbLateral": "平级模式",
+        "breadcrumbLevel": "层级模式",
+        "breadcrumbLevelDetail": "层级模式详情",
+        "breadcrumbLateralDetail": "平级模式详情"
       }
     }
   }

+ 57 - 1
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'),
             },

+ 23 - 0
apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue

@@ -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>

+ 27 - 0
apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue

@@ -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>

+ 13 - 0
apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue

@@ -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>

+ 8 - 0
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: [],
       },
     ];

+ 7 - 2
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 };

+ 0 - 31
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',

+ 2 - 10
packages/@core/forward/helpers/src/generate-routes-frontend.ts

@@ -12,7 +12,7 @@ async function generateRoutesByFrontend(
 ): Promise<RouteRecordRaw[]> {
   // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
   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 };

+ 1 - 1
packages/@core/locales/src/langs/zh-CN.json

@@ -169,7 +169,7 @@
       "width": "宽度",
       "visible": "显示侧边栏",
       "collapsed": "折叠菜单",
-      "collapsedShowTitle": "显示菜单名"
+      "collapsedShowTitle": "折叠显示菜单名"
     },
     "tabbar": {
       "title": "标签栏",

+ 3 - 3
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%;

+ 5 - 1
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;
-
   /**
    * 标题名称
    */

+ 0 - 1
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"

+ 3 - 3
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"
     >
       <SidebarCollapseButton
         v-if="isSidebarMixed && expandOnHover"

+ 1 - 0
packages/@core/ui-kit/menu-ui/src/components/menu-item.vue

@@ -106,6 +106,7 @@ onBeforeUnmount(() => {
     <div v-show="!showTooltip" :class="[e('content')]">
       <VbenMenuBadge
         v-if="rootMenu.props.mode !== 'horizontal'"
+        class="right-2"
         v-bind="props"
       />
       <VbenIcon :class="nsMenu.e('icon')" :icon="icon" fallback />

+ 4 - 4
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));
     }

+ 6 - 5
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;
       }
     }
 

+ 1 - 0
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"
       />
     </template>
     <template #title>{{ menu.name }}</template>

+ 2 - 2
packages/@core/ui-kit/shadcn-ui/src/components/menu-badge/menu-badge.vue

@@ -43,13 +43,13 @@ const badgeStyle = computed(() => {
 });
 </script>
 <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" />
     <div
       v-else
       :class="badgeClass"
       :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 }}
     </div>

+ 7 - 2
packages/effects/layouts/src/basic/menu/use-extra-menu.ts

@@ -80,14 +80,19 @@ function useExtraMenu() {
 
   watch(
     () => route.path,
-    () => {
+    (path) => {
+      const currentPath = path;
+      // if (preferences.sidebar.expandOnHover) {
+      //   return;
+      // }
       const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
         menus.value,
-        route.path,
+        currentPath,
       );
       extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
       extraMenus.value = rootMenu?.children ?? [];
     },
+    { immediate: true },
   );
 
   return {

+ 5 - 3
packages/effects/layouts/src/basic/menu/use-mixed-menu.ts

@@ -57,7 +57,7 @@ function useMixedMenu() {
    * 侧边菜单激活路径
    */
   const sidebarActive = computed(() => {
-    return route.path;
+    return route?.meta?.activePath ?? route.path;
   });
 
   /**
@@ -104,9 +104,11 @@ function useMixedMenu() {
 
   watch(
     () => route.path,
-    (path: string) => {
-      calcSideMenus(path);
+    (path) => {
+      const currentPath = (route?.meta?.activePath as string) ?? path;
+      calcSideMenus(currentPath);
     },
+    { immediate: true },
   );
 
   // 初始化计算侧边菜单

+ 1 - 1
packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue

@@ -27,7 +27,7 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
   </SwitchItem>
   <SwitchItem
     v-model="sidebarCollapsedShowTitle"
-    :disabled="!sidebarEnable || disabled"
+    :disabled="!sidebarEnable || disabled || !sidebarCollapsed"
   >
     {{ $t('preferences.sidebar.collapsedShowTitle') }}
   </SwitchItem>

+ 0 - 3
pnpm-lock.yaml

@@ -741,9 +741,6 @@ importers:
       '@vben-core/shadcn-ui':
         specifier: workspace:*
         version: link:../shadcn-ui
-      '@vben-core/toolkit':
-        specifier: workspace:*
-        version: link:../../shared/toolkit
       '@vben-core/typings':
         specifier: workspace:*
         version: link:../../shared/typings