vben
2020-12-14 cbcd9098671e2fff9152976b7e9e815fea29d074
wip(menu): perf menu
2个文件已删除
4个文件已添加
8个文件已修改
788 ■■■■ 已修改文件
src/components/Application/src/AppLogo.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/index.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/BasicMenu.tsx 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/BasicMenu.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/components/BasicMenuItem.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/components/BasicSubMenuItem.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/components/ExpandIcon.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/helper.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/index.less 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/props.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/types.ts 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/appEnum.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/index.less 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/helper/menuHelper.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Application/src/AppLogo.vue
@@ -66,8 +66,9 @@
  .@{prefix-cls} {
    display: flex;
    align-items: center;
    padding-left: 12px;
    padding-left: 7px;
    cursor: pointer;
    transition: all 0.2s ease;
    &.collapsed-show-title {
      padding-left: 20px;
src/components/Menu/index.ts
@@ -2,6 +2,8 @@
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export const BasicMenu = createAsyncComponent(() => import('./src/BasicMenu'), { loading: false });
export const BasicMenu = createAsyncComponent(() => import('./src/BasicMenu.vue'), {
  loading: false,
});
withInstall(BasicMenu);
src/components/Menu/src/BasicMenu.tsx
File was deleted
src/components/Menu/src/BasicMenu.vue
New file
@@ -0,0 +1,209 @@
<template>
  <slot name="header" v-if="!getIsHorizontal" />
  <ScrollContainer :class="`${prefixCls}-wrapper`" :style="getWrapperStyle">
    <Menu
      :selectedKeys="selectedKeys"
      :defaultSelectedKeys="defaultSelectedKeys"
      :mode="mode"
      :openKeys="getOpenKeys"
      :inlineIndent="inlineIndent"
      :theme="theme"
      @openChange="handleOpenChange"
      :class="getMenuClass"
      @click="handleMenuClick"
      :subMenuOpenDelay="0.2"
      v-bind="getInlineCollapseOptions"
    >
      <template v-for="item in items" :key="item.path">
        <BasicSubMenuItem
          :item="item"
          :theme="theme"
          :level="1"
          :appendClass="appendClass"
          :parentPath="currentParentPath"
          :showTitle="showTitle"
          :isHorizontal="isHorizontal"
        />
      </template>
    </Menu>
  </ScrollContainer>
</template>
<script lang="ts">
  import type { MenuState } from './types';
  import {
    computed,
    defineComponent,
    unref,
    reactive,
    watch,
    toRefs,
    ref,
    CSSProperties,
  } from 'vue';
  import { Menu } from 'ant-design-vue';
  import BasicSubMenuItem from './components/BasicSubMenuItem.vue';
  import { ScrollContainer } from '/@/components/Container';
  import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
  import { appStore } from '/@/store/modules/app';
  import { useOpenKeys } from './useOpenKeys';
  import { useRouter } from 'vue-router';
  import { isFunction } from '/@/utils/is';
  import { getCurrentParentPath } from '/@/router/menus';
  import { basicProps } from './props';
  import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
  import { REDIRECT_NAME } from '/@/router/constant';
  import { tabStore } from '/@/store/modules/tab';
  import { useDesign } from '/@/hooks/web/useDesign';
  // import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
  export default defineComponent({
    name: 'BasicMenu',
    components: {
      Menu,
      ScrollContainer,
      BasicSubMenuItem,
      // BasicSubMenuItem: createAsyncComponent(() => import('./components/BasicSubMenuItem.vue')),
    },
    props: basicProps,
    emits: ['menuClick'],
    setup(props, { emit }) {
      const currentParentPath = ref('');
      const isClickGo = ref(false);
      const menuState = reactive<MenuState>({
        defaultSelectedKeys: [],
        openKeys: [],
        selectedKeys: [],
        collapsedOpenKeys: [],
      });
      const { prefixCls } = useDesign('basic-menu');
      const { items, mode, accordion } = toRefs(props);
      const { getCollapsed, getIsHorizontal, getTopMenuAlign, getSplit } = useMenuSetting();
      const { currentRoute } = useRouter();
      const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys(
        menuState,
        items,
        mode,
        accordion
      );
      const getMenuClass = computed(() => {
        const { type, mode } = props;
        return [
          prefixCls,
          `justify-${unref(getTopMenuAlign)}`,
          {
            [`${prefixCls}--hide-title`]: !unref(showTitle),
            [`${prefixCls}--collapsed-show-title`]: props.collapsedShowTitle,
            [`${prefixCls}__second`]:
              !props.isHorizontal && appStore.getProjectConfig.menuSetting.split,
            [`${prefixCls}__sidebar-hor`]:
              type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL,
          },
        ];
      });
      const showTitle = computed(() => props.collapsedShowTitle && unref(getCollapsed));
      const getInlineCollapseOptions = computed(() => {
        const isInline = props.mode === MenuModeEnum.INLINE;
        const inlineCollapseOptions: { inlineCollapsed?: boolean } = {};
        if (isInline) {
          inlineCollapseOptions.inlineCollapsed = unref(getCollapsed);
        }
        return inlineCollapseOptions;
      });
      const getWrapperStyle = computed(
        (): CSSProperties => {
          return {
            height: `calc(100% - ${props.showLogo ? '48px' : '0px'})`,
            overflowY: 'hidden',
          };
        }
      );
      watch(
        () => tabStore.getCurrentTab,
        () => {
          if (unref(currentRoute).name === REDIRECT_NAME) return;
          handleMenuChange();
          unref(getSplit) && getParentPath();
        }
      );
      watch(
        () => props.items,
        () => {
          handleMenuChange();
        },
        {
          immediate: true,
        }
      );
      getParentPath();
      async function getParentPath() {
        const { appendClass } = props;
        if (!appendClass) return '';
        const parentPath = await getCurrentParentPath(unref(currentRoute).path);
        currentParentPath.value = parentPath;
      }
      async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) {
        const { beforeClickFn } = props;
        if (beforeClickFn && isFunction(beforeClickFn)) {
          const flag = await beforeClickFn(key);
          if (!flag) return;
        }
        emit('menuClick', key);
        isClickGo.value = true;
        menuState.openKeys = keyPath;
        menuState.selectedKeys = [key];
      }
      function handleMenuChange() {
        if (unref(isClickGo)) {
          isClickGo.value = false;
          return;
        }
        const path = unref(currentRoute).path;
        if (props.mode !== MenuModeEnum.HORIZONTAL) {
          setOpenKeys(path);
        }
        menuState.selectedKeys = [path];
      }
      return {
        prefixCls,
        getIsHorizontal,
        getWrapperStyle,
        handleMenuClick,
        getInlineCollapseOptions,
        getMenuClass,
        handleOpenChange,
        getOpenKeys,
        currentParentPath,
        showTitle,
        ...toRefs(menuState),
      };
    },
  });
</script>
<style lang="less">
  @import './index.less';
</style>
src/components/Menu/src/components/BasicMenuItem.vue
New file
@@ -0,0 +1,39 @@
<template>
  <MenuItem :class="getLevelClass">
    <MenuContent v-bind="$props" :item="item" />
  </MenuItem>
</template>
<script lang="ts">
  import { defineComponent, computed } from 'vue';
  import { Menu } from 'ant-design-vue';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { itemProps } from '../props';
  import MenuContent from '../MenuContent';
  export default defineComponent({
    name: 'BasicMenuItem',
    components: { MenuItem: Menu.Item, MenuContent },
    props: itemProps,
    setup(props) {
      const { prefixCls } = useDesign('basic-menu-item');
      const getLevelClass = computed(() => {
        const { appendClass, level, item, parentPath, theme } = props;
        const isAppendActiveCls = appendClass && level === 1 && item.path === parentPath;
        const levelCls = [
          `${prefixCls}__level${level}`,
          theme,
          {
            'top-active-menu': isAppendActiveCls,
          },
        ];
        return levelCls;
      });
      return {
        prefixCls,
        getLevelClass,
      };
    },
  });
</script>
src/components/Menu/src/components/BasicSubMenuItem.vue
New file
@@ -0,0 +1,53 @@
<template>
  <BasicMenuItem v-if="!menuHasChildren(item)" v-bind="$props" />
  <SubMenu v-else :class="[`${prefixCls}__level${level}`, theme]">
    <template #title>
      <MenuContent v-bind="$props" :item="item" />
    </template>
    <!-- <template #expandIcon="{ key }">
      <ExpandIcon :key="key" />
    </template> -->
    <template v-for="childrenItem in item.children || []" :key="childrenItem.path">
      <BasicSubMenuItem v-bind="$props" :item="childrenItem" :level="level + 1" />
    </template>
  </SubMenu>
</template>
<script lang="ts">
  import type { Menu as MenuType } from '/@/router/types';
  import { defineComponent } from 'vue';
  import { Menu } from 'ant-design-vue';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { itemProps } from '../props';
  import BasicMenuItem from './BasicMenuItem.vue';
  import MenuContent from '../MenuContent';
  // import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
  export default defineComponent({
    name: 'BasicSubMenuItem',
    components: {
      BasicMenuItem,
      SubMenu: Menu.SubMenu,
      MenuItem: Menu.Item,
      MenuContent,
      // ExpandIcon: createAsyncComponent(() => import('./ExpandIcon.vue')),
    },
    props: itemProps,
    setup() {
      const { prefixCls } = useDesign('basic-menu-item');
      function menuHasChildren(menuTreeItem: MenuType): boolean {
        return (
          Reflect.has(menuTreeItem, 'children') &&
          !!menuTreeItem.children &&
          menuTreeItem.children.length > 0
        );
      }
      return {
        prefixCls,
        menuHasChildren,
      };
    },
  });
</script>
src/components/Menu/src/components/ExpandIcon.vue
New file
@@ -0,0 +1,43 @@
<template>
  <BasicArrow :expand="getIsOpen" bottom inset :class="getWrapperClass" />
</template>
<script lang="ts">
  import { defineComponent, PropType, computed } from 'vue';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { BasicArrow } from '/@/components/Basic';
  import { propTypes } from '/@/utils/propTypes';
  export default defineComponent({
    name: 'BasicMenuItem',
    components: { BasicArrow },
    props: {
      key: propTypes.string,
      openKeys: {
        type: Array as PropType<string[]>,
        default: [],
      },
      collapsed: propTypes.bool,
    },
    setup(props) {
      const { prefixCls } = useDesign('basic-menu');
      const getIsOpen = computed(() => {
        return props.openKeys.includes(props.key);
      });
      const getWrapperClass = computed(() => {
        return [
          `${prefixCls}__expand-icon`,
          {
            [`${prefixCls}__expand-icon--collapsed`]: props.collapsed,
          },
        ];
      });
      return {
        prefixCls,
        getIsOpen,
        getWrapperClass,
      };
    },
  });
</script>
src/components/Menu/src/helper.ts
File was deleted
src/components/Menu/src/index.less
@@ -4,6 +4,7 @@
.active-style() {
  color: @white;
  // background: @primary-color !important;
  background: linear-gradient(
    118deg,
    rgba(@primary-color, 0.8),
@@ -27,6 +28,7 @@
  //   right: 16px;
  //   width: 10px;
  //   transform-origin: none;
  //   opacity: 0.45;
  //   span[role='img'] {
  //     margin-right: 0;
@@ -52,9 +54,9 @@
      > .ant-menu-item-group-list
      > .ant-menu-submenu
      > .ant-menu-submenu-title,
    &.ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title {
      padding-right: 20px !important;
      padding-left: 20px !important;
    &.ant-menu-inline-collapsed .ant-menu-submenu-title {
      padding-right: 16px !important;
      padding-left: 16px !important;
    }
  }
@@ -87,32 +89,33 @@
    }
  }
  // .ant-menu-item {
  //   transition: unset;
  // }
  .ant-menu-item {
    transition: unset;
  }
  // scrollbar -s tart
  &-wrapper {
    /* 滚动槽 */
    &::-webkit-scrollbar {
      width: 5px;
      height: 5px;
    }
  // &-wrapper {
    &::-webkit-scrollbar-track {
      background: rgba(0, 0, 0, 0);
    }
  /* 滚动槽 */
  // &::-webkit-scrollbar {
  //   width: 5px;
  //   height: 5px;
  // }
    &::-webkit-scrollbar-thumb {
      background: rgba(255, 255, 255, 0.2);
      border-radius: 3px;
      box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
    }
  // &::-webkit-scrollbar-track {
  //   background: rgba(0, 0, 0, 0);
  // }
    ::-webkit-scrollbar-thumb:hover {
      background: @border-color-dark;
    }
  }
  // &::-webkit-scrollbar-thumb {
  //   background: rgba(255, 255, 255, 0.2);
  //   border-radius: 3px;
  //   box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
  // }
  // ::-webkit-scrollbar-thumb:hover {
  //   background: @border-color-dark;
  // }
  // }
  // scrollbar  end
@@ -225,14 +228,6 @@
    }
  }
  &:not(.@{basic-menu-prefix-cls}__sidebar-hor).ant-menu-inline-collapsed {
    .@{basic-menu-prefix-cls}-item__level1 {
      > div {
        align-items: center;
      }
    }
  }
  &.ant-menu-dark:not(.@{basic-menu-prefix-cls}__sidebar-hor):not(.@{basic-menu-prefix-cls}__second) {
    // Reset menu item row height
    .ant-menu-item:not(.@{basic-menu-prefix-cls}-item__level1),
@@ -254,10 +249,6 @@
    overflow: hidden;
    background: @sider-dark-bg-color;
    .active-menu-style();
    // .menu-item-icon.app-iconify {
    //   display: inline-block !important;
    // }
    .ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1,
    .ant-menu-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 {
@@ -304,10 +295,6 @@
    overflow: hidden;
    border-right: none;
    // .menu-item-icon.app-iconify {
    //   display: inline-block !important;
    // }
    .ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1,
    .ant-menu-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 {
      color: @primary-color;
@@ -332,6 +319,7 @@
      align-items: center;
    }
  }
  .@{basic-menu-prefix-cls}__tag {
    position: absolute;
    top: calc(50% - 8px);
@@ -368,6 +356,20 @@
      background: @warning-color;
    }
  }
  .ant-menu-submenu,
  .ant-menu-submenu-inline {
    transition: unset;
  }
  // .ant-menu-submenu-arrow {
  //   transition: all 0.15s ease 0s;
  // }
  .ant-menu-inline.ant-menu-sub {
    box-shadow: unset !important;
    transition: unset;
  }
}
.ant-menu-dark {
@@ -375,7 +377,6 @@
    > ul {
      background: @sider-dark-bg-color;
    }
    .active-menu-style();
  }
}
src/components/Menu/src/props.ts
@@ -3,57 +3,47 @@
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { ThemeEnum } from '/@/enums/appEnum';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = {
  items: {
    type: Array as PropType<Menu[]>,
    default: () => [],
  },
  appendClass: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  appendClass: propTypes.bool,
  collapsedShowTitle: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  collapsedShowTitle: propTypes.bool,
  // 最好是4 倍数
  inlineIndent: {
    type: Number as PropType<number>,
    default: 20,
  },
  inlineIndent: propTypes.number.def(20),
  // 菜单组件的mode属性
  mode: {
    type: String as PropType<MenuModeEnum>,
    default: MenuModeEnum.INLINE,
  },
  showLogo: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  showLogo: propTypes.bool,
  type: {
    type: String as PropType<MenuTypeEnum>,
    default: MenuTypeEnum.MIX,
  },
  theme: {
    type: String as PropType<string>,
    default: ThemeEnum.DARK,
  },
  inlineCollapsed: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  theme: propTypes.string.def(ThemeEnum.DARK),
  inlineCollapsed: propTypes.bool,
  isHorizontal: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  accordion: {
    type: Boolean as PropType<boolean>,
    default: true,
  },
  isHorizontal: propTypes.bool,
  accordion: propTypes.bool.def(true),
  beforeClickFn: {
    type: Function as PropType<(key: string) => Promise<boolean>>,
  },
};
export const itemProps = {
  item: {
    type: Object as PropType<Menu>,
    default: {},
  },
  level: propTypes.number,
  theme: propTypes.oneOf(['dark', 'light']),
  appendClass: propTypes.bool,
  parentPath: propTypes.string,
  showTitle: propTypes.bool,
  isHorizontal: propTypes.bool,
};
src/components/Menu/src/types.ts
@@ -1,15 +1,15 @@
import { ComputedRef } from 'vue';
import { ThemeEnum } from '/@/enums/appEnum';
import { MenuModeEnum } from '/@/enums/menuEnum';
// import { ComputedRef } from 'vue';
// import { ThemeEnum } from '/@/enums/appEnum';
// import { MenuModeEnum } from '/@/enums/menuEnum';
export interface MenuState {
  // 默认选中的列表
  defaultSelectedKeys: string[];
  // 模式
  mode: MenuModeEnum;
  // mode: MenuModeEnum;
  // 主题
  theme: ComputedRef<ThemeEnum> | ThemeEnum;
  // // 主题
  // theme: ComputedRef<ThemeEnum> | ThemeEnum;
  // 缩进
  inlineIndent?: number;
src/enums/appEnum.ts
@@ -1,4 +1,4 @@
export const SIDE_BAR_MINI_WIDTH = 58;
export const SIDE_BAR_MINI_WIDTH = 48;
export const SIDE_BAR_SHOW_TIT_MINI_WIDTH = 80;
export enum ContentEnum {
src/layouts/default/tabs/index.less
@@ -34,9 +34,9 @@
        border: 1px solid darken(@border-color-light, 6%);
        transition: none;
        &:not(.ant-tabs-tab-active)::before {
        &:not(.ant-tabs-tab-active)::after {
          position: absolute;
          top: -1px;
          bottom: -1px;
          left: 50%;
          width: 100%;
          height: 2px;
@@ -53,7 +53,7 @@
            opacity: 1;
          }
          &:not(.ant-tabs-tab-active)::before {
          &:not(.ant-tabs-tab-active)::after {
            opacity: 1;
            transform: translate(-50%, 0) scaleX(1);
            transition: all 0.3s ease-in-out;
src/router/helper/menuHelper.ts
@@ -42,6 +42,7 @@
  forEach(menuList, (m) => {
    !isUrl(m.path) && joinParentPath(menuList, m);
  });
  return menuList[0];
}