Browse Source

feat(breadcrumb): add breadcrumb demo #143

vben 4 years ago
parent
commit
819bcbe526

+ 3 - 0
CHANGELOG.zh_CN.md

@@ -6,6 +6,8 @@
 - 新增左侧菜单混合模式
 - 新增 markdown 嵌入表单内示例
 - 新增主框架外页面示例
+- `route.meta` 新增`currentActiveMenu`,`hideTab`,`hideMenu`参数 用于控制详情页面包屑级菜单显示隐藏。
+- 新增面包屑导航示例
 
 ### 🐛 Bug Fixes
 
@@ -14,6 +16,7 @@
 - 修复图表库切换页面导致宽高计算错误
 - 修复多语言配置 `Locale.show`导致配置不生效
 - 修复路由类型错误
+- 修复菜单分割时权限失效问题
 
 ## 2.0.0-rc.14 (2020-12-15)
 

+ 10 - 7
src/components/Menu/src/BasicMenu.vue

@@ -118,16 +118,21 @@
       listenerLastChangeTab((route) => {
         if (route.name === REDIRECT_NAME) return;
         handleMenuChange(route);
-      }, false);
+        const currentActiveMenu = route.meta?.currentActiveMenu;
+        if (currentActiveMenu) {
+          menuState.selectedKeys = [currentActiveMenu];
+          setOpenKeys(currentActiveMenu);
+        }
+      });
 
       watch(
         () => props.items,
         () => {
           handleMenuChange();
-        },
-        {
-          immediate: true,
         }
+        // {
+        //   immediate: true,
+        // }
       );
 
       async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) {
@@ -149,9 +154,7 @@
           return;
         }
         const path = (route || unref(currentRoute)).path;
-        if (props.mode !== MenuModeEnum.HORIZONTAL) {
-          setOpenKeys(path);
-        }
+        setOpenKeys(path);
         if (props.isHorizontal && unref(getSplit)) {
           const parentPath = await getCurrentParentPath(path);
           menuState.selectedKeys = [parentPath];

+ 12 - 4
src/components/Menu/src/components/BasicSubMenuItem.vue

@@ -1,6 +1,9 @@
 <template>
-  <BasicMenuItem v-if="!menuHasChildren(item)" v-bind="$props" />
-  <SubMenu v-else :class="[`${prefixCls}__level${level}`, theme]">
+  <BasicMenuItem v-if="!menuHasChildren(item) && getShowMenu" v-bind="$props" />
+  <SubMenu
+    v-if="menuHasChildren(item) && getShowMenu"
+    :class="[`${prefixCls}__level${level}`, theme]"
+  >
     <template #title>
       <MenuItemContent v-bind="$props" :item="item" />
     </template>
@@ -16,7 +19,7 @@
 <script lang="ts">
   import type { Menu as MenuType } from '/@/router/types';
 
-  import { defineComponent } from 'vue';
+  import { defineComponent, computed } from 'vue';
   import { Menu } from 'ant-design-vue';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { itemProps } from '../props';
@@ -35,8 +38,12 @@
       // ExpandIcon: createAsyncComponent(() => import('./ExpandIcon.vue')),
     },
     props: itemProps,
-    setup() {
+    setup(props) {
       const { prefixCls } = useDesign('basic-menu-item');
+
+      const getShowMenu = computed(() => {
+        return !props.item.meta?.hideMenu;
+      });
       function menuHasChildren(menuTreeItem: MenuType): boolean {
         return (
           Reflect.has(menuTreeItem, 'children') &&
@@ -47,6 +54,7 @@
       return {
         prefixCls,
         menuHasChildren,
+        getShowMenu,
       };
     },
   });

+ 3 - 0
src/components/Menu/src/useOpenKeys.ts

@@ -18,6 +18,9 @@ export function useOpenKeys(
   const { getCollapsed } = useMenuSetting();
 
   function setOpenKeys(path: string) {
+    if (mode.value === MenuModeEnum.HORIZONTAL) {
+      return;
+    }
     const menuList = toRaw(menus.value);
     if (!unref(accordion)) {
       menuState.openKeys = es6Unique([...menuState.openKeys, ...getAllParentPath(menuList, path)]);

+ 83 - 18
src/layouts/default/header/components/Breadcrumb.vue

@@ -1,12 +1,12 @@
 <template>
   <div :class="[prefixCls, `${prefixCls}--${theme}`]">
     <a-breadcrumb :routes="routes">
-      <template #itemRender="{ route, routes }">
+      <template #itemRender="{ route, routes, paths }">
         <Icon :icon="route.meta.icon" v-if="getShowBreadCrumbIcon && route.meta.icon" />
-        <span v-if="routes.indexOf(route) === routes.length - 1">
+        <span v-if="!hasRedirect(routes, route)">
           {{ t(route.meta.title) }}
         </span>
-        <router-link v-else :to="route.path">
+        <router-link v-else to="" @click="handleClick(route, paths, $event)">
           {{ t(route.meta.title) }}
         </router-link>
       </template>
@@ -30,6 +30,8 @@
   import { useRootSetting } from '/@/hooks/setting/useRootSetting';
 
   import { propTypes } from '/@/utils/propTypes';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { isString } from '/@/utils/is';
 
   export default defineComponent({
     name: 'LayoutBreadcrumb',
@@ -45,22 +47,12 @@
 
       const { t } = useI18n();
       watchEffect(() => {
-        if (currentRoute.value.name === REDIRECT_NAME) {
-          return;
-        }
+        if (currentRoute.value.name === REDIRECT_NAME) return;
+
         const matched = currentRoute.value?.matched;
         if (!matched || matched.length === 0) return;
 
-        let breadcrumbList = filter(toRaw(matched), (item) => {
-          if (!item.meta) {
-            return false;
-          }
-          const { title, hideBreadcrumb } = item.meta;
-          if (!title || hideBreadcrumb) {
-            return false;
-          }
-          return true;
-        });
+        let breadcrumbList = filterItem(toRaw(matched));
 
         const filterBreadcrumbList = breadcrumbList.filter(
           (item) => item.path !== PageEnum.BASE_HOME
@@ -71,13 +63,86 @@
             path: PageEnum.BASE_HOME,
             meta: {
               title: t('layout.header.home'),
+              isLink: true,
             },
           } as unknown) as RouteLocationMatched);
         }
-        routes.value = filterBreadcrumbList.length === 1 ? [] : filterBreadcrumbList;
+
+        if (currentRoute.value.meta?.currentActiveMenu) {
+          filterBreadcrumbList.push((currentRoute.value as unknown) as RouteLocationMatched);
+        }
+        // routes.value = filterBreadcrumbList.length === 1 ? [] : filterBreadcrumbList;
+        routes.value = filterBreadcrumbList;
       });
 
-      return { routes, t, prefixCls, getShowBreadCrumbIcon };
+      function filterItem(list: RouteLocationMatched[]) {
+        let resultList = filter(list, (item) => {
+          const { meta } = item;
+
+          if (!meta) {
+            return false;
+          }
+          const { title, hideBreadcrumb, hideMenu } = meta;
+          if (!title || hideBreadcrumb || hideMenu) {
+            return false;
+          }
+
+          return true;
+        }).filter((item) => !item.meta?.hideBreadcrumb || !item.meta?.hideMenu);
+
+        resultList = resultList.filter((item) => item.path !== PageEnum.BASE_HOME);
+        return resultList;
+      }
+
+      function handleClick(route: RouteLocationMatched, paths: string[], e: Event) {
+        e?.preventDefault();
+        const {
+          children,
+          redirect,
+          meta,
+          //  components
+        } = route;
+
+        // const isParent =
+        //   components?.default?.name === 'DefaultLayout' || (components?.default as any)?.parentView;
+
+        if (
+          children?.length &&
+          !redirect
+          // && !isParent
+        ) {
+          e?.stopPropagation();
+          return;
+        }
+        if (meta?.carryParam) {
+          return;
+        }
+
+        const go = useGo();
+        if (redirect && isString(redirect)) {
+          go(redirect);
+        } else {
+          const ps = paths.slice(1);
+          const lastPath = ps.pop() || '';
+          const parentPath = ps.pop() || '';
+          let path = `${parentPath}/${lastPath}`;
+          path = /^\//.test(path) ? path : `/${path}`;
+          go(path);
+        }
+      }
+
+      function hasRedirect(routes: RouteLocationMatched[], route: RouteLocationMatched) {
+        if (route?.meta?.isLink) {
+          return true;
+        }
+
+        if (routes.indexOf(route) === routes.length - 1) {
+          return false;
+        }
+        return true;
+      }
+
+      return { routes, t, prefixCls, getShowBreadCrumbIcon, handleClick, hasRedirect };
     },
   });
 </script>

+ 1 - 1
src/layouts/default/setting/handler.ts

@@ -59,7 +59,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
       return { menuSetting: { bgColor: value } };
 
     case HandlerEnum.MENU_SPLIT:
-      return { menuSetting: { split: value, collapsedShowTitle: true } };
+      return { menuSetting: { split: value } };
 
     case HandlerEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE:
       return { menuSetting: { closeMixSidebarOnChange: value } };

+ 1 - 0
src/layouts/default/sider/MixSider.vue

@@ -38,6 +38,7 @@
 
     <div :class="`${prefixCls}-menu-list`" ref="sideRef" :style="getMenuStyle">
       <div
+        v-show="openMenu"
         :class="[
           `${prefixCls}-menu-list__title`,
           {

+ 20 - 4
src/layouts/default/tabs/index.vue

@@ -30,6 +30,7 @@
 
   import { Tabs } from 'ant-design-vue';
   import TabContent from './components/TabContent.vue';
+  import type { RouteLocationNormalized } from 'vue-router';
 
   import { useGo } from '/@/hooks/web/usePage';
 
@@ -43,6 +44,8 @@
   import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
   import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
 
+  import router from '/@/router';
+
   export default defineComponent({
     name: 'MultipleTabs',
     components: {
@@ -61,7 +64,9 @@
       const go = useGo();
       const { getShowQuick, getShowRedo } = useMultipleTabSetting();
 
-      const getTabsState = computed(() => tabStore.getTabsState);
+      const getTabsState = computed(() => {
+        return tabStore.getTabsState.filter((item) => !item.meta?.hideTab);
+      });
 
       const unClose = computed(() => unref(getTabsState).length === 1);
 
@@ -78,13 +83,24 @@
         const { name } = route;
         if (name === REDIRECT_NAME || !route || !userStore.getTokenState) return;
 
-        const { path, fullPath } = route;
-        const p = fullPath || path;
+        const { path, fullPath, meta = {} } = route;
 
+        const { currentActiveMenu, hideTab } = meta;
+        const isHide = !hideTab ? null : currentActiveMenu;
+        const p = isHide || fullPath || path;
         if (activeKeyRef.value !== p) {
           activeKeyRef.value = p;
         }
-        tabStore.addTabAction(unref(route));
+
+        if (isHide) {
+          const findParentRoute = router
+            .getRoutes()
+            .find((item) => item.path === currentActiveMenu);
+          findParentRoute &&
+            tabStore.addTabAction((findParentRoute as unknown) as RouteLocationNormalized);
+        } else {
+          tabStore.addTabAction(unref(route));
+        }
       });
 
       function handleChange(activeKey: any) {

+ 7 - 0
src/locales/lang/en/routes/demo/feat.ts

@@ -15,4 +15,11 @@ export default {
   tab: 'Tab with parameters',
   tab1: 'Tab with parameter 1',
   tab2: 'Tab with parameter 2',
+
+  breadcrumb: 'Breadcrumbs',
+  breadcrumbFlat: 'Flat Mode',
+  breadcrumbFlatDetail: 'Flat mode details',
+
+  breadcrumbChildren: 'Level mode',
+  breadcrumbChildrenDetail: 'Level mode detail',
 };

+ 7 - 0
src/locales/lang/zh_CN/routes/demo/feat.ts

@@ -15,4 +15,11 @@ export default {
   tab: 'Tab带参',
   tab1: 'Tab带参1',
   tab2: 'Tab带参2',
+
+  breadcrumb: '面包屑导航',
+  breadcrumbFlat: '平级模式',
+  breadcrumbFlatDetail: '平级详情',
+
+  breadcrumbChildren: '层级模式',
+  breadcrumbChildrenDetail: '层级详情',
 };

+ 2 - 2
src/router/helper/routeHelper.ts

@@ -16,7 +16,7 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
     const { component, name } = item;
     const { children } = item;
     if (component) {
-      item.component = dynamicImport(component);
+      item.component = dynamicImport(component as string);
     } else if (name) {
       item.component = getParentLayout(name);
     }
@@ -31,7 +31,7 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
   routeList.forEach((route) => {
     if (route.component) {
       if ((route.component as string).toUpperCase() === 'LAYOUT') {
-        route.component = LayoutMap.get(route.component);
+        route.component = LayoutMap.get(route.component as LayoutMapKey);
       } else {
         route.children = [cloneDeep(route)];
         route.component = LAYOUT;

+ 4 - 2
src/router/menus/index.ts

@@ -71,8 +71,10 @@ export async function getShallowMenus(): Promise<Menu[]> {
 export async function getChildrenMenus(parentPath: string) {
   const menus = await getAsyncMenus();
   const parent = menus.find((item) => item.path === parentPath);
-  if (!parent) return [] as Menu[];
-  return parent.children;
+  if (!parent || !parent.children) return [] as Menu[];
+  const routes = router.getRoutes();
+
+  return !isBackMode() ? filter(parent.children, basicFilter(routes)) : parent.children;
 }
 
 // 通用过滤方法

+ 3 - 3
src/router/menus/modules/demo/comp.ts

@@ -151,9 +151,6 @@ const menu: MenuModule = {
       {
         path: 'loading',
         name: t('routes.demo.comp.loading'),
-        tag: {
-          content: 'new',
-        },
       },
       {
         path: 'tree',
@@ -176,6 +173,9 @@ const menu: MenuModule = {
       {
         name: t('routes.demo.editor.editor'),
         path: 'editor',
+        tag: {
+          content: 'new',
+        },
         children: [
           {
             path: 'markdown',

+ 22 - 0
src/router/menus/modules/demo/feat.ts

@@ -19,6 +19,7 @@ const menu: MenuModule = {
         path: 'tabs',
         name: t('routes.demo.feat.tabs'),
       },
+
       {
         path: 'context-menu',
         name: t('routes.demo.feat.contextMenu'),
@@ -86,6 +87,27 @@ const menu: MenuModule = {
         ],
       },
       {
+        name: t('routes.demo.feat.breadcrumb'),
+        path: 'breadcrumb',
+        tag: {
+          content: 'new',
+        },
+        children: [
+          {
+            path: 'flat',
+            name: t('routes.demo.feat.breadcrumbFlat'),
+          },
+          {
+            path: 'flatDetail',
+            name: t('routes.demo.feat.breadcrumbFlatDetail'),
+          },
+          {
+            path: 'children',
+            name: t('routes.demo.feat.breadcrumbChildrenDetail'),
+          },
+        ],
+      },
+      {
         path: 'testTab',
         name: t('routes.demo.feat.tab'),
         children: [

+ 4 - 4
src/router/menus/modules/demo/iframe.ts

@@ -8,14 +8,14 @@ const menu: MenuModule = {
     path: '/frame',
     children: [
       {
-        path: 'antv',
-        name: t('routes.demo.iframe.antv'),
-      },
-      {
         path: 'doc',
         name: t('routes.demo.iframe.doc'),
       },
       {
+        path: 'antv',
+        name: t('routes.demo.iframe.antv'),
+      },
+      {
         path: 'https://vvbin.cn/doc-next/',
         name: t('routes.demo.iframe.docExternal'),
       },

+ 0 - 3
src/router/menus/modules/demo/level.ts

@@ -6,9 +6,6 @@ const menu: MenuModule = {
   menu: {
     name: t('routes.demo.level.level'),
     path: '/level',
-    tag: {
-      dot: true,
-    },
     children: [
       {
         path: 'menu1',

+ 1 - 6
src/router/menus/modules/demo/page.ts

@@ -6,9 +6,7 @@ const menu: MenuModule = {
   menu: {
     name: t('routes.demo.page.page'),
     path: '/page-demo',
-    tag: {
-      dot: true,
-    },
+
     children: [
       {
         path: 'form',
@@ -102,9 +100,6 @@ const menu: MenuModule = {
       {
         path: 'list',
         name: t('routes.demo.page.list'),
-        tag: {
-          content: 'new',
-        },
         children: [
           {
             path: 'basic',

+ 9 - 8
src/router/routes/modules/demo/charts.ts

@@ -14,12 +14,21 @@ const charts: AppRouteModule = {
   },
   children: [
     {
+      path: 'apexChart',
+      name: 'ApexChart',
+      meta: {
+        title: t('routes.demo.charts.apexChart'),
+      },
+      component: () => import('/@/views/demo/echarts/apex/index.vue'),
+    },
+    {
       path: 'echarts',
       name: 'Echarts',
       component: getParentLayout('Echarts'),
       meta: {
         title: 'Echarts',
       },
+      redirect: '/charts/echarts/map',
       children: [
         {
           path: 'map',
@@ -47,14 +56,6 @@ const charts: AppRouteModule = {
         },
       ],
     },
-    {
-      path: 'apexChart',
-      name: 'ApexChart',
-      meta: {
-        title: t('routes.demo.charts.apexChart'),
-      },
-      component: () => import('/@/views/demo/echarts/apex/index.vue'),
-    },
   ],
 };
 

+ 62 - 0
src/router/routes/modules/demo/feat.ts

@@ -29,6 +29,68 @@ const feat: AppRouteModule = {
         title: t('routes.demo.feat.tabs'),
       },
     },
+    {
+      path: 'breadcrumb',
+      name: 'BreadcrumbDemo',
+      redirect: '/feat/breadcrumb/flat',
+      component: getParentLayout('BreadcrumbDemo'),
+      meta: {
+        title: t('routes.demo.feat.breadcrumb'),
+      },
+
+      children: [
+        {
+          path: 'flat',
+          name: 'BreadcrumbFlatDemo',
+          component: () => import('/@/views/demo/feat/breadcrumb/FlatList.vue'),
+          meta: {
+            title: t('routes.demo.feat.breadcrumbFlat'),
+          },
+        },
+        {
+          path: 'flatDetail',
+          name: 'BreadcrumbFlatDetailDemo',
+          component: () => import('/@/views/demo/feat/breadcrumb/FlatListDetail.vue'),
+          meta: {
+            title: t('routes.demo.feat.breadcrumbFlatDetail'),
+            hideMenu: true,
+            hideTab: true,
+            currentActiveMenu: '/feat/breadcrumb/flat',
+          },
+        },
+        {
+          path: 'children',
+          name: 'BreadcrumbChildrenDemo',
+          component: getParentLayout('BreadcrumbChildrenDemo'),
+          redirect: '/feat/breadcrumb/children',
+          meta: {
+            title: t('routes.demo.feat.breadcrumbFlat'),
+          },
+          children: [
+            {
+              path: '',
+              name: 'BreadcrumbChildren',
+              component: () => import('/@/views/demo/feat/breadcrumb/ChildrenList.vue'),
+              meta: {
+                title: t('routes.demo.feat.breadcrumbChildren'),
+                hideBreadcrumb: true,
+              },
+            },
+            {
+              path: 'childrenDetail',
+              name: 'BreadcrumbChildrenDetailDemo',
+              component: () => import('/@/views/demo/feat/breadcrumb/ChildrenListDetail.vue'),
+              meta: {
+                currentActiveMenu: '/feat/breadcrumb/children',
+                title: t('routes.demo.feat.breadcrumbChildrenDetail'),
+                hideTab: true,
+                hideMenu: true,
+              },
+            },
+          ],
+        },
+      ],
+    },
 
     {
       path: 'context-menu',

+ 9 - 9
src/router/routes/modules/demo/iframe.ts

@@ -8,7 +8,7 @@ const iframe: AppRouteModule = {
   path: '/frame',
   name: 'Frame',
   component: LAYOUT,
-  redirect: '/frame/antv',
+  redirect: '/frame/doc',
   meta: {
     icon: 'mdi:page-next-outline',
     title: t('routes.demo.iframe.frame'),
@@ -16,21 +16,21 @@ const iframe: AppRouteModule = {
 
   children: [
     {
-      path: 'antv',
-      name: 'Antv',
+      path: 'doc',
+      name: 'Doc',
       component: IFrame,
       meta: {
-        frameSrc: 'https://2x.antdv.com/docs/vue/introduce-cn/',
-        title: t('routes.demo.iframe.antv'),
+        frameSrc: 'https://vvbin.cn/doc-next/',
+        title: t('routes.demo.iframe.doc'),
       },
     },
     {
-      path: 'doc',
-      name: 'Doc',
+      path: 'antv',
+      name: 'Antv',
       component: IFrame,
       meta: {
-        frameSrc: 'https://vvbin.cn/doc-next/',
-        title: t('routes.demo.iframe.doc'),
+        frameSrc: 'https://2x.antdv.com/docs/vue/introduce-cn/',
+        title: t('routes.demo.iframe.antv'),
       },
     },
     {

+ 3 - 1
src/router/routes/modules/demo/level.ts

@@ -7,7 +7,7 @@ const permission: AppRouteModule = {
   path: '/level',
   name: 'Level',
   component: LAYOUT,
-  redirect: '/level/menu1/menu1-1',
+  redirect: '/level/menu1/menu1-1/menu1-1-1',
   meta: {
     icon: 'carbon:user-role',
     title: t('routes.demo.level.level'),
@@ -21,6 +21,7 @@ const permission: AppRouteModule = {
       meta: {
         title: 'Menu1',
       },
+      redirect: '/level/menu1/menu1-1/menu1-1-1',
       children: [
         {
           path: 'menu1-1',
@@ -29,6 +30,7 @@ const permission: AppRouteModule = {
           meta: {
             title: 'Menu1-1',
           },
+          redirect: '/level/menu1/menu1-1/menu1-1-1',
           children: [
             {
               path: 'menu1-1-1',

+ 10 - 1
src/router/types.ts

@@ -30,13 +30,22 @@ export interface RouteMeta {
 
   // Used internally to mark single-level menus
   single?: boolean;
+
+  // Currently active menu
+  currentActiveMenu?: string;
+
+  // Never show in tab
+  hideTab?: boolean;
+
+  // Never show in menu
+  hideMenu?: boolean;
 }
 
 // @ts-ignore
 export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
   name: string;
   meta: RouteMeta;
-  component?: Component;
+  component?: Component | string;
   components?: Component;
   children?: AppRouteRecordRaw[];
   props?: Record<string, any>;

+ 10 - 0
src/views/demo/feat/breadcrumb/ChildrenList.vue

@@ -0,0 +1,10 @@
+<template>
+  <div class="p-5">
+    <router-link to="/feat/breadcrumb/children/childrenDetail">进入子级详情页</router-link>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({});
+</script>

+ 8 - 0
src/views/demo/feat/breadcrumb/ChildrenListDetail.vue

@@ -0,0 +1,8 @@
+<template>
+  <div> 子级详情页 </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({});
+</script>

+ 10 - 0
src/views/demo/feat/breadcrumb/FlatList.vue

@@ -0,0 +1,10 @@
+<template>
+  <div class="p-5">
+    <router-link to="/feat/breadcrumb/flatDetail">进入平级详情页</router-link>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({});
+</script>

+ 8 - 0
src/views/demo/feat/breadcrumb/FlatListDetail.vue

@@ -0,0 +1,8 @@
+<template>
+  <div> 平级详情页 </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({});
+</script>