Browse Source

refactor(menu): added component. Solve the menu stuck problem

vben 4 years ago
parent
commit
ff2b12b409
43 changed files with 1793 additions and 213 deletions
  1. 4 0
      CHANGELOG.zh_CN.md
  2. 1 2
      mock/sys/user.ts
  3. 9 9
      package.json
  4. 49 48
      src/components/Application/src/search/AppSearchModal.vue
  5. 0 1
      src/components/MenuPlus/index.ts
  6. 0 64
      src/components/MenuPlus/src/index.vue
  7. 1 0
      src/components/SimpleMenu/index.ts
  8. 135 0
      src/components/SimpleMenu/src/SimpleMenu.vue
  9. 70 0
      src/components/SimpleMenu/src/SimpleMenuTag.vue
  10. 115 0
      src/components/SimpleMenu/src/SimpleSubMenu.vue
  11. 149 0
      src/components/SimpleMenu/src/components/Menu.vue
  12. 78 0
      src/components/SimpleMenu/src/components/MenuCollapseTransition.vue
  13. 103 0
      src/components/SimpleMenu/src/components/MenuItem.vue
  14. 329 0
      src/components/SimpleMenu/src/components/SubMenuItem.vue
  15. 332 0
      src/components/SimpleMenu/src/components/menu.less
  16. 25 0
      src/components/SimpleMenu/src/components/types.ts
  17. 86 0
      src/components/SimpleMenu/src/components/useMenu.ts
  18. 18 0
      src/components/SimpleMenu/src/components/useSimpleMenuContext.ts
  19. 67 0
      src/components/SimpleMenu/src/index.less
  20. 5 0
      src/components/SimpleMenu/src/types.ts
  21. 45 0
      src/components/SimpleMenu/src/useOpenKeys.ts
  22. 1 1
      src/components/Table/src/components/editable/EditableCell.vue
  23. 0 16
      src/components/Table/src/hooks/useProvinceTable.ts
  24. 6 0
      src/design/ant/index.less
  25. 0 2
      src/design/color.less
  26. 6 3
      src/hooks/setting/useMenuSetting.ts
  27. 1 1
      src/layouts/default/header/index.vue
  28. 14 2
      src/layouts/default/menu/index.tsx
  29. 11 2
      src/layouts/default/menu/useLayoutMenu.ts
  30. 17 9
      src/layouts/default/sider/MixSider.vue
  31. 5 0
      src/locales/lang/en/component/drawer.ts
  32. 3 0
      src/locales/lang/en/component/menu.ts
  33. 4 0
      src/locales/lang/en/component/modal.ts
  34. 5 0
      src/locales/lang/zh_CN/component/drawer.ts
  35. 3 0
      src/locales/lang/zh_CN/component/menu.ts
  36. 4 0
      src/locales/lang/zh_CN/component/modal.ts
  37. 1 1
      src/logics/theme/index.ts
  38. 2 0
      src/router/guard/index.ts
  39. 1 1
      src/router/guard/stateGuard.ts
  40. 2 0
      src/router/menus/index.ts
  41. 2 2
      src/utils/mitt.ts
  42. 1 0
      vite.config.ts
  43. 83 49
      yarn.lock

+ 4 - 0
CHANGELOG.zh_CN.md

@@ -1,5 +1,9 @@
 ## Wip
 
+### ✨ Refactor
+
+- 新增 `SimpleMenu`组件替代左侧菜单组件(顶部菜单没有替换,功能尽量做到简单不卡)。解决菜单卡顿问题。
+
 ### 🐛 Bug Fixes
 
 - 修复 `TableAction`图标问题

+ 1 - 2
mock/sys/user.ts

@@ -39,7 +39,7 @@ export default [
   // mock user login
   {
     url: '/api/login',
-    timeout: 1000,
+    timeout: 200,
     method: 'post',
     response: ({ body }) => {
       const { username, password } = body;
@@ -62,7 +62,6 @@ export default [
   },
   {
     url: '/api/getUserInfoById',
-    timeout: 200,
     method: 'get',
     response: ({ query }) => {
       const { userId } = query;

+ 9 - 9
package.json

@@ -19,7 +19,7 @@
   },
   "dependencies": {
     "@iconify/iconify": "^2.0.0-rc.6",
-    "@vueuse/core": "^4.0.5",
+    "@vueuse/core": "^4.0.8",
     "ant-design-vue": "^2.0.0-rc.8",
     "apexcharts": "^3.23.1",
     "axios": "^0.21.1",
@@ -45,12 +45,12 @@
   "devDependencies": {
     "@commitlint/cli": "^11.0.0",
     "@commitlint/config-conventional": "^11.0.0",
-    "@iconify/json": "^1.1.286",
+    "@iconify/json": "^1.1.287",
     "@ls-lint/ls-lint": "^1.9.2",
     "@purge-icons/generated": "^0.5.1",
     "@types/echarts": "^4.9.3",
     "@types/fs-extra": "^9.0.6",
-    "@types/http-proxy": "^1.17.4",
+    "@types/http-proxy": "^1.17.5",
     "@types/koa-static": "^4.0.1",
     "@types/lodash-es": "^4.17.4",
     "@types/mockjs": "^1.0.3",
@@ -63,24 +63,24 @@
     "@typescript-eslint/eslint-plugin": "^4.13.0",
     "@typescript-eslint/parser": "^4.13.0",
     "@vitejs/plugin-legacy": "^1.2.1",
-    "@vitejs/plugin-vue": "^1.0.5",
+    "@vitejs/plugin-vue": "^1.0.6",
     "@vitejs/plugin-vue-jsx": "^1.0.2",
     "@vue/compiler-sfc": "^3.0.5",
     "@vuedx/typecheck": "^0.5.0",
     "@vuedx/typescript-plugin-vue": "^0.5.0",
     "autoprefixer": "^10.2.1",
-    "commitizen": "^4.2.2",
+    "commitizen": "^4.2.3",
     "conventional-changelog-cli": "^2.1.1",
     "conventional-changelog-custom-config": "^0.3.1",
     "cross-env": "^7.0.3",
     "dotenv": "^8.2.0",
-    "eslint": "^7.17.0",
+    "eslint": "^7.18.0",
     "eslint-config-prettier": "^7.1.0",
     "eslint-plugin-prettier": "^3.3.1",
     "eslint-plugin-vue": "^7.4.1",
     "esno": "^0.4.0",
     "fs-extra": "^9.0.1",
-    "husky": "^4.3.7",
+    "husky": "^4.3.8",
     "koa-static": "^5.0.0",
     "less": "^4.1.0",
     "lint-staged": "^10.5.3",
@@ -96,11 +96,11 @@
     "stylelint-order": "^4.1.0",
     "ts-node": "^9.1.0",
     "typescript": "^4.1.3",
-    "vite": "^2.0.0-beta.27",
+    "vite": "^2.0.0-beta.30",
     "vite-plugin-html": "^2.0.0-beta.5",
     "vite-plugin-mock": "^2.0.0-beta.3",
     "vite-plugin-purge-icons": "^0.5.1",
-    "vite-plugin-pwa": "^0.3.6",
+    "vite-plugin-pwa": "^0.3.8",
     "vue-eslint-parser": "^7.3.0",
     "yargs": "^16.2.0"
   },

+ 49 - 48
src/components/Application/src/search/AppSearchModal.vue

@@ -2,54 +2,52 @@
   <Teleport to="body">
     <transition name="zoom-fade" mode="out-in">
       <div :class="getClass" @click.stop v-if="visible">
-        <ClickOutSide @clickOutside="handleClose">
-          <div :class="`${prefixCls}-content`">
-            <div :class="`${prefixCls}-input__wrapper`">
-              <a-input
-                :class="`${prefixCls}-input`"
-                :placeholder="t('common.searchText')"
-                allow-clear
-                @change="handleSearch"
-              >
-                <template #prefix>
-                  <SearchOutlined />
-                </template>
-              </a-input>
-              <span :class="`${prefixCls}-cancel`" @click="handleClose">{{
-                t('common.cancelText')
-              }}</span>
-            </div>
+        <div :class="`${prefixCls}-content`" v-click-outside="handleClose">
+          <div :class="`${prefixCls}-input__wrapper`">
+            <a-input
+              :class="`${prefixCls}-input`"
+              :placeholder="t('common.searchText')"
+              allow-clear
+              @change="handleSearch"
+            >
+              <template #prefix>
+                <SearchOutlined />
+              </template>
+            </a-input>
+            <span :class="`${prefixCls}-cancel`" @click="handleClose">{{
+              t('common.cancelText')
+            }}</span>
+          </div>
 
-            <div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
-              {{ t('component.app.searchNotData') }}
-            </div>
-            <ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap">
-              <li
-                :ref="setRefs(index)"
-                v-for="(item, index) in searchResult"
-                :key="item.path"
-                :data-index="index"
-                @mouseenter="handleMouseenter"
-                @click="handleEnter"
-                :class="[
-                  `${prefixCls}-list__item`,
-                  {
-                    [`${prefixCls}-list__item--active`]: activeIndex === index,
-                  },
-                ]"
-              >
-                <div :class="`${prefixCls}-list__item-icon`">
-                  <g-icon :icon="item.icon || 'mdi:form-select'" :size="20" />
-                </div>
-                <div :class="`${prefixCls}-list__item-text`">{{ item.name }}</div>
-                <div :class="`${prefixCls}-list__item-enter`">
-                  <g-icon icon="ant-design:enter-outlined" :size="20" />
-                </div>
-              </li>
-            </ul>
-            <AppSearchFooter />
+          <div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
+            {{ t('component.app.searchNotData') }}
           </div>
-        </ClickOutSide>
+          <ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap">
+            <li
+              :ref="setRefs(index)"
+              v-for="(item, index) in searchResult"
+              :key="item.path"
+              :data-index="index"
+              @mouseenter="handleMouseenter"
+              @click="handleEnter"
+              :class="[
+                `${prefixCls}-list__item`,
+                {
+                  [`${prefixCls}-list__item--active`]: activeIndex === index,
+                },
+              ]"
+            >
+              <div :class="`${prefixCls}-list__item-icon`">
+                <g-icon :icon="item.icon || 'mdi:form-select'" :size="20" />
+              </div>
+              <div :class="`${prefixCls}-list__item-text`">{{ item.name }}</div>
+              <div :class="`${prefixCls}-list__item-enter`">
+                <g-icon icon="ant-design:enter-outlined" :size="20" />
+              </div>
+            </li>
+          </ul>
+          <AppSearchFooter />
+        </div>
       </div>
     </transition>
   </Teleport>
@@ -63,17 +61,20 @@
   import { SearchOutlined } from '@ant-design/icons-vue';
   import AppSearchFooter from './AppSearchFooter.vue';
   import { useI18n } from '/@/hooks/web/useI18n';
-  import { ClickOutSide } from '/@/components/ClickOutSide';
   import { useAppInject } from '/@/hooks/web/useAppInject';
+  import clickOutside from '/@/directives/clickOutside';
 
   export default defineComponent({
     name: 'AppSearchModal',
-    components: { SearchOutlined, ClickOutSide, AppSearchFooter },
+    components: { SearchOutlined, AppSearchFooter },
     emits: ['close'],
 
     props: {
       visible: Boolean,
     },
+    directives: {
+      clickOutside,
+    },
     setup(_, { emit }) {
       const scrollWrap = ref<ElRef>(null);
       const { prefixCls } = useDesign('app-search-modal');

+ 0 - 1
src/components/MenuPlus/index.ts

@@ -1 +0,0 @@
-export { default as Menu } from './src/index.vue';

+ 0 - 64
src/components/MenuPlus/src/index.vue

@@ -1,64 +0,0 @@
-<template>
-  <ul :class="getClass" :style="getStyle">
-    <slot></slot>
-  </ul>
-</template>
-
-<script lang="ts">
-  import { defineComponent, ref, computed, CSSProperties, unref } from 'vue';
-  import { useDesign } from '/@/hooks/web/useDesign';
-  import { propTypes } from '/@/utils/propTypes';
-  export default defineComponent({
-    props: {
-      mode: propTypes.oneOf(['horizontal', 'vertical']).def('vertical'),
-      theme: propTypes.oneOf(['light', 'dark', 'primary']).def('light'),
-      activeName: propTypes.oneOfType([propTypes.string, propTypes.number]),
-      openNames: propTypes.array.def([]),
-      accordion: propTypes.bool,
-      width: propTypes.string.def('210px'),
-    },
-    setup(props) {
-      const currentActiveName = ref(props.activeName);
-      const openedNames = ref<string[]>();
-
-      const { prefixCls } = useDesign('menu');
-
-      const getClass = computed(() => {
-        const { theme, mode } = props;
-        let curTheme = theme;
-        if (mode === 'vertical' && theme === 'primary') {
-          curTheme = 'light';
-        }
-        return [
-          prefixCls,
-          `${prefixCls}-${curTheme}`,
-          {
-            [`${prefixCls}-${mode}`]: mode,
-          },
-        ];
-      });
-
-      const getStyle = computed(
-        (): CSSProperties => {
-          const { mode, width } = props;
-          if (mode === 'vertical') {
-            return {
-              width: width,
-            };
-          }
-          return {};
-        }
-      );
-
-      function updateActiveName() {
-        if (unref(currentActiveName) === undefined) {
-          currentActiveName.value = -1;
-        }
-      }
-
-      function updateOpened() {}
-
-      return { getClass, getStyle };
-    },
-  });
-</script>

+ 1 - 0
src/components/SimpleMenu/index.ts

@@ -0,0 +1 @@
+export { default as SimpleMenu } from './src/SimpleMenu.vue';

+ 135 - 0
src/components/SimpleMenu/src/SimpleMenu.vue

@@ -0,0 +1,135 @@
+<template>
+  <Menu
+    v-bind="getBindValues"
+    @select="handleSelect"
+    :activeName="activeName"
+    :openNames="openNames"
+    :class="prefixCls"
+    :activeSubMenuNames="activeSubMenuNames"
+  >
+    <template v-for="item in items" :key="item.path">
+      <SimpleSubMenu
+        :item="item"
+        :parent="true"
+        :collapsedShowTitle="collapsedShowTitle"
+        :collapse="collapse"
+      />
+    </template>
+  </Menu>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { MenuState } from './types';
+  import type { Menu as MenuType } from '/@/router/types';
+
+  import { defineComponent, computed, ref, unref, reactive, toRefs, watch } from 'vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  import Menu from './components/Menu.vue';
+  import SimpleSubMenu from './SimpleSubMenu.vue';
+  import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
+  import { propTypes } from '/@/utils/propTypes';
+  import { REDIRECT_NAME } from '/@/router/constant';
+  import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
+  import { isFunction } from '/@/utils/is';
+
+  import { useOpenKeys } from './useOpenKeys';
+  export default defineComponent({
+    name: 'SimpleMenu',
+    inheritAttrs: false,
+    components: {
+      Menu,
+      SimpleSubMenu,
+    },
+    props: {
+      items: {
+        type: Array as PropType<MenuType[]>,
+        default: () => [],
+      },
+      collapse: propTypes.bool,
+      mixSider: propTypes.bool,
+      theme: propTypes.string,
+      accordion: propTypes.bool.def(true),
+      collapsedShowTitle: propTypes.bool,
+      beforeClickFn: {
+        type: Function as PropType<(key: string) => Promise<boolean>>,
+      },
+    },
+    setup(props, { attrs, emit }) {
+      const currentActiveMenu = ref('');
+      const isClickGo = ref(false);
+
+      const menuState = reactive<MenuState>({
+        activeName: '',
+        openNames: [],
+        activeSubMenuNames: [],
+      });
+
+      const { currentRoute } = useRouter();
+      const { prefixCls } = useDesign('simple-menu');
+      const { items, accordion, mixSider } = toRefs(props);
+      const { setOpenKeys } = useOpenKeys(menuState, items, accordion, mixSider);
+
+      const getBindValues = computed(() => ({ ...attrs, ...props }));
+
+      watch(
+        () => props.collapse,
+        (collapse) => {
+          if (collapse) {
+            menuState.openNames = [];
+          } else {
+            setOpenKeys(currentRoute.value.path);
+          }
+        },
+        { immediate: true }
+      );
+
+      listenerLastChangeTab((route) => {
+        if (route.name === REDIRECT_NAME) return;
+
+        currentActiveMenu.value = route.meta?.currentActiveMenu;
+        handleMenuChange(route);
+
+        if (unref(currentActiveMenu)) {
+          menuState.activeName = unref(currentActiveMenu);
+          setOpenKeys(unref(currentActiveMenu));
+        }
+      });
+
+      async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
+        if (unref(isClickGo)) {
+          isClickGo.value = false;
+          return;
+        }
+        const path = (route || unref(currentRoute)).path;
+        menuState.activeName = path;
+
+        setOpenKeys(path);
+        // if (unref(currentActiveMenu)) return;
+      }
+
+      async function handleSelect(key: string) {
+        const { beforeClickFn } = props;
+        if (beforeClickFn && isFunction(beforeClickFn)) {
+          const flag = await beforeClickFn(key);
+          if (!flag) return;
+        }
+        emit('menuClick', key);
+
+        isClickGo.value = true;
+        setOpenKeys(key);
+        menuState.activeName = key;
+      }
+
+      return {
+        prefixCls,
+        getBindValues,
+        handleSelect,
+        ...toRefs(menuState),
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @import './index.less';
+</style>

+ 70 - 0
src/components/SimpleMenu/src/SimpleMenuTag.vue

@@ -0,0 +1,70 @@
+<template>
+  <span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span>
+</template>
+<script lang="ts">
+  import type { Menu } from '/@/router/types';
+  import type { PropType } from 'vue';
+
+  import { defineComponent, computed } from 'vue';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  export default defineComponent({
+    name: 'SimpleMenuTag',
+    props: {
+      item: {
+        type: Object as PropType<Menu>,
+        default: {},
+      },
+      collapseParent: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('simple-menu');
+
+      const getShowTag = computed(() => {
+        const { item } = props;
+
+        if (!item) return false;
+
+        const { tag } = item;
+        if (!tag) return false;
+
+        const { dot, content } = tag;
+        if (!dot && !content) return false;
+        return true;
+      });
+
+      const getContent = computed(() => {
+        if (!getShowTag.value) return '';
+        const { item, collapseParent } = props;
+        const { tag } = item;
+        const { dot, content } = tag!;
+        return dot || collapseParent ? '' : content;
+      });
+
+      const getTagClass = computed(() => {
+        const { item, collapseParent } = props;
+        const { tag = {} } = item || {};
+        const { dot, type = 'error' } = tag;
+        const tagCls = `${prefixCls}-tag`;
+        return [
+          tagCls,
+
+          [`${tagCls}--${type}`],
+          {
+            [`${tagCls}--collapse`]: collapseParent,
+            [`${tagCls}--dot`]: dot,
+          },
+        ];
+      });
+      return {
+        getTagClass,
+        getShowTag,
+        getContent,
+      };
+    },
+  });
+</script>

+ 115 - 0
src/components/SimpleMenu/src/SimpleSubMenu.vue

@@ -0,0 +1,115 @@
+<template>
+  <MenuItem
+    :name="item.path"
+    v-if="!menuHasChildren(item) && getShowMenu"
+    v-bind="$props"
+    :class="getLevelClass"
+  >
+    <Icon v-if="getIcon" :icon="getIcon" :size="16" />
+    <div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-1 collapse-title">
+      {{ getI18nName }}
+    </div>
+    <template #title>
+      <span :class="['ml-2']">
+        {{ getI18nName }}
+      </span>
+      <SimpleMenuTag :item="item" :collapseParent="getIsCollapseParent" />
+    </template>
+  </MenuItem>
+  <SubMenu
+    :name="item.path"
+    v-if="menuHasChildren(item) && getShowMenu"
+    :class="[getLevelClass, theme]"
+    :collapsedShowTitle="collapsedShowTitle"
+  >
+    <template #title>
+      <Icon v-if="getIcon" :icon="getIcon" :size="16" />
+
+      <div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-2 collapse-title">
+        {{ getI18nName }}
+      </div>
+
+      <span v-show="getShowSubTitle" :class="['ml-2', `${prefixCls}-sub-title`]">
+        {{ getI18nName }}
+      </span>
+      <SimpleMenuTag :item="item" :collapseParent="!!collapse && !!parent" />
+    </template>
+    <template v-for="childrenItem in item.children || []" :key="childrenItem.path">
+      <SimpleSubMenu v-bind="$props" :item="childrenItem" :parent="false" />
+    </template>
+  </SubMenu>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { Menu } from '/@/router/types';
+
+  import { defineComponent, computed } from 'vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import Icon from '/@/components/Icon/index';
+
+  import MenuItem from './components/MenuItem.vue';
+  import SubMenu from './components/SubMenuItem.vue';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+  const { t } = useI18n();
+
+  export default defineComponent({
+    name: 'SimpleSubMenu',
+    components: {
+      SubMenu,
+      MenuItem,
+      SimpleMenuTag: createAsyncComponent(() => import('./SimpleMenuTag.vue')),
+      Icon,
+    },
+    props: {
+      item: {
+        type: Object as PropType<Menu>,
+        default: {},
+      },
+      parent: propTypes.bool,
+      collapsedShowTitle: propTypes.bool,
+      collapse: propTypes.bool,
+      theme: propTypes.oneOf(['dark', 'light']),
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('simple-menu');
+
+      const getShowMenu = computed(() => {
+        return !props.item.meta?.hideMenu;
+      });
+
+      const getIcon = computed(() => props.item?.icon);
+      const getI18nName = computed(() => t(props.item?.name));
+      const getShowSubTitle = computed(() => !props.collapse || !props.parent);
+      const getIsCollapseParent = computed(() => !!props.collapse && !!props.parent);
+      const getLevelClass = computed(() => {
+        return [
+          {
+            [`${prefixCls}__parent`]: props.parent,
+            [`${prefixCls}__children`]: !props.parent,
+          },
+        ];
+      });
+
+      function menuHasChildren(menuTreeItem: Menu): boolean {
+        return (
+          Reflect.has(menuTreeItem, 'children') &&
+          !!menuTreeItem.children &&
+          menuTreeItem.children.length > 0
+        );
+      }
+
+      return {
+        prefixCls,
+        menuHasChildren,
+        getShowMenu,
+        getIcon,
+        getI18nName,
+        getShowSubTitle,
+        getLevelClass,
+        getIsCollapseParent,
+      };
+    },
+  });
+</script>

+ 149 - 0
src/components/SimpleMenu/src/components/Menu.vue

@@ -0,0 +1,149 @@
+<template>
+  <ul :class="getClass">
+    <slot></slot>
+  </ul>
+</template>
+
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { SubMenuProvider } from './types';
+  import {
+    defineComponent,
+    ref,
+    computed,
+    onMounted,
+    watchEffect,
+    watch,
+    nextTick,
+    getCurrentInstance,
+    provide,
+  } from 'vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { propTypes } from '/@/utils/propTypes';
+  import { createSimpleRootMenuContext } from './useSimpleMenuContext';
+  import Mitt from '/@/utils/mitt';
+  import { isString } from '/@/utils/is';
+  export default defineComponent({
+    name: 'Menu',
+    props: {
+      theme: propTypes.oneOf(['light', 'dark']).def('light'),
+      activeName: propTypes.oneOfType([propTypes.string, propTypes.number]),
+      openNames: {
+        type: Array as PropType<string[]>,
+        default: [],
+      },
+      accordion: propTypes.bool.def(true),
+      width: propTypes.string.def('100%'),
+      collapsedWidth: propTypes.string.def('48px'),
+      indentSize: propTypes.number.def(16),
+      collapse: propTypes.bool.def(true),
+      activeSubMenuNames: {
+        type: Array as PropType<(string | number)[]>,
+        default: [],
+      },
+    },
+    emits: ['select', 'open-change'],
+    setup(props, { emit }) {
+      const rootMenuEmitter = new Mitt();
+      const instance = getCurrentInstance();
+
+      const currentActiveName = ref<string | number>('');
+      const openedNames = ref<string[]>([]);
+
+      const { prefixCls } = useDesign('menu');
+
+      const isRemoveAllPopup = ref(false);
+
+      createSimpleRootMenuContext({
+        rootMenuEmitter: rootMenuEmitter,
+        activeName: currentActiveName,
+      });
+
+      const getClass = computed(() => {
+        const { theme } = props;
+        return [
+          prefixCls,
+          `${prefixCls}-${theme}`,
+          `${prefixCls}-vertical`,
+          {
+            [`${prefixCls}-collapse`]: props.collapse,
+          },
+        ];
+      });
+
+      watchEffect(() => {
+        openedNames.value = props.openNames;
+      });
+
+      watchEffect(() => {
+        if (props.activeName) {
+          currentActiveName.value = props.activeName;
+        }
+      });
+
+      watch(
+        () => props.openNames,
+        () => {
+          nextTick(() => {
+            updateOpened();
+          });
+        }
+      );
+
+      function updateOpened() {
+        rootMenuEmitter.emit('on-update-opened', openedNames.value);
+      }
+
+      function addSubMenu(name: string) {
+        if (openedNames.value.includes(name)) return;
+        openedNames.value.push(name);
+        updateOpened();
+      }
+
+      function removeSubMenu(name: string) {
+        openedNames.value = openedNames.value.filter((item) => item !== name);
+        updateOpened();
+      }
+
+      function removeAll() {
+        openedNames.value = [];
+        updateOpened();
+      }
+
+      function sliceIndex(index: number) {
+        if (index === -1) return;
+        openedNames.value = openedNames.value.slice(0, index + 1);
+        updateOpened();
+      }
+
+      provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
+        addSubMenu,
+        removeSubMenu,
+        getOpenNames: () => openedNames.value,
+        removeAll,
+        isRemoveAllPopup,
+        sliceIndex,
+        level: 0,
+        props,
+      });
+
+      onMounted(() => {
+        openedNames.value = !props.collapse ? [...props.openNames] : [];
+        updateOpened();
+        rootMenuEmitter.on('on-menu-item-select', (name: string) => {
+          currentActiveName.value = name;
+
+          nextTick(() => {
+            props.collapse && removeAll();
+          });
+          emit('select', name);
+        });
+      });
+
+      return { getClass, openedNames };
+    },
+  });
+</script>
+<style lang="less">
+  @import './menu.less';
+</style>

+ 78 - 0
src/components/SimpleMenu/src/components/MenuCollapseTransition.vue

@@ -0,0 +1,78 @@
+<template>
+  <transition mode="out-in" v-on="on">
+    <slot></slot>
+  </transition>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { addClass, removeClass } from '/@/utils/domUtils';
+
+  export default defineComponent({
+    name: 'MenuCollapseTransition',
+    setup() {
+      return {
+        on: {
+          beforeEnter(el: any) {
+            addClass(el, 'collapse-transition');
+            if (!el.dataset) el.dataset = {};
+
+            el.dataset.oldPaddingTop = el.style.paddingTop;
+            el.dataset.oldPaddingBottom = el.style.paddingBottom;
+
+            el.style.height = '0';
+            el.style.paddingTop = 0;
+            el.style.paddingBottom = 0;
+          },
+
+          enter(el: any) {
+            el.dataset.oldOverflow = el.style.overflow;
+            if (el.scrollHeight !== 0) {
+              el.style.height = el.scrollHeight + 'px';
+              el.style.paddingTop = el.dataset.oldPaddingTop;
+              el.style.paddingBottom = el.dataset.oldPaddingBottom;
+            } else {
+              el.style.height = '';
+              el.style.paddingTop = el.dataset.oldPaddingTop;
+              el.style.paddingBottom = el.dataset.oldPaddingBottom;
+            }
+
+            el.style.overflow = 'hidden';
+          },
+
+          afterEnter(el: any) {
+            removeClass(el, 'collapse-transition');
+            el.style.height = '';
+            el.style.overflow = el.dataset.oldOverflow;
+          },
+
+          beforeLeave(el: any) {
+            if (!el.dataset) el.dataset = {};
+            el.dataset.oldPaddingTop = el.style.paddingTop;
+            el.dataset.oldPaddingBottom = el.style.paddingBottom;
+            el.dataset.oldOverflow = el.style.overflow;
+
+            el.style.height = el.scrollHeight + 'px';
+            el.style.overflow = 'hidden';
+          },
+
+          leave(el: any) {
+            if (el.scrollHeight !== 0) {
+              addClass(el, 'collapse-transition');
+              el.style.height = 0;
+              el.style.paddingTop = 0;
+              el.style.paddingBottom = 0;
+            }
+          },
+
+          afterLeave(el: any) {
+            removeClass(el, 'collapse-transition');
+            el.style.height = '';
+            el.style.overflow = el.dataset.oldOverflow;
+            el.style.paddingTop = el.dataset.oldPaddingTop;
+            el.style.paddingBottom = el.dataset.oldPaddingBottom;
+          },
+        },
+      };
+    },
+  });
+</script>

+ 103 - 0
src/components/SimpleMenu/src/components/MenuItem.vue

@@ -0,0 +1,103 @@
+<template>
+  <li :class="getClass" @click.stop="handleClickItem" :style="getCollapse ? {} : getItemStyle">
+    <Tooltip placement="right" v-if="showTooptip">
+      <template #title>
+        <slot name="title"></slot>
+      </template>
+      <div :class="`${prefixCls}-tooltip`">
+        <slot />
+      </div>
+    </Tooltip>
+
+    <template v-else>
+      <slot></slot>
+      <slot name="title"></slot>
+    </template>
+  </li>
+</template>
+
+<script lang="ts">
+  import { PropType } from 'vue';
+  import { defineComponent, ref, computed, unref, getCurrentInstance, watch } from 'vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useMenuItem } from './useMenu';
+  import { Tooltip } from 'ant-design-vue';
+  import { useSimpleRootMenuContext } from './useSimpleMenuContext';
+  export default defineComponent({
+    name: 'MenuItem',
+    components: { Tooltip },
+    props: {
+      name: {
+        type: [String, Number] as PropType<string | number>,
+        required: true,
+      },
+      disabled: propTypes.bool,
+    },
+    setup(props, { slots }) {
+      const instance = getCurrentInstance();
+
+      const active = ref(false);
+
+      const { getItemStyle, getParentList, getParentMenu, getParentRootMenu } = useMenuItem(
+        instance
+      );
+
+      const { prefixCls } = useDesign('menu');
+
+      const { rootMenuEmitter, activeName } = useSimpleRootMenuContext();
+
+      const getClass = computed(() => {
+        return [
+          `${prefixCls}-item`,
+          {
+            [`${prefixCls}-item-active`]: unref(active),
+            [`${prefixCls}-item-selected`]: unref(active),
+            [`${prefixCls}-item-disabled`]: !!props.disabled,
+          },
+        ];
+      });
+
+      const getCollapse = computed(() => unref(getParentRootMenu)?.props.collapse);
+
+      const showTooptip = computed(() => {
+        return unref(getParentMenu)?.type.name === 'Menu' && unref(getCollapse) && slots.title;
+      });
+
+      function handleClickItem() {
+        const { disabled } = props;
+        if (disabled) return;
+
+        rootMenuEmitter.emit('on-menu-item-select', props.name);
+        if (unref(getCollapse)) return;
+        const { uidList } = getParentList();
+        rootMenuEmitter.emit('on-update-opened', {
+          opend: false,
+          parent: instance?.parent,
+          uidList: uidList,
+        });
+      }
+      watch(
+        () => activeName.value,
+        (name: string) => {
+          if (name === props.name) {
+            const { list, uidList } = getParentList();
+            active.value = true;
+            list.forEach((item) => {
+              if (item.proxy) {
+                (item.proxy as any).active = true;
+              }
+            });
+
+            rootMenuEmitter.emit('on-update-active-name:submenu', uidList);
+          } else {
+            active.value = false;
+          }
+        },
+        { immediate: true }
+      );
+
+      return { getClass, prefixCls, getItemStyle, getCollapse, handleClickItem, showTooptip };
+    },
+  });
+</script>

+ 329 - 0
src/components/SimpleMenu/src/components/SubMenuItem.vue

@@ -0,0 +1,329 @@
+<template>
+  <li :class="getClass">
+    <template v-if="!getCollapse">
+      <div :class="`${prefixCls}-submenu-title`" @click.stop="handleClick" :style="getItemStyle">
+        <slot name="title"></slot>
+        <Icon
+          icon="eva:arrow-ios-downward-outline"
+          :size="14"
+          :class="`${prefixCls}-submenu-title-icon`"
+        />
+      </div>
+      <MenuCollapseTransition>
+        <ul :class="prefixCls" v-show="opened">
+          <slot></slot>
+        </ul>
+      </MenuCollapseTransition>
+    </template>
+
+    <Popover
+      placement="right"
+      :overlayClassName="`${prefixCls}-menu-popover`"
+      v-else
+      :visible="getIsOpend"
+      @visibleChange="handleVisibleChange"
+      :overlayStyle="getOverlayStyle"
+      :align="{ offset: [0, 0] }"
+    >
+      <div :class="getSubClass" v-bind="getEvents(false)">
+        <div
+          :class="[
+            {
+              [`${prefixCls}-submenu-popup`]: !getParentSubMenu,
+              [`${prefixCls}-submenu-collapsed-show-tit`]: collapsedShowTitle,
+            },
+          ]"
+        >
+          <slot name="title"></slot>
+        </div>
+        <Icon
+          v-if="getParentSubMenu"
+          icon="eva:arrow-ios-downward-outline"
+          :size="14"
+          :class="`${prefixCls}-submenu-title-icon`"
+        />
+      </div>
+      <template #content v-show="opened">
+        <div v-bind="getEvents(true)">
+          <ul :class="[prefixCls, `${prefixCls}-${getTheme}`, `${prefixCls}-popup`]">
+            <slot></slot>
+          </ul>
+        </div>
+      </template>
+    </Popover>
+  </li>
+</template>
+
+<script lang="ts">
+  import type { CSSProperties, PropType } from 'vue';
+  import type { SubMenuProvider } from './types';
+  import {
+    defineComponent,
+    computed,
+    unref,
+    getCurrentInstance,
+    toRefs,
+    reactive,
+    provide,
+    onBeforeMount,
+    inject,
+  } from 'vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useMenuItem } from './useMenu';
+  import { useSimpleRootMenuContext } from './useSimpleMenuContext';
+  import MenuCollapseTransition from './MenuCollapseTransition.vue';
+  import Icon from '/@/components/Icon';
+  import { Popover } from 'ant-design-vue';
+  import { isBoolean, isObject } from '/@/utils/is';
+  import Mitt from '/@/utils/mitt';
+
+  const DELAY = 200;
+  export default defineComponent({
+    name: 'SubMenu',
+    components: {
+      Icon,
+      MenuCollapseTransition,
+      Popover,
+    },
+    props: {
+      name: {
+        type: [String, Number] as PropType<string | number>,
+        required: true,
+      },
+      disabled: propTypes.bool,
+      collapsedShowTitle: propTypes.bool,
+    },
+    setup(props) {
+      const instance = getCurrentInstance();
+
+      const state = reactive({
+        active: false,
+        opened: false,
+      });
+
+      const data = reactive({
+        timeout: null as TimeoutHandle | null,
+        mouseInChild: false,
+        isChild: false,
+      });
+
+      const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } = useMenuItem(
+        instance
+      );
+
+      const { prefixCls } = useDesign('menu');
+
+      const subMenuEmitter = new Mitt();
+
+      const { rootMenuEmitter } = useSimpleRootMenuContext();
+
+      const {
+        addSubMenu: parentAddSubmenu,
+        removeSubMenu: parentRemoveSubmenu,
+        removeAll: parentRemoveAll,
+        getOpenNames: parentGetOpenNames,
+        isRemoveAllPopup,
+        sliceIndex,
+        level,
+        props: rootProps,
+        handleMouseleave: parentHandleMouseleave,
+      } = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!;
+
+      const getClass = computed(() => {
+        return [
+          `${prefixCls}-submenu`,
+          {
+            [`${prefixCls}-item-active`]: state.active,
+            [`${prefixCls}-opened`]: state.opened,
+            [`${prefixCls}-submenu-disabled`]: props.disabled,
+            [`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu),
+            [`${prefixCls}-child-item-active`]: state.active,
+          },
+        ];
+      });
+
+      const getAccordion = computed(() => rootProps.accordion);
+      const getCollapse = computed(() => rootProps.collapse);
+      const getTheme = computed(() => rootProps.theme);
+
+      const getOverlayStyle = computed(
+        (): CSSProperties => {
+          return {
+            minWidth: '200px',
+          };
+        }
+      );
+
+      const getIsOpend = computed(() => {
+        const name = props.name;
+        if (unref(getCollapse)) {
+          return parentGetOpenNames().includes(name);
+        }
+        return state.opened;
+      });
+
+      const getSubClass = computed(() => {
+        const isActive = rootProps.activeSubMenuNames.includes(props.name);
+        return [
+          `${prefixCls}-submenu-title`,
+          {
+            [`${prefixCls}-submenu-active`]: isActive,
+            [`${prefixCls}-submenu-active-border`]: isActive && level === 0,
+            [`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0,
+          },
+        ];
+      });
+
+      function getEvents(deep: boolean) {
+        if (!unref(getCollapse)) {
+          return {};
+        }
+        return {
+          onMouseenter: handleMouseenter,
+          onMouseleave: () => handleMouseleave(deep),
+        };
+      }
+
+      function handleClick() {
+        const { disabled } = props;
+        if (disabled || unref(getCollapse)) return;
+        const opened = state.opened;
+        if (unref(getAccordion)) {
+          const { uidList } = getParentList();
+          rootMenuEmitter.emit('on-update-opened', {
+            opend: false,
+            parent: instance?.parent,
+            uidList: uidList,
+          });
+        }
+        state.opened = !opened;
+      }
+
+      function handleMouseenter() {
+        const disabled = props.disabled;
+        if (disabled) return;
+
+        subMenuEmitter.emit('submenu:mouse-enter-child');
+
+        const index = parentGetOpenNames().findIndex((item) => item === props.name);
+
+        sliceIndex(index);
+
+        const isRoot = level === 0 && parentGetOpenNames().length === 2;
+        if (isRoot) {
+          parentRemoveAll();
+        }
+        data.isChild = parentGetOpenNames().includes(props.name);
+        clearTimeout(data.timeout!);
+        data.timeout = setTimeout(() => {
+          parentAddSubmenu(props.name);
+        }, DELAY);
+      }
+
+      function handleMouseleave(deepDispatch = false) {
+        const parentName = getParentMenu.value?.props.name;
+        if (!parentName) {
+          isRemoveAllPopup.value = true;
+        }
+
+        if (parentGetOpenNames().slice(-1)[0] === props.name) {
+          data.isChild = false;
+        }
+
+        subMenuEmitter.emit('submenu:mouse-leave-child');
+        if (data.timeout) {
+          clearTimeout(data.timeout!);
+          data.timeout = setTimeout(() => {
+            if (isRemoveAllPopup.value) {
+              parentRemoveAll();
+            } else if (!data.mouseInChild) {
+              parentRemoveSubmenu(props.name);
+            }
+          }, DELAY);
+        }
+        if (deepDispatch) {
+          if (getParentSubMenu.value) {
+            parentHandleMouseleave?.(true);
+          }
+        }
+      }
+
+      onBeforeMount(() => {
+        subMenuEmitter.on('submenu:mouse-enter-child', () => {
+          data.mouseInChild = true;
+          isRemoveAllPopup.value = false;
+          clearTimeout(data.timeout!);
+        });
+        subMenuEmitter.on('submenu:mouse-leave-child', () => {
+          if (data.isChild) return;
+          data.mouseInChild = false;
+          clearTimeout(data.timeout!);
+        });
+
+        rootMenuEmitter.on(
+          'on-update-opened',
+          (data: boolean | (string | number)[] | Recordable) => {
+            if (unref(getCollapse)) return;
+            if (isBoolean(data)) {
+              state.opened = data;
+              return;
+            }
+
+            if (isObject(data)) {
+              const { opend, parent, uidList } = data as Recordable;
+              if (parent === instance?.parent) {
+                state.opened = opend;
+              } else if (!uidList.includes(instance?.uid)) {
+                state.opened = false;
+              }
+              return;
+            }
+
+            if (props.name && Array.isArray(data)) {
+              state.opened = (data as (string | number)[]).includes(props.name);
+            }
+          }
+        );
+
+        rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => {
+          state.active = data.includes(instance?.uid!);
+        });
+      });
+
+      function handleVisibleChange(visible: boolean) {
+        state.opened = visible;
+      }
+
+      // provide
+      provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
+        addSubMenu: parentAddSubmenu,
+        removeSubMenu: parentRemoveSubmenu,
+        getOpenNames: parentGetOpenNames,
+        removeAll: parentRemoveAll,
+        isRemoveAllPopup,
+        sliceIndex,
+        level: level + 1,
+        handleMouseleave,
+        props: rootProps,
+      });
+
+      return {
+        getClass,
+        prefixCls,
+        getCollapse,
+        getItemStyle,
+        handleClick,
+        handleVisibleChange,
+        getParentSubMenu,
+        getOverlayStyle,
+        getTheme,
+        getIsOpend,
+        getEvents,
+        getSubClass,
+        ...toRefs(state),
+        ...toRefs(data),
+      };
+    },
+  });
+</script>

+ 332 - 0
src/components/SimpleMenu/src/components/menu.less

@@ -0,0 +1,332 @@
+@menu-prefix-cls: ~'@{namespace}-menu';
+@menu-popup-prefix-cls: ~'@{namespace}-menu-popup';
+@submenu-popup-prefix-cls: ~'@{namespace}-menu-submenu-popup';
+
+// @menu-dark: #191a23;
+// @menu-dark-active-bg: #101117;
+@transition-time: 0.2s;
+@menu-dark-subsidiary-color: rgba(255, 255, 255, 0.7);
+
+.light-border {
+  &::after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    display: block;
+    width: 2px;
+    background: @primary-color;
+    content: '';
+  }
+}
+
+.@{menu-prefix-cls}-menu-popover {
+  .ant-popover-arrow {
+    display: none;
+  }
+
+  .ant-popover-inner-content {
+    padding: 0;
+  }
+
+  .@{menu-prefix-cls} {
+    &-opened > * > &-submenu-title-icon {
+      transform: translateY(-50%) rotate(90deg) !important;
+    }
+
+    &-item,
+    &-submenu-title {
+      position: relative;
+      z-index: 1;
+      padding: 12px 20px;
+      color: @menu-dark-subsidiary-color;
+      cursor: pointer;
+      transition: all @transition-time @ease-in-out;
+
+      // &:hover {
+      //   color: @primary-color;
+      // }
+
+      &-icon {
+        position: absolute;
+        top: 50%;
+        right: 18px;
+        transform: translateY(-50%) rotate(-90deg);
+        transition: transform @transition-time @ease-in-out;
+      }
+    }
+
+    &-dark {
+      .@{menu-prefix-cls}-item,
+      .@{menu-prefix-cls}-submenu-title {
+        color: @menu-dark-subsidiary-color;
+        // background: @menu-dark-active-bg;
+
+        &:hover {
+          color: #fff;
+        }
+
+        &-selected {
+          color: #fff;
+          background: @primary-color !important;
+        }
+      }
+    }
+
+    &-light {
+      .@{menu-prefix-cls}-item,
+      .@{menu-prefix-cls}-submenu-title {
+        color: @text-color-base;
+
+        &:hover {
+          color: @primary-color;
+        }
+
+        &-selected {
+          z-index: 2;
+          color: @primary-color;
+          background: fade(@primary-color, 8);
+
+          .light-border();
+        }
+      }
+    }
+  }
+}
+
+.content();
+.content() {
+  .@{menu-prefix-cls} {
+    position: relative;
+    display: block;
+    width: 100%;
+    padding: 0;
+    margin: 0;
+    font-size: @font-size-base;
+    color: @text-color-base;
+    list-style: none;
+    outline: none;
+
+    .collapse-transition {
+      transition: @transition-time height ease-in-out, @transition-time padding-top ease-in-out,
+        @transition-time padding-bottom ease-in-out;
+    }
+
+    &-light {
+      background: #fff;
+
+      .@{menu-prefix-cls}-submenu-active {
+        color: @primary-color !important;
+        // background: fade(@primary-color, 8);
+
+        &-border {
+          .light-border();
+        }
+      }
+    }
+
+    &-dark {
+      // background: @menu-dark;
+
+      .@{menu-prefix-cls}-submenu-active {
+        color: #fff !important;
+      }
+    }
+
+    &-item {
+      position: relative;
+      z-index: 1;
+      display: flex;
+      font-size: @font-size-base;
+      color: inherit;
+      list-style: none;
+      cursor: pointer;
+      outline: none;
+      align-items: center;
+      // transition: all @transition-time @ease-in-out;
+
+      &:hover,
+      &:active {
+        color: inherit;
+      }
+    }
+
+    &-item > i {
+      margin-right: 6px;
+    }
+
+    &-submenu-title > i,
+    &-submenu-title span > i {
+      margin-right: 8px;
+    }
+
+    // vertical
+    &-vertical &-item,
+    &-vertical &-submenu-title {
+      position: relative;
+      z-index: 1;
+      padding: 12px 24px;
+      cursor: pointer;
+      // transition: all @transition-time @ease-in-out;
+
+      &:hover {
+        color: @primary-color;
+      }
+
+      .@{menu-prefix-cls}-tooltip {
+        width: calc(100% - 0px);
+        padding: 12px 0;
+        text-align: center;
+      }
+      .@{menu-prefix-cls}-submenu-popup {
+        padding: 12px 0;
+      }
+    }
+
+    &-vertical &-submenu-collapse {
+      .@{submenu-popup-prefix-cls} {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+      .@{menu-prefix-cls}-submenu-collapsed-show-tit {
+        flex-direction: column;
+      }
+    }
+
+    &-vertical&-collapse &-item,
+    &-vertical&-collapse &-submenu-title {
+      padding: 0 0;
+    }
+
+    &-vertical &-submenu-title-icon {
+      position: absolute;
+      top: 50%;
+      right: 18px;
+      transform: translateY(-50%);
+    }
+
+    &-submenu-title-icon {
+      transition: transform @transition-time @ease-in-out;
+    }
+
+    &-vertical &-opened > * > &-submenu-title-icon {
+      transform: translateY(-50%) rotate(180deg);
+    }
+
+    &-vertical &-submenu {
+      &-nested {
+        padding-left: 20px;
+      }
+      .@{menu-prefix-cls}-item {
+        padding-left: 43px;
+      }
+    }
+
+    &-light&-vertical &-item {
+      &-active:not(.@{menu-prefix-cls}-submenu) {
+        z-index: 2;
+        color: @primary-color;
+        background: fade(@primary-color, 8);
+
+        .light-border();
+      }
+      &-active.@{menu-prefix-cls}-submenu {
+        color: @primary-color;
+      }
+    }
+
+    &-light&-vertical&-collapse {
+      > li.@{menu-prefix-cls}-item-active,
+      .@{menu-prefix-cls}-submenu-active {
+        position: relative;
+        background: fade(@primary-color, 3);
+
+        &::after {
+          display: none;
+        }
+
+        &::before {
+          position: absolute;
+          top: 0;
+          left: 0;
+          width: 3px;
+          height: 100%;
+          background: @primary-color;
+          content: '';
+        }
+      }
+    }
+
+    &-dark&-vertical &-item,
+    &-dark&-vertical &-submenu-title {
+      color: @menu-dark-subsidiary-color;
+      &-active:not(.@{menu-prefix-cls}-submenu) {
+        color: #fff !important;
+        background: @primary-color !important;
+      }
+
+      &:hover {
+        color: #fff;
+        // background: @menu-dark;
+      }
+
+      // &-active:not(.@{menu-prefix-cls}-submenu) {
+      //   color: @primary-color;
+      // }
+    }
+
+    &-dark&-vertical&-collapse {
+      > li.@{menu-prefix-cls}-item-active,
+      .@{menu-prefix-cls}-submenu-active {
+        position: relative;
+        color: #fff !important;
+        background-color: @sider-dark-darken-bg-color !important;
+
+        &::before {
+          position: absolute;
+          top: 0;
+          left: 0;
+          width: 3px;
+          height: 100%;
+          background: @primary-color;
+          content: '';
+        }
+
+        .@{menu-prefix-cls}-submenu-collapse {
+          background-color: transparent;
+        }
+      }
+    }
+
+    &-dark&-vertical &-submenu &-item {
+      // &:hover {
+      //   color: #fff;
+      //   background: transparent;
+      // }
+
+      &-active,
+      &-active:hover {
+        color: #fff;
+        border-right: none;
+      }
+    }
+
+    &-dark&-vertical &-child-item-active > &-submenu-title {
+      color: #fff;
+    }
+
+    &-dark&-vertical &-opened {
+      // background: @menu-dark-active-bg;
+      // .@{menu-prefix-cls}-submenu-title {
+      //   background: @menu-dark;
+      // }
+
+      .@{menu-prefix-cls}-submenu-has-parent-submenu {
+        .@{menu-prefix-cls}-submenu-title {
+          background: transparent;
+        }
+      }
+    }
+  }
+}

+ 25 - 0
src/components/SimpleMenu/src/components/types.ts

@@ -0,0 +1,25 @@
+import { Ref } from 'vue';
+
+export interface Props {
+  theme: string;
+  activeName?: string | number | undefined;
+  openNames: string[];
+  accordion: boolean;
+  width: string;
+  collapsedWidth: string;
+  indentSize: number;
+  collapse: boolean;
+  activeSubMenuNames: (string | number)[];
+}
+
+export interface SubMenuProvider {
+  addSubMenu: (name: string | number, update?: boolean) => void;
+  removeSubMenu: (name: string | number, update?: boolean) => void;
+  removeAll: () => void;
+  sliceIndex: (index: number) => void;
+  isRemoveAllPopup: Ref<boolean>;
+  getOpenNames: () => (string | number)[];
+  handleMouseleave?: Fn;
+  level: number;
+  props: Props;
+}

+ 86 - 0
src/components/SimpleMenu/src/components/useMenu.ts

@@ -0,0 +1,86 @@
+import { computed, ComponentInternalInstance, unref } from 'vue';
+import type { CSSProperties } from 'vue';
+
+export function useMenuItem(instance: ComponentInternalInstance | null) {
+  const getParentMenu = computed(() => {
+    return findParentMenu(['Menu', 'SubMenu']);
+  });
+
+  const getParentRootMenu = computed(() => {
+    return findParentMenu(['Menu']);
+  });
+
+  const getParentSubMenu = computed(() => {
+    return findParentMenu(['SubMenu']);
+  });
+
+  const getItemStyle = computed(
+    (): CSSProperties => {
+      let parent = instance?.parent;
+      if (!parent) return {};
+      const indentSize = (unref(getParentRootMenu)?.props.indentSize as number) ?? 20;
+      let padding = indentSize;
+
+      if (unref(getParentRootMenu)?.props.collapse) {
+        padding = indentSize;
+      } else {
+        while (parent && parent.type.name !== 'Menu') {
+          if (parent.type.name === 'SubMenu') {
+            padding += indentSize;
+          }
+          parent = parent.parent;
+        }
+      }
+      return { paddingLeft: padding + 'px' };
+    }
+  );
+
+  function findParentMenu(name: string[]) {
+    let parent = instance?.parent;
+    if (!parent) return null;
+    while (parent && name.indexOf(parent.type.name!) === -1) {
+      parent = parent.parent;
+    }
+    return parent;
+  }
+
+  function getParentList() {
+    let parent = instance;
+    if (!parent)
+      return {
+        uidList: [],
+        list: [],
+      };
+    const ret = [];
+    while (parent && parent.type.name !== 'Menu') {
+      if (parent.type.name === 'SubMenu') {
+        ret.push(parent);
+      }
+      parent = parent.parent;
+    }
+    return {
+      uidList: ret.map((item) => item.uid),
+      list: ret,
+    };
+  }
+
+  function getParentInstance(instance: ComponentInternalInstance, name = 'SubMenu') {
+    let parent = instance.parent;
+    while (parent) {
+      if (parent.type.name !== name) {
+        return parent;
+      }
+      parent = parent.parent;
+    }
+    return parent;
+  }
+
+  return {
+    getParentMenu,
+    getParentInstance,
+    getParentRootMenu,
+    getParentList,
+    getParentSubMenu,
+    getItemStyle,
+  };
+}

+ 18 - 0
src/components/SimpleMenu/src/components/useSimpleMenuContext.ts

@@ -0,0 +1,18 @@
+import type { InjectionKey, Ref } from 'vue';
+import { createContext, useContext } from '/@/hooks/core/useContext';
+import Mitt from '/@/utils/mitt';
+
+export interface SimpleRootMenuContextProps {
+  rootMenuEmitter: Mitt;
+  activeName: Ref<string | number>;
+}
+
+const key: InjectionKey<SimpleRootMenuContextProps> = Symbol();
+
+export function createSimpleRootMenuContext(context: SimpleRootMenuContextProps) {
+  return createContext<SimpleRootMenuContextProps>(context, key, { readonly: false, native: true });
+}
+
+export function useSimpleRootMenuContext() {
+  return useContext<SimpleRootMenuContextProps>(key);
+}

+ 67 - 0
src/components/SimpleMenu/src/index.less

@@ -0,0 +1,67 @@
+@simple-prefix-cls: ~'@{namespace}-simple-menu';
+@prefix-cls: ~'@{namespace}-menu';
+
+.@{prefix-cls} {
+  &-dark&-vertical .@{simple-prefix-cls}__parent {
+    background-color: @sider-dark-bg-color;
+    > .@{prefix-cls}-submenu-title {
+      background-color: @sider-dark-bg-color;
+    }
+  }
+
+  &-dark&-vertical .@{simple-prefix-cls}__children,
+  &-dark&-popup .@{simple-prefix-cls}__children {
+    background-color: @sider-dark-lighten-1-bg-color;
+    > .@{prefix-cls}-submenu-title {
+      background-color: @sider-dark-lighten-1-bg-color;
+    }
+  }
+
+  .collapse-title {
+    font-size: 12px;
+  }
+}
+
+.@{simple-prefix-cls} {
+  &-tag {
+    position: absolute;
+    top: calc(50% - 10px);
+    right: 30px;
+    display: inline-block;
+    padding: 2px 3px;
+    margin-right: 4px;
+    font-size: 10px;
+    line-height: 14px;
+    color: #fff;
+    border-radius: 2px;
+
+    &--collapse {
+      top: 6px !important;
+      right: 2px;
+    }
+
+    &--dot {
+      top: calc(50% - 4px);
+      width: 6px;
+      height: 6px;
+      padding: 0;
+      border-radius: 50%;
+    }
+
+    &--primary {
+      background: @primary-color;
+    }
+
+    &--error {
+      background: @error-color;
+    }
+
+    &--success {
+      background: @success-color;
+    }
+
+    &--warn {
+      background: @warning-color;
+    }
+  }
+}

+ 5 - 0
src/components/SimpleMenu/src/types.ts

@@ -0,0 +1,5 @@
+export interface MenuState {
+  activeName: string;
+  openNames: string[];
+  activeSubMenuNames: string[];
+}

+ 45 - 0
src/components/SimpleMenu/src/useOpenKeys.ts

@@ -0,0 +1,45 @@
+import type { Menu as MenuType } from '/@/router/types';
+import type { MenuState } from './types';
+
+import { Ref, toRaw } from 'vue';
+
+import { unref } from 'vue';
+import { es6Unique } from '/@/utils';
+import { getAllParentPath } from '/@/router/helper/menuHelper';
+import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+
+export function useOpenKeys(
+  menuState: MenuState,
+  menus: Ref<MenuType[]>,
+  accordion: Ref<boolean>,
+  mixSider: Ref<boolean>
+  // mode: Ref<MenuModeEnum>,
+) {
+  async function setOpenKeys(path: string) {
+    // if (mode.value === MenuModeEnum.HORIZONTAL) {
+    //   return;
+    // }
+    const native = !mixSider.value;
+    useTimeoutFn(
+      () => {
+        const menuList = toRaw(menus.value);
+        if (menuList?.length === 0) {
+          menuState.activeSubMenuNames = [];
+          menuState.openNames = [];
+          return;
+        }
+        const keys = getAllParentPath(menuList, path);
+        if (!unref(accordion)) {
+          menuState.openNames = es6Unique([...menuState.openNames, ...keys]);
+        } else {
+          menuState.openNames = keys;
+        }
+        menuState.activeSubMenuNames = menuState.openNames;
+      },
+      16,
+      native
+    );
+  }
+
+  return { setOpenKeys };
+}

+ 1 - 1
src/components/Table/src/components/editable/EditableCell.vue

@@ -31,7 +31,7 @@
   import type { CSSProperties, PropType } from 'vue';
   import type { BasicColumn } from '../../types/table';
 
-  import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
+  import { defineComponent, ref, unref, nextTick, computed, watchEffect } from 'vue';
   import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
 
   import { useDesign } from '/@/hooks/web/useDesign';

+ 0 - 16
src/components/Table/src/hooks/useProvinceTable.ts

@@ -1,16 +0,0 @@
-import type { Ref } from 'vue';
-import type { TableActionType } from '../types/table';
-
-import { provide, inject } from 'vue';
-
-const key = Symbol('table');
-
-type Instance = TableActionType & { wrapRef: Ref<Nullable<HTMLElement>> };
-
-export function provideTable(instance: Instance) {
-  provide(key, instance);
-}
-
-export function injectTable(): Instance {
-  return inject(key) as Instance;
-}

+ 6 - 0
src/design/ant/index.less

@@ -22,6 +22,12 @@
   background: rgba(0, 0, 0, 0.3);
 }
 
+.ant-popover {
+  &-content {
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  }
+}
+
 // =================================
 // ==============descriptions=======
 // =================================

+ 0 - 2
src/design/color.less

@@ -9,7 +9,6 @@
   --sider-dark-darken-bg-color: #273352;
   --sider-dark-lighten-1-bg-color: #273352;
   --sider-dark-lighten-2-bg-color: #273352;
-  --sider-dark-lighten-3-bg-color: #273352;
 }
 
 @white: #fff;
@@ -88,7 +87,6 @@
 @sider-dark-darken-bg-color: var(--sider-dark-darken-bg-color);
 @sider-dark-lighten-1-bg-color: var(--sider-dark-lighten-1-bg-color);
 @sider-dark-lighten-2-bg-color: var(--sider-dark-lighten-2-bg-color);
-@sider-dark-lighten-3-bg-color: var(--sider-dark-lighten-3-bg-color);
 
 // trigger
 @trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2);

+ 6 - 3
src/hooks/setting/useMenuSetting.ts

@@ -78,9 +78,12 @@ const getIsMixMode = computed(() => {
 });
 
 const getRealWidth = computed(() => {
-  return unref(getCollapsed) && !unref(getMixSideFixed)
-    ? unref(getMiniWidthNumber)
-    : unref(getMenuWidth);
+  if (unref(getIsMixSidebar)) {
+    return unref(getCollapsed) && !unref(getMixSideFixed)
+      ? unref(getMiniWidthNumber)
+      : unref(getMenuWidth);
+  }
+  return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth);
 });
 
 const getMiniWidthNumber = computed(() => {

+ 1 - 1
src/layouts/default/header/index.vue

@@ -142,7 +142,7 @@
       });
 
       const getLogoWidth = computed(() => {
-        if (!unref(getIsMixMode)) {
+        if (!unref(getIsMixMode) || unref(getIsMobile)) {
           return {};
         }
         const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);

+ 14 - 2
src/layouts/default/menu/index.tsx

@@ -4,6 +4,7 @@ import type { PropType, CSSProperties } from 'vue';
 
 import { computed, defineComponent, unref, toRef } from 'vue';
 import { BasicMenu } from '/@/components/Menu';
+import { SimpleMenu } from '/@/components/SimpleMenu';
 import { AppLogo } from '/@/components/Application';
 
 import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
@@ -126,7 +127,18 @@ export default defineComponent({
     }
 
     function renderMenu() {
-      return (
+      const menus = unref(menusRef);
+      if (!menus || !menus.length) return null;
+      return !props.isHorizontal ? (
+        <SimpleMenu
+          items={menus}
+          theme={unref(getComputedMenuTheme)}
+          accordion={unref(getAccordion)}
+          collapse={unref(getCollapsed)}
+          collapsedShowTitle={unref(getCollapsedShowTitle)}
+          onMenuClick={handleMenuClick}
+        />
+      ) : (
         <BasicMenu
           beforeClickFn={beforeMenuClickFn}
           isHorizontal={props.isHorizontal}
@@ -135,7 +147,7 @@ export default defineComponent({
           showLogo={unref(getIsShowLogo)}
           mode={unref(getComputedMenuMode)}
           theme={unref(getComputedMenuTheme)}
-          items={unref(menusRef)}
+          items={menus}
           accordion={unref(getAccordion)}
           onMenuClick={handleMenuClick}
         />

+ 11 - 2
src/layouts/default/menu/useLayoutMenu.ts

@@ -40,7 +40,12 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
     async ([path]: [string, MenuSplitTyeEnum]) => {
       if (unref(splitNotLeft) || unref(getIsMobile)) return;
 
-      const parentPath = await getCurrentParentPath(path);
+      const { meta } = unref(currentRoute);
+      const currentActiveMenu = meta.currentActiveMenu;
+      let parentPath = await getCurrentParentPath(path);
+      if (!parentPath) {
+        parentPath = await getCurrentParentPath(currentActiveMenu);
+      }
       parentPath && throttleHandleSplitLeftMenu(parentPath);
     },
     {
@@ -67,11 +72,15 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
 
   // Handle left menu split
   async function handleSplitLeftMenu(parentPath: string) {
+    console.log('======================');
+    console.log(unref(getSplitLeft));
+    console.log('======================');
     if (unref(getSplitLeft) || unref(getIsMobile)) return;
 
     // spilt mode left
     const children = await getChildrenMenus(parentPath);
-    if (!children) {
+
+    if (!children || !children.length) {
       setMenuSetting({ hidden: true });
       menusRef.value = [];
       return;

+ 17 - 9
src/layouts/default/sider/MixSider.vue

@@ -61,9 +61,7 @@
         />
       </div>
       <ScrollContainer :class="`${prefixCls}-menu-list__content`">
-        <BasicMenu
-          :isHorizontal="false"
-          mode="inline"
+        <SimpleMenu
           :items="chilrenMenus"
           :theme="getMenuTheme"
           mixSider
@@ -85,7 +83,7 @@
 
   import { defineComponent, onMounted, ref, computed, unref } from 'vue';
 
-  import { BasicMenu, MenuTag } from '/@/components/Menu';
+  import { MenuTag } from '/@/components/Menu';
   import { ScrollContainer } from '/@/components/Container';
   import Icon from '/@/components/Icon';
   import { AppLogo } from '/@/components/Application';
@@ -103,13 +101,14 @@
   import clickOutside from '/@/directives/clickOutside';
   import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus';
   import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
+  import { SimpleMenu } from '/@/components/SimpleMenu';
 
   export default defineComponent({
     name: 'LayoutMixSider',
     components: {
       ScrollContainer,
       AppLogo,
-      BasicMenu,
+      SimpleMenu,
       MenuTag,
       Icon,
       Trigger,
@@ -335,6 +334,7 @@
 <style lang="less">
   @prefix-cls: ~'@{namespace}-layout-mix-sider';
   @tag-prefix-cls: ~'@{namespace}-basic-menu-item-tag';
+  @menu-prefix-cls: ~'@{namespace}-menu';
   @width: 80px;
   .@{prefix-cls} {
     position: fixed;
@@ -351,6 +351,10 @@
       right: 2px;
     }
 
+    .@{menu-prefix-cls} {
+      width: 100% !important;
+    }
+
     &-dom {
       height: 100%;
       overflow: hidden;
@@ -392,6 +396,10 @@
         }
       }
       .@{prefix-cls}-menu-list {
+        &__content {
+          box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
+        }
+
         &__title {
           .pushpin {
             color: rgba(0, 0, 0, 0.35);
@@ -578,10 +586,10 @@
 
     &-drag-bar {
       position: absolute;
-      top: 0;
-      right: -3px;
-      width: 3px;
-      height: 100%;
+      top: 50px;
+      right: -1px;
+      width: 1px;
+      height: calc(100% - 50px);
       cursor: ew-resize;
       background: #f8f8f9;
       border-top: none;

+ 5 - 0
src/locales/lang/en/component/drawer.ts

@@ -0,0 +1,5 @@
+export default {
+  loadingText: 'Loading...',
+  cancelText: 'Close',
+  okText: 'Confirm',
+};

+ 3 - 0
src/locales/lang/en/component/menu.ts

@@ -0,0 +1,3 @@
+export default {
+  search: 'Menu search',
+};

+ 4 - 0
src/locales/lang/en/component/modal.ts

@@ -0,0 +1,4 @@
+export default {
+  cancelText: 'Close',
+  okText: 'Confirm',
+};

+ 5 - 0
src/locales/lang/zh_CN/component/drawer.ts

@@ -0,0 +1,5 @@
+export default {
+  loadingText: '加载中...',
+  cancelText: '关闭',
+  okText: '确认',
+};

+ 3 - 0
src/locales/lang/zh_CN/component/menu.ts

@@ -0,0 +1,3 @@
+export default {
+  search: '菜单搜索',
+};

+ 4 - 0
src/locales/lang/zh_CN/component/modal.ts

@@ -0,0 +1,4 @@
+export default {
+  cancelText: '关闭',
+  okText: '确认',
+};

+ 1 - 1
src/logics/theme/index.ts

@@ -71,7 +71,7 @@ export function updateSidebarBgColor(color: string) {
 
   setCssVar(SIDER_DARK_BG_COLOR, color);
   setCssVar(SIDER_DARK_DARKEN_BG_COLOR, darken(color, 6));
-  setCssVar(SIDER_LIGHTEN_1_BG_COLOR, lighten(color, 4));
+  setCssVar(SIDER_LIGHTEN_1_BG_COLOR, lighten(color, 5));
   setCssVar(SIDER_LIGHTEN_2_BG_COLOR, lighten(color, 8));
 
   // only #ffffff is light

+ 2 - 0
src/router/guard/index.ts

@@ -8,6 +8,7 @@ import { createMessageGuard } from './messageGuard';
 import { createScrollGuard } from './scrollGuard';
 import { createHttpGuard } from './httpGuard';
 import { createPageGuard } from './pageGuard';
+import { createStateGuard } from './stateGuard';
 
 export function createGuard(router: Router) {
   createPageGuard(router);
@@ -18,4 +19,5 @@ export function createGuard(router: Router) {
   createTitleGuard(router);
   createProgressGuard(router);
   createPermissionGuard(router);
+  createStateGuard(router);
 }

+ 1 - 1
src/router/guard/stateGuard.ts

@@ -3,7 +3,7 @@ import { appStore } from '/@/store/modules/app';
 import { PageEnum } from '/@/enums/pageEnum';
 import { removeTabChangeListener } from '/@/logics/mitt/tabChange';
 
-export function createHttpGuard(router: Router) {
+export function createStateGuard(router: Router) {
   router.afterEach((to) => {
     // Just enter the login page and clear the authentication information
     if (to.path === PageEnum.BASE_LOGIN) {

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

@@ -54,7 +54,9 @@ export const getMenus = async (): Promise<Menu[]> => {
 // 获取当前路径的顶级路径
 export async function getCurrentParentPath(currentPath: string) {
   const menus = await getAsyncMenus();
+
   const allParentPath = await getAllParentPath(menus, currentPath);
+
   return allParentPath?.[0];
 }
 

+ 2 - 2
src/utils/mitt.ts

@@ -28,7 +28,7 @@ export default class Mitt {
    * @param {Function} handler Function to call in response to given event
    */
   on(type: string | Symbol, handler: Fn) {
-    const handlers = this.cache.get(type);
+    const handlers = this.cache?.get(type);
     const added = handlers && handlers.push(handler);
     if (!added) {
       this.cache.set(type, [handler]);
@@ -57,7 +57,7 @@ export default class Mitt {
    * @param {string|symbol} type The event type to invoke
    * @param {*} [evt] Any value (object is recommended and powerful), passed to each handler
    */
-  emit(type: string | Symbol, evt: any) {
+  emit(type: string | Symbol, evt?: any) {
     for (const handler of (this.cache.get(type) || []).slice()) handler(evt);
     for (const handler of (this.cache.get('*') || []).slice()) handler(type, evt);
   }

+ 1 - 0
vite.config.ts

@@ -89,6 +89,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
     optimizeDeps: {
       include: [
         '@ant-design/icons-vue',
+        'echarts/map/js/china',
         'ant-design-vue/es/locale/zh_CN',
         'moment/dist/locale/zh-cn',
         'ant-design-vue/es/locale/en_US',

+ 83 - 49
yarn.lock

@@ -1123,10 +1123,10 @@
   resolved "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.2.1.tgz#29a5a86bcfaa41555c8483a287294e520cc28cd6"
   integrity sha512-WmvsSfVKQx62vLbHXJvdh4PDjSK9YU6VW9ppXTlbjgDKCYtpy2sMWbK4i9OBdxY6RRwMMVctZhWo6Y5jfMRyTg==
 
-"@eslint/eslintrc@^0.2.2":
-  version "0.2.2"
-  resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76"
-  integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==
+"@eslint/eslintrc@^0.3.0":
+  version "0.3.0"
+  resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318"
+  integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==
   dependencies:
     ajv "^6.12.4"
     debug "^4.1.1"
@@ -1135,7 +1135,7 @@
     ignore "^4.0.6"
     import-fresh "^3.2.1"
     js-yaml "^3.13.1"
-    lodash "^4.17.19"
+    lodash "^4.17.20"
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
@@ -1184,10 +1184,10 @@
   dependencies:
     cross-fetch "^3.0.6"
 
-"@iconify/json@^1.1.285":
-  version "1.1.285"
-  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.285.tgz#2f1665c9f3ce4cd9eb2e0c980c0ac8955ce520fc"
-  integrity sha512-ABoWg/GibeN3hzTvvzd9oSmSo3V8Hyb3f0LMMUD195xlrd8083nBzFFhA12EfEMnxNsouj6ZtvlgIDnYWEXRow==
+"@iconify/json@^1.1.287":
+  version "1.1.287"
+  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.287.tgz#31fe253ce97fb2bf673a60c2467810a3f48a00c3"
+  integrity sha512-wvmQDpHqzbYZv2mDsdp1eXUN+ff53FjElT19uVxFRPOkY2kaIhs7dMPS/ZeDD38TE2eH1arTzZ2KhtB+Mxe8VQ==
 
 "@intlify/core-base@9.0.0-beta.16":
   version "9.0.0-beta.16"
@@ -1494,10 +1494,10 @@
   resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz#682477dbbbd07cd032731cb3b0e7eaee3d026b69"
   integrity sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==
 
-"@types/http-proxy@^1.17.4":
-  version "1.17.4"
-  resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
-  integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
+"@types/http-proxy@^1.17.5":
+  version "1.17.5"
+  resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d"
+  integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q==
   dependencies:
     "@types/node" "*"
 
@@ -1751,10 +1751,10 @@
     "@typescript-eslint/types" "4.13.0"
     eslint-visitor-keys "^2.0.0"
 
-"@vitejs/plugin-legacy@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.2.0.tgz#e6a2f7802f1a81c712f72656300fcdf7541eeab0"
-  integrity sha512-eoJi1M7Or16bkRjXFtdG39c8ElvbgxUxlXFo8GO2VmgOGO42r6Ku5MJD4ZkweIM7XGunyFvmEwTYgpUVC4PiPg==
+"@vitejs/plugin-legacy@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.2.1.tgz#783a72c56ce987f00caf334acc33195a0bbf6f24"
+  integrity sha512-bVOYH7WxffDSvfFfCGk/UYCzKw59n18fHGOV3VXRSQmeaBmbxuq0CRdAS3EtPvp74DjgA4GiZ+BsrQ0LyF0/yA==
   dependencies:
     "@babel/standalone" "^7.12.12"
     core-js "^3.8.2"
@@ -1772,10 +1772,10 @@
     "@vue/babel-plugin-jsx" "^1.0.1"
     hash-sum "^2.0.0"
 
-"@vitejs/plugin-vue@^1.0.5":
-  version "1.0.5"
-  resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.0.5.tgz#2639178e975bebc505e9be1c88d25faf9bc4dd06"
-  integrity sha512-Fq/Z1rTs7j3QhvmIjeIHqInw2YneXa8Td3z7cYQhyAZXF/WmGMegbapeBqGAoAcGSOfWpOO7Tr0c/T+Qke0O6Q==
+"@vitejs/plugin-vue@^1.0.6":
+  version "1.0.6"
+  resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.0.6.tgz#698afa5a77a6dcd22cf7757801f46a6f01cdbb53"
+  integrity sha512-cWJewtxnVVpjlhq6DoZ7VP7sF1jTZYVg66ehslZ0tJANWk1uRiCXdqD8yQ4npZ4XewDICQzK+c+9i3Xsubx59w==
 
 "@vue/babel-helper-vue-transform-on@^1.0.0":
   version "1.0.0"
@@ -2023,18 +2023,18 @@
     vscode-languageserver-textdocument "^1.0.1"
     vscode-uri "^2.1.2"
 
-"@vueuse/core@^4.0.5":
-  version "4.0.5"
-  resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.5.tgz#97bd5f24a28401598436629312eafe66ed0e1bed"
-  integrity sha512-Kfy5ys9o1XIY6NwX9O7iad4/FbHrcDuP/LtsgIFvl7XDQtbYArHu5ZSOQyBwqE32TdAqnFi5sYd4vjSvVvpD4A==
+"@vueuse/core@^4.0.8":
+  version "4.0.8"
+  resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.8.tgz#d5690154c147ae787bf5d67bf8fe3046dff96d85"
+  integrity sha512-wD0JJUXpRgRBPCnGsAqcVk9Zz545zOmIjGv/1Mlco3rVmal7LEZ3rJh8SnBelxuyVNvRwifkK1gtbT24jY6V8Q==
   dependencies:
-    "@vueuse/shared" "4.0.5"
+    "@vueuse/shared" "4.0.8"
     vue-demi latest
 
-"@vueuse/shared@4.0.5":
-  version "4.0.5"
-  resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.5.tgz#0610210da9a01843cdb3fa88c177b29b62738efc"
-  integrity sha512-PUSlwoSaerwHA1PPjBGnerXPIvAcVGoxcpjNdbHW44lPqoWskWl2CxG+l2Iz+Zf2iapCatp3ovXnMd16RRvQ1Q==
+"@vueuse/shared@4.0.8":
+  version "4.0.8"
+  resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.8.tgz#ba6c350b5f0ef12e2a603d956cc6d2809ff5be4f"
+  integrity sha512-euAfdZeFHGAyCBoy7izgufC/kTt+yEjuVjeCmfuDQNAj7QsdzEpRlyblD+EGifHbyGFx8F3Ql6/bQzdTdwRFHA==
   dependencies:
     vue-demi latest
 
@@ -2800,7 +2800,7 @@ commander@~2.17.1:
   resolved "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
   integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
 
-commitizen@^4.0.3, commitizen@^4.2.2:
+commitizen@^4.0.3:
   version "4.2.2"
   resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.2.tgz#1a93dd07208521ea1ebbf832593542dac714cc79"
   integrity sha512-uz+E6lGsDBDI2mYA4QfOxFeqdWUYwR1ky11YmLgg2BnEEP3YbeejpT4lxzGjkYqumnXr062qTOGavR9NtX/iwQ==
@@ -2820,6 +2820,26 @@ commitizen@^4.0.3, commitizen@^4.2.2:
     strip-bom "4.0.0"
     strip-json-comments "3.0.1"
 
+commitizen@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.3.tgz#088d0ef72500240d331b11e02e288223667c1475"
+  integrity sha512-pYlYEng7XMV2TW4xtjDKBGqeJ0Teq2zyRSx2S3Ml1XAplHSlJZK8vm1KdGclpMEZuGafbS5TeHXIVnHk8RWIzQ==
+  dependencies:
+    cachedir "2.2.0"
+    cz-conventional-changelog "3.2.0"
+    dedent "0.7.0"
+    detect-indent "6.0.0"
+    find-node-modules "2.0.0"
+    find-root "1.1.0"
+    fs-extra "8.1.0"
+    glob "7.1.4"
+    inquirer "6.5.2"
+    is-utf8 "^0.2.1"
+    lodash "^4.17.20"
+    minimist "1.2.5"
+    strip-bom "4.0.0"
+    strip-json-comments "3.0.1"
+
 common-tags@^1.8.0:
   version "1.8.0"
   resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
@@ -3159,6 +3179,20 @@ currently-unhandled@^0.4.1:
   dependencies:
     array-find-index "^1.0.1"
 
+cz-conventional-changelog@3.2.0:
+  version "3.2.0"
+  resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.2.0.tgz#6aef1f892d64113343d7e455529089ac9f20e477"
+  integrity sha512-yAYxeGpVi27hqIilG1nh4A9Bnx4J3Ov+eXy4koL3drrR+IO9GaWPsKjik20ht608Asqi8TQPf0mczhEeyAtMzg==
+  dependencies:
+    chalk "^2.4.1"
+    commitizen "^4.0.3"
+    conventional-commit-types "^3.0.0"
+    lodash.map "^4.5.1"
+    longest "^2.0.1"
+    word-wrap "^1.0.3"
+  optionalDependencies:
+    "@commitlint/load" ">6.1.1"
+
 cz-conventional-changelog@3.3.0:
   version "3.3.0"
   resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz#9246947c90404149b3fe2cf7ee91acad3b7d22d2"
@@ -3571,13 +3605,13 @@ eslint-visitor-keys@^2.0.0:
   resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
   integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
 
-eslint@^7.17.0:
-  version "7.17.0"
-  resolved "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz#4ccda5bf12572ad3bf760e6f195886f50569adb0"
-  integrity sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==
+eslint@^7.18.0:
+  version "7.18.0"
+  resolved "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67"
+  integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==
   dependencies:
     "@babel/code-frame" "^7.0.0"
-    "@eslint/eslintrc" "^0.2.2"
+    "@eslint/eslintrc" "^0.3.0"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
@@ -3601,7 +3635,7 @@ eslint@^7.17.0:
     js-yaml "^3.13.1"
     json-stable-stringify-without-jsonify "^1.0.1"
     levn "^0.4.1"
-    lodash "^4.17.19"
+    lodash "^4.17.20"
     minimatch "^3.0.4"
     natural-compare "^1.4.0"
     optionator "^0.9.1"
@@ -4423,10 +4457,10 @@ human-signals@^1.1.1:
   resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
   integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
 
-husky@^4.3.7:
-  version "4.3.7"
-  resolved "https://registry.npmjs.org/husky/-/husky-4.3.7.tgz#ca47bbe6213c1aa8b16bbd504530d9600de91e88"
-  integrity sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw==
+husky@^4.3.8:
+  version "4.3.8"
+  resolved "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d"
+  integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==
   dependencies:
     chalk "^4.0.0"
     ci-info "^2.0.0"
@@ -7840,20 +7874,20 @@ vite-plugin-purge-icons@^0.5.1:
     "@purge-icons/generated" "^0.5.1"
     rollup-plugin-purge-icons "^0.5.1"
 
-vite-plugin-pwa@^0.3.6:
-  version "0.3.6"
-  resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.3.6.tgz#a522af3fd5461991907f6829975b437f2c847339"
-  integrity sha512-GDgT8jFGHUz2j11I7Z0W+X5mnkaUoMVitJ/UjN/ezjy9HcXrvxaIVnhzMdESJSv+dxy4DD9ymD91cF9Ei6//cQ==
+vite-plugin-pwa@^0.3.8:
+  version "0.3.8"
+  resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.3.8.tgz#c98a683ddbbe87bd55db79acb6a5c849f2a29879"
+  integrity sha512-W5FBJeS3KjaCG1qu7LMTX9+E0u6qNHFk+hk917s4MnAlQ/XnBs30kgRXVBXtVAPhgvn8rqj2ww+2OYed+MKtIg==
   dependencies:
     debug "^4.3.2"
     fast-glob "^3.2.4"
     pretty-bytes "^5.5.0"
     workbox-build "^6.0.2"
 
-vite@^2.0.0-beta.27:
-  version "2.0.0-beta.27"
-  resolved "https://registry.npmjs.org/vite/-/vite-2.0.0-beta.27.tgz#a2e4b3a698e67c89fd963ff51ee5283ec564c65c"
-  integrity sha512-1fGPjSVE4MmCGVguFy7pPurCLnvHu4fJSzVjejd9GoFqCNie+JKCpe3KGsxIb9B8ot/aDd4ISCB0+fH1/01FUA==
+vite@^2.0.0-beta.30:
+  version "2.0.0-beta.30"
+  resolved "https://registry.npmjs.org/vite/-/vite-2.0.0-beta.30.tgz#d0c1056d1fb05c489614360f92363eebec41a6b4"
+  integrity sha512-wOeO64J3k4jGjCOkH/6RUcIyT/HOTaDZSiXE75aWYqV9hI7Q6uEeSXbAFtb9bG82RGLEWdsqtCvx5t7gaeqtsw==
   dependencies:
     esbuild "^0.8.26"
     postcss "^8.2.1"