Просмотр исходного кода

feat(layout): added setting. Used to fix the left mixed mode menu

vben 4 лет назад
Родитель
Сommit
97180e83f5

+ 2 - 0
CHANGELOG.zh_CN.md

@@ -3,6 +3,8 @@
 ### ✨ Features
 
 - 新增`mixSideTrigger`配置。用于配置左侧混合模式菜单打开方式。可选`hover`,默认`click`
+- 新增`mixSideFixed`配置。用于固定左侧混合模式菜单
+- modal 组件新增`height`和`min-height`属性
 
 ### 🐛 Bug Fixes
 

+ 1 - 1
package.json

@@ -7,7 +7,7 @@
     "build": "cross-env  vite build --mode=production && esno ./build/script/postBuild.ts",
     "build:site": "cross-env SITE=true npm run build ",
     "build:no-cache": "yarn  clean:cache && npm run build",
-    "typecheck": "typecheck .",
+    "typecheck": "vuedx-typecheck .",
     "report": "cross-env REPORT=true npm run build ",
     "preview": "npm run build && esno ./build/script/preview.ts",
     "preview:dist": "esno ./build/script/preview.ts",

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

@@ -125,15 +125,13 @@
         }
       });
 
-      watch(
-        () => props.items,
-        () => {
-          handleMenuChange();
-        }
-        // {
-        //   immediate: true,
-        // }
-      );
+      !props.mixSider &&
+        watch(
+          () => props.items,
+          () => {
+            handleMenuChange();
+          }
+        );
 
       async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) {
         const { beforeClickFn } = props;

+ 20 - 8
src/components/Menu/src/useOpenKeys.ts

@@ -8,6 +8,7 @@ import { unref } from 'vue';
 import { es6Unique } from '/@/utils';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
 import { getAllParentPath } from '/@/router/helper/menuHelper';
+import { useTimeoutFn } from '/@/hooks/core/useTimeout';
 
 export function useOpenKeys(
   menuState: MenuState,
@@ -15,18 +16,29 @@ export function useOpenKeys(
   mode: Ref<MenuModeEnum>,
   accordion: Ref<boolean>
 ) {
-  const { getCollapsed, getIsMixSidebar } = useMenuSetting();
+  const { getCollapsed, getIsMixSidebar, getMixSideFixed } = useMenuSetting();
 
-  function setOpenKeys(path: string) {
+  async function setOpenKeys(path: string) {
     if (mode.value === MenuModeEnum.HORIZONTAL) {
       return;
     }
-    const menuList = toRaw(menus.value);
-    if (!unref(accordion)) {
-      menuState.openKeys = es6Unique([...menuState.openKeys, ...getAllParentPath(menuList, path)]);
-    } else {
-      menuState.openKeys = getAllParentPath(menuList, path);
-    }
+    const native = unref(getIsMixSidebar) && unref(getMixSideFixed);
+
+    useTimeoutFn(
+      () => {
+        const menuList = toRaw(menus.value);
+        if (!unref(accordion)) {
+          menuState.openKeys = es6Unique([
+            ...menuState.openKeys,
+            ...getAllParentPath(menuList, path),
+          ]);
+        } else {
+          menuState.openKeys = getAllParentPath(menuList, path);
+        }
+      },
+      16,
+      native
+    );
   }
 
   const getOpenKeys = computed(() => {

+ 1 - 1
src/components/Table/src/hooks/useDataSource.ts

@@ -218,7 +218,7 @@ export function useDataSource(
   onMounted(() => {
     useTimeoutFn(() => {
       unref(propsRef).immediate && fetch();
-    }, 0);
+    }, 16);
   });
 
   return {

+ 12 - 9
src/hooks/core/useTimeout.ts

@@ -3,20 +3,23 @@ import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
 
 import { isFunction } from '/@/utils/is';
 
-export function useTimeoutFn(handle: Fn<any>, wait: number) {
+export function useTimeoutFn(handle: Fn<any>, wait: number, native = false) {
   if (!isFunction(handle)) {
     throw new Error('handle is not Function!');
   }
 
   const { readyRef, stop, start } = useTimeoutRef(wait);
-
-  watch(
-    readyRef,
-    (maturity) => {
-      maturity && handle();
-    },
-    { immediate: false }
-  );
+  if (native) {
+    handle();
+  } else {
+    watch(
+      readyRef,
+      (maturity) => {
+        maturity && handle();
+      },
+      { immediate: false }
+    );
+  }
   return { readyRef, stop, start };
 }
 

+ 9 - 2
src/hooks/setting/useMenuSetting.ts

@@ -1,6 +1,6 @@
 import type { MenuSetting } from '/@/types/config';
 
-import { computed, unref } from 'vue';
+import { computed, unref, ref } from 'vue';
 
 import { appStore } from '/@/store/modules/app';
 
@@ -8,6 +8,8 @@ import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appE
 import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
 import { useFullContent } from '/@/hooks/web/useFullContent';
 
+const mixSideHasChildren = ref(false);
+
 // Get menu configuration
 const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
 
@@ -39,6 +41,8 @@ const getCanDrag = computed(() => unref(getMenuSetting).canDrag);
 
 const getAccordion = computed(() => unref(getMenuSetting).accordion);
 
+const getMixSideFixed = computed(() => unref(getMenuSetting).mixSideFixed);
+
 const getTopMenuAlign = computed(() => unref(getMenuSetting).topMenuAlign);
 
 const getCloseMixSidebarOnChange = computed(() => unref(getMenuSetting).closeMixSidebarOnChange);
@@ -87,7 +91,8 @@ const getCalcContentWidth = computed(() => {
     unref(getIsTopMenu) || !unref(getShowMenu) || (unref(getSplit) && unref(getMenuHidden))
       ? 0
       : unref(getIsMixSidebar)
-      ? SIDE_BAR_SHOW_TIT_MINI_WIDTH
+      ? SIDE_BAR_SHOW_TIT_MINI_WIDTH +
+        (unref(getMixSideFixed) && unref(mixSideHasChildren) ? unref(getRealWidth) : 0)
       : unref(getRealWidth);
 
   return `calc(100% - ${unref(width)}px)`;
@@ -148,5 +153,7 @@ export function useMenuSetting() {
     getIsMixSidebar,
     getCloseMixSidebarOnChange,
     getMixSideTrigger,
+    getMixSideFixed,
+    mixSideHasChildren,
   };
 }

+ 7 - 0
src/layouts/default/setting/SettingDrawer.tsx

@@ -75,6 +75,7 @@ export default defineComponent({
       getIsMixSidebar,
       getCloseMixSidebarOnChange,
       getMixSideTrigger,
+      getMixSideFixed,
     } = useMenuSetting();
 
     const {
@@ -110,6 +111,12 @@ export default defineComponent({
             def={unref(getSplit)}
             disabled={!unref(getShowMenuRef) || unref(getMenuType) !== MenuTypeEnum.MIX}
           />
+          <SwitchItem
+            title={t('layout.setting.mixSidebarFixed')}
+            event={HandlerEnum.MENU_FIXED_MIX_SIDEBAR}
+            def={unref(getMixSideFixed)}
+            disabled={!unref(getIsMixSidebar)}
+          />
 
           <SwitchItem
             title={t('layout.setting.closeMixSidebarOnChange')}

+ 1 - 0
src/layouts/default/setting/enum.ts

@@ -27,6 +27,7 @@ export enum HandlerEnum {
   MENU_FIXED,
   MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE,
   MENU_TRIGGER_MIX_SIDEBAR,
+  MENU_FIXED_MIX_SIDEBAR,
 
   // header
   HEADER_SHOW,

+ 3 - 0
src/layouts/default/setting/handler.ts

@@ -70,6 +70,9 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
     case HandlerEnum.MENU_TRIGGER_MIX_SIDEBAR:
       return { menuSetting: { mixSideTrigger: value } };
 
+    case HandlerEnum.MENU_FIXED_MIX_SIDEBAR:
+      return { menuSetting: { mixSideTrigger: value } };
+
     // ============transition==================
     case HandlerEnum.OPEN_PAGE_LOADING:
       appStore.commitPageLoadingState(false);

+ 120 - 21
src/layouts/default/sider/MixSider.vue

@@ -1,5 +1,5 @@
 <template>
-  <div :class="`${prefixCls}-dom`" />
+  <div :class="`${prefixCls}-dom`" :style="getDomStyle" />
 
   <div
     v-click-outside="handleClickOutside"
@@ -27,7 +27,7 @@
           v-bind="getItemEvents(item)"
         >
           <MenuTag :item="item" :showTitle="false" :isHorizontal="false" />
-          <g-icon
+          <Icon
             :class="`${prefixCls}-module__icon`"
             :size="22"
             :icon="item.meta && item.meta.icon"
@@ -48,6 +48,14 @@
         ]"
       >
         <span class="text"> {{ title }}</span>
+        <Icon
+          :size="16"
+          v-if="getMixSideFixed"
+          icon="ri:pushpin-2-fill"
+          class="pushpin"
+          @click="handleFixedMenu"
+        />
+        <Icon :size="16" v-else icon="ri:pushpin-2-line" class="pushpin" @click="handleFixedMenu" />
       </div>
       <ScrollContainer :class="`${prefixCls}-menu-list__content`">
         <BasicMenu
@@ -70,20 +78,23 @@
 <script lang="ts">
   import { defineComponent, onMounted, ref, computed, CSSProperties, unref } from 'vue';
   import type { Menu } from '/@/router/types';
-  import type { RouteLocationNormalized } from 'vue-router';
+  import { RouteLocationNormalized } from 'vue-router';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus';
   import { useI18n } from '/@/hooks/web/useI18n';
   import { ScrollContainer } from '/@/components/Container';
+  import Icon from '/@/components/Icon';
   import { AppLogo } from '/@/components/Application';
   import { useGo } from '/@/hooks/web/usePage';
   import { BasicMenu, MenuTag } from '/@/components/Menu';
   import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
   import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
   import { useDragLine } from './useLayoutSider';
+  import { useGlobSetting } from '/@/hooks/setting';
+
+  import { SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
 
   import clickOutside from '/@/directives/clickOutside';
-  import { useGlobSetting } from '/@/hooks/setting';
 
   export default defineComponent({
     name: 'LayoutMixSider',
@@ -92,6 +103,7 @@
       AppLogo,
       BasicMenu,
       MenuTag,
+      Icon,
     },
     directives: {
       clickOutside,
@@ -101,6 +113,7 @@
       const activePath = ref('');
       const chilrenMenus = ref<Menu[]>([]);
       const openMenu = ref(false);
+      const isInit = ref(false);
       const dragBarRef = ref<ElRef>(null);
       const sideRef = ref<ElRef>(null);
       const currentRoute = ref<Nullable<RouteLocationNormalized>>(null);
@@ -114,7 +127,12 @@
         getCloseMixSidebarOnChange,
         getMenuTheme,
         getMixSideTrigger,
+        getRealWidth,
+        getMixSideFixed,
+        mixSideHasChildren,
+        setMenuSetting,
       } = useMenuSetting();
+
       const { title } = useGlobSetting();
 
       useDragLine(sideRef, dragBarRef, true);
@@ -127,14 +145,41 @@
         }
       );
 
+      const getIsFixed = computed(() => {
+        mixSideHasChildren.value = unref(chilrenMenus).length > 0;
+        const isFixed = unref(getMixSideFixed) && unref(mixSideHasChildren);
+        if (isFixed) {
+          openMenu.value = true;
+        }
+        return isFixed;
+      });
+
+      const getDomStyle = computed(
+        (): CSSProperties => {
+          const fixedWidth = unref(getIsFixed) ? unref(getRealWidth) : 0;
+          const width = `${SIDE_BAR_SHOW_TIT_MINI_WIDTH + fixedWidth}px`;
+          return {
+            width,
+            maxWidth: width,
+            minWidth: width,
+            flex: `0 0 ${width}`,
+          };
+        }
+      );
+
       const getMenuEvents = computed(() => {
-        return unref(getMixSideTrigger) === 'hover'
-          ? {
-              onMouseleave: () => {
-                openMenu.value = false;
-              },
-            }
-          : {};
+        // return unref(getMixSideTrigger) === 'hover'
+        //   ? {
+        //       onMouseleave: () => {
+        //         closeMenu();
+        //       },
+        //     }
+        //   : {};
+        return {
+          onMouseleave: () => {
+            closeMenu();
+          },
+        };
       });
 
       const getShowDragBar = computed(() => unref(getCanDrag));
@@ -145,9 +190,9 @@
 
       listenerLastChangeTab((route) => {
         currentRoute.value = route;
-        setActive();
+        setActive(true);
         if (unref(getCloseMixSidebarOnChange)) {
-          openMenu.value = false;
+          closeMenu();
         }
       });
 
@@ -156,7 +201,11 @@
 
         if (unref(activePath) === path) {
           if (!hover) {
-            openMenu.value = !unref(openMenu);
+            if (!unref(openMenu)) {
+              openMenu.value = true;
+            } else {
+              closeMenu();
+            }
           }
           if (!unref(openMenu)) {
             setActive();
@@ -169,18 +218,32 @@
         if (!children || children.length === 0) {
           go(path);
           chilrenMenus.value = [];
-          openMenu.value = false;
+          closeMenu();
           return;
         }
         chilrenMenus.value = children;
       }
 
-      async function setActive() {
+      async function setActive(setChildren = false) {
         const path = currentRoute.value?.path;
         if (!path) return;
         const parentPath = await getCurrentParentPath(path);
         activePath.value = parentPath;
         // hanldeModuleClick(parentPath);
+        if (unref(getMixSideFixed)) {
+          const activeMenu = unref(menuModules).find((item) => item.path === unref(activePath));
+          const p = activeMenu?.path;
+          if (p) {
+            const children = await getChildrenMenus(p);
+            if (setChildren) {
+              chilrenMenus.value = children;
+              openMenu.value = children.length > 0;
+            }
+            if (children.length === 0) {
+              chilrenMenus.value = [];
+            }
+          }
+        }
       }
 
       function handleMenuClick(path: string) {
@@ -188,7 +251,7 @@
       }
 
       function handleClickOutside() {
-        openMenu.value = false;
+        closeMenu();
         setActive();
       }
 
@@ -203,6 +266,18 @@
         };
       }
 
+      function handleFixedMenu() {
+        setMenuSetting({
+          mixSideFixed: !unref(getIsFixed),
+        });
+      }
+
+      function closeMenu() {
+        if (!unref(getIsFixed)) {
+          openMenu.value = false;
+        }
+      }
+
       return {
         t,
         prefixCls,
@@ -221,6 +296,9 @@
         getMenuTheme,
         getItemEvents,
         getMenuEvents,
+        getDomStyle,
+        handleFixedMenu,
+        getMixSideFixed,
       };
     },
   });
@@ -241,7 +319,7 @@
     min-width: @width;
     overflow: hidden;
     background: @sider-dark-bg-color;
-    transition: all 0.2s ease 0s;
+    transition: all 0.3s ease 0s;
     flex: 0 0 @width;
     .@{tag-prefix-cls} {
       position: absolute;
@@ -293,6 +371,17 @@
           }
         }
       }
+      .@{prefix-cls}-menu-list {
+        &__title {
+          .pushpin {
+            color: rgba(0, 0, 0, 0.35);
+
+            &:hover {
+              color: rgba(0, 0, 0, 0.85);
+            }
+          }
+        }
+      }
     }
     @border-color: @sider-dark-lighten-1-bg-color;
 
@@ -388,20 +477,30 @@
       &__title {
         display: flex;
         height: @header-height;
-        margin-left: -6px;
+        // margin-left: -6px;
         font-size: 18px;
         color: @primary-color;
         border-bottom: 1px solid rgb(238, 238, 238);
         opacity: 0;
         transition: unset;
-        // justify-content: center;
         align-items: center;
-        justify-content: start;
+        justify-content: space-between;
 
         &.show {
+          min-width: 130px;
           opacity: 1;
           transition: all 0.5s ease;
         }
+
+        .pushpin {
+          margin-right: 6px;
+          color: rgba(255, 255, 255, 0.65);
+          cursor: pointer;
+
+          &:hover {
+            color: #fff;
+          }
+        }
       }
 
       &__content {

+ 2 - 0
src/locales/lang/en/layout/setting.ts

@@ -77,4 +77,6 @@ export default {
   mixSidebarTrigger: 'Mixed menu Trigger',
   triggerHover: 'Hover',
   triggerClick: 'Click',
+
+  mixSidebarFixed: 'Fixed expanded menu',
 };

+ 2 - 0
src/locales/lang/zh_CN/layout/setting.ts

@@ -76,4 +76,6 @@ export default {
   mixSidebarTrigger: '混合菜单触发方式',
   triggerHover: '悬停',
   triggerClick: '点击',
+
+  mixSidebarFixed: '固定展开菜单',
 };

+ 2 - 0
src/settings/projectSetting.ts

@@ -110,6 +110,8 @@ const setting: ProjectConfig = {
     closeMixSidebarOnChange: false,
     // Module opening method ‘click’ |'hover'
     mixSideTrigger: MixSidebarTriggerEnum.CLICK,
+    // Fixed expanded menu
+    mixSideFixed: false,
   },
 
   // Multi-label

+ 1 - 0
src/types/config.d.ts

@@ -21,6 +21,7 @@ export interface MenuSetting {
   closeMixSidebarOnChange: boolean;
   collapsedShowTitle: boolean;
   mixSideTrigger: MixSidebarTriggerEnum;
+  mixSideFixed: boolean;
 }
 
 export interface MultiTabsSetting {

+ 2 - 2
tsconfig.json

@@ -1,13 +1,13 @@
 {
   "compilerOptions": {
-    "target": "es2016",
+    "target": "esnext",
     "module": "esnext",
     "moduleResolution": "node",
     "strict": true,
     "forceConsistentCasingInFileNames": true,
     "allowSyntheticDefaultImports": true,
     "strictFunctionTypes": false,
-    "jsx": "react",
+    "jsx": "preserve",
     "baseUrl": ".",
     "allowJs": true,
     "sourceMap": true,