Bläddra i källkod

perf(tabs): perf multiple-tabs

vben 4 år sedan
förälder
incheckning
27e50b4747
35 ändrade filer med 593 tillägg och 401 borttagningar
  1. 2 2
      src/components/Dropdown/index.ts
  2. 1 0
      src/components/Menu/src/BasicMenu.tsx
  3. 11 0
      src/hooks/setting/useMenuSetting.ts
  4. 0 42
      src/layouts/default/LayoutTrigger.tsx
  5. 2 2
      src/layouts/default/content/index.vue
  6. 29 0
      src/layouts/default/feature/index.vue
  7. 0 28
      src/layouts/default/footer/index.less
  8. 0 34
      src/layouts/default/footer/index.tsx
  9. 74 0
      src/layouts/default/footer/index.vue
  10. 1 1
      src/layouts/default/header/LayoutHeader.tsx
  11. 1 1
      src/layouts/default/header/LayoutMultipleHeader.tsx
  12. 2 1
      src/layouts/default/header/index.less
  13. 12 45
      src/layouts/default/index.tsx
  14. 95 0
      src/layouts/default/index.vue
  15. 0 78
      src/layouts/default/multitabs/TabContent.tsx
  16. 0 114
      src/layouts/default/multitabs/index.tsx
  17. 1 1
      src/layouts/default/setting/index.vue
  18. 1 1
      src/layouts/default/sider/useLayoutSider.tsx
  19. 21 0
      src/layouts/default/tabs/components/QuickButton.vue
  20. 72 0
      src/layouts/default/tabs/components/TabContent.vue
  21. 58 32
      src/layouts/default/tabs/index.less
  22. 123 0
      src/layouts/default/tabs/index.vue
  23. 0 0
      src/layouts/default/tabs/types.ts
  24. 4 3
      src/layouts/default/tabs/useMultipleTabs.ts
  25. 0 0
      src/layouts/default/tabs/useTabDropdown.ts
  26. 25 0
      src/layouts/default/trigger/HeaderTrigger.vue
  27. 18 0
      src/layouts/default/trigger/SiderTrigger.vue
  28. 21 0
      src/layouts/default/trigger/index.vue
  29. 9 7
      src/layouts/iframe/index.vue
  30. 3 3
      src/layouts/iframe/useFrameKeepAlive.ts
  31. 2 0
      src/layouts/page/index.tsx
  32. 0 1
      src/layouts/page/useCache.ts
  33. 2 2
      src/locales/lang/en/layout/multipleTab.ts
  34. 2 2
      src/locales/lang/zh_CN/layout/multipleTab.ts
  35. 1 1
      src/router/constant.ts

+ 2 - 2
src/components/Dropdown/index.ts

@@ -1,7 +1,7 @@
 import { withInstall } from '../util';
 
-import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
-export const Dropdown = createAsyncComponent(() => import('./src/Dropdown'));
+import Dropdown from './src/Dropdown';
 
 withInstall(Dropdown);
 export * from './src/types';
+export { Dropdown };

+ 1 - 0
src/components/Menu/src/BasicMenu.tsx

@@ -243,6 +243,7 @@ export default defineComponent({
           onOpenChange={handleOpenChange}
           class={unref(getMenuClass)}
           onClick={handleMenuClick}
+          subMenuOpenDelay={0.2}
           {...unref(getInlineCollapseOptions)}
         >
           {{

+ 11 - 0
src/hooks/setting/useMenuSetting.ts

@@ -6,6 +6,7 @@ import { appStore } from '/@/store/modules/app';
 
 import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
 import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
+import { useFullContent } from '/@/hooks/web/useFullContent';
 
 // Get menu configuration
 const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
@@ -78,6 +79,15 @@ const getCalcContentWidth = computed(() => {
   return `calc(100% - ${unref(width)}px)`;
 });
 
+const { getFullContent: fullContent } = useFullContent();
+
+const getShowSidebar = computed(() => {
+  return (
+    unref(getSplit) ||
+    (unref(getShowMenu) && unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && !unref(fullContent))
+  );
+});
+
 // Set menu configuration
 function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
   appStore.commitProjectConfigState({ menuSetting });
@@ -119,5 +129,6 @@ export function useMenuSetting() {
     getMenuHidden,
     getIsTopMenu,
     getMenuBgColor,
+    getShowSidebar,
   };
 }

+ 0 - 42
src/layouts/default/LayoutTrigger.tsx

@@ -1,42 +0,0 @@
-import type { FunctionalComponent } from 'vue';
-
-import { defineComponent, unref } from 'vue';
-
-import {
-  DoubleRightOutlined,
-  DoubleLeftOutlined,
-  MenuUnfoldOutlined,
-  MenuFoldOutlined,
-} from '@ant-design/icons-vue';
-
-import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
-import { propTypes } from '/@/utils/propTypes';
-
-const SiderTrigger: FunctionalComponent = () => {
-  const { getCollapsed } = useMenuSetting();
-  return unref(getCollapsed) ? <DoubleRightOutlined /> : <DoubleLeftOutlined />;
-};
-
-const HeaderTrigger: FunctionalComponent<{
-  theme?: string;
-}> = (props) => {
-  const { toggleCollapsed, getCollapsed } = useMenuSetting();
-  return (
-    <span class={['layout-trigger', props.theme]} onClick={toggleCollapsed}>
-      {unref(getCollapsed) ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
-    </span>
-  );
-};
-
-export default defineComponent({
-  name: 'LayoutTrigger',
-  props: {
-    sider: propTypes.bool.def(true),
-    theme: propTypes.oneOf(['light', 'dark']),
-  },
-  setup(props) {
-    return () => {
-      return props.sider ? <SiderTrigger /> : <HeaderTrigger theme={props.theme} />;
-    };
-  },
-});

+ 2 - 2
src/layouts/default/content/index.vue

@@ -6,7 +6,7 @@
         :loading="getPageLoading"
         background="rgba(240, 242, 245, 0.6)"
         absolute
-        :class="`${prefixCls}__loading`"
+        :class="`${prefixCls}-loading`"
       />
     </transition>
     <PageLayout />
@@ -53,7 +53,7 @@
       margin: 0 auto;
     }
 
-    &__loading {
+    &-loading {
       position: absolute;
       top: 200px;
       z-index: @page-loading-z-index;

+ 29 - 0
src/layouts/default/feature/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <LayoutLockPage />
+  <BackTop v-if="getUseOpenBackTop" :target="getTarget" />
+  <SettingDrawer v-if="getShowSettingButton" />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+  import { BackTop } from 'ant-design-vue';
+  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+
+  export default defineComponent({
+    name: 'LayoutFeatures',
+    components: {
+      BackTop,
+      LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')),
+      SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')),
+    },
+    setup() {
+      const { getUseOpenBackTop, getShowSettingButton } = useRootSetting();
+
+      return {
+        getTarget: () => document.body,
+        getUseOpenBackTop,
+        getShowSettingButton,
+      };
+    },
+  });
+</script>

+ 0 - 28
src/layouts/default/footer/index.less

@@ -1,28 +0,0 @@
-@normal-color: rgba(0, 0, 0, 0.45);
-
-@hover-color: rgba(0, 0, 0, 0.85);
-
-.layout-footer {
-  color: @normal-color;
-  text-align: center;
-
-  &__links {
-    margin-bottom: 8px;
-
-    a {
-      color: @normal-color;
-
-      &:hover {
-        color: @hover-color;
-      }
-    }
-
-    .github {
-      margin: 0 30px;
-
-      &:hover {
-        color: @hover-color;
-      }
-    }
-  }
-}

+ 0 - 34
src/layouts/default/footer/index.tsx

@@ -1,34 +0,0 @@
-import './index.less';
-
-import { defineComponent } from 'vue';
-import { Layout } from 'ant-design-vue';
-
-import { GithubFilled } from '@ant-design/icons-vue';
-
-import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
-import { openWindow } from '/@/utils';
-
-import { useI18n } from '/@/hooks/web/useI18n';
-
-export default defineComponent({
-  name: 'LayoutContent',
-  setup() {
-    const { t } = useI18n();
-    return () => {
-      return (
-        <Layout.Footer class="layout-footer">
-          {() => (
-            <>
-              <div class="layout-footer__links">
-                <a onClick={() => openWindow(SITE_URL)}>{t('layout.footer.onlinePreview')}</a>
-                <GithubFilled onClick={() => openWindow(GITHUB_URL)} class="github" />
-                <a onClick={() => openWindow(DOC_URL)}>{t('layout.footer.onlineDocument')}</a>
-              </div>
-              <div>Copyright &copy;2020 Vben Admin</div>
-            </>
-          )}
-        </Layout.Footer>
-      );
-    };
-  },
-});

+ 74 - 0
src/layouts/default/footer/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <Footer :class="prefixCls" v-if="getShowLayoutFooter">
+    <div :class="`${prefixCls}__links`">
+      <a @click="openWindow(SITE_URL)">{{ t('layout.footer.onlinePreview') }}</a>
+      <GithubFilled @click="openWindow(GITHUB_URL)" :class="`${prefixCls}__github`" />
+      <a @click="openWindow(DOC_URL)">{{ t('layout.footer.onlineDocument') }}</a>
+    </div>
+    <div>Copyright &copy;2020 Vben Admin</div>
+  </Footer>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, unref } from 'vue';
+  import { Layout } from 'ant-design-vue';
+
+  import { GithubFilled } from '@ant-design/icons-vue';
+
+  import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
+  import { openWindow } from '/@/utils';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+  import { useRouter } from 'vue-router';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  export default defineComponent({
+    name: 'LayoutFooter',
+    components: { Footer: Layout.Footer, GithubFilled },
+    setup() {
+      const { t } = useI18n();
+      const { getShowFooter } = useRootSetting();
+      const { currentRoute } = useRouter();
+      const { prefixCls } = useDesign('layout-footer');
+
+      const getShowLayoutFooter = computed(() => {
+        return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
+      });
+      return { getShowLayoutFooter, prefixCls, t, DOC_URL, GITHUB_URL, SITE_URL, openWindow };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  @import (reference) '../../../design/index.less';
+  @prefix-cls: ~'@{namespace}-layout-footer';
+
+  @normal-color: rgba(0, 0, 0, 0.45);
+
+  @hover-color: rgba(0, 0, 0, 0.85);
+
+  .@{prefix-cls} {
+    color: @normal-color;
+    text-align: center;
+
+    &__links {
+      margin-bottom: 8px;
+
+      a {
+        color: @normal-color;
+
+        &:hover {
+          color: @hover-color;
+        }
+      }
+    }
+
+    &__github {
+      margin: 0 30px;
+
+      &:hover {
+        color: @hover-color;
+      }
+    }
+  }
+</style>

+ 1 - 1
src/layouts/default/header/LayoutHeader.tsx

@@ -19,7 +19,7 @@ import UserDropdown from './UserDropdown';
 import LayoutMenu from '../menu';
 import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
 import LockAction from './actions/LockAction';
-import LayoutTrigger from '../LayoutTrigger';
+import LayoutTrigger from '../trigger/index.vue';
 import NoticeAction from './notice/NoticeActionItem.vue';
 import {
   RedoOutlined,

+ 1 - 1
src/layouts/default/header/LayoutMultipleHeader.tsx

@@ -3,7 +3,7 @@ import './LayoutMultipleHeader.less';
 import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue';
 
 import LayoutHeader from './LayoutHeader';
-import MultipleTabs from '../multitabs/index';
+import MultipleTabs from '../tabs/index.vue';
 
 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';

+ 2 - 1
src/layouts/default/header/index.less

@@ -1,4 +1,5 @@
 @import (reference) '../../../design/index.less';
+@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
 
 .layout-header {
   display: flex;
@@ -24,7 +25,7 @@
     height: 100%;
     align-items: center;
 
-    .layout-trigger {
+    .@{header-trigger-prefix-cls} {
       display: flex;
       height: 100%;
       padding: 1px 10px 0 16px;

+ 12 - 45
src/layouts/default/index.tsx

@@ -1,32 +1,28 @@
 import './index.less';
 
-import { defineComponent, unref, computed, ref } from 'vue';
-import { Layout, BackTop } from 'ant-design-vue';
-import LayoutHeader from './header/LayoutHeader';
+import { defineComponent, unref, ref } from 'vue';
+import { Layout } from 'ant-design-vue';
+import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 
+import LayoutHeader from './header/LayoutHeader';
 import LayoutContent from './content/index.vue';
-import LayoutFooter from './footer';
-import LayoutLockPage from '/@/views/sys/lock/index.vue';
 import LayoutSideBar from './sider';
-import SettingBtn from './setting/index.vue';
 import LayoutMultipleHeader from './header/LayoutMultipleHeader';
 
-import { MenuModeEnum } from '/@/enums/menuEnum';
-
-import { useRouter } from 'vue-router';
-import { useFullContent } from '/@/hooks/web/useFullContent';
 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
-import { useRootSetting } from '/@/hooks/setting/useRootSetting';
 import { createLayoutContext } from './useLayoutContext';
 
 import { registerGlobComp } from '/@/components/registerGlobComp';
 import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
 import { isMobile } from '/@/utils/is';
+
+const LayoutFeatures = createAsyncComponent(() => import('/@/layouts/default/feature/index.vue'));
+const LayoutFooter = createAsyncComponent(() => import('/@/layouts/default/footer/index.vue'));
+
 export default defineComponent({
   name: 'DefaultLayout',
   setup() {
-    const { currentRoute } = useRouter();
     const headerRef = ref<ComponentRef>(null);
     const isMobileRef = ref(false);
 
@@ -43,56 +39,27 @@ export default defineComponent({
 
     const { getShowFullHeaderRef } = useHeaderSetting();
 
-    const { getUseOpenBackTop, getShowSettingButton, getShowFooter } = useRootSetting();
-
-    const { getShowMenu, getMenuMode, getSplit } = useMenuSetting();
-
-    const { getFullContent } = useFullContent();
-
-    const getShowLayoutFooter = computed(() => {
-      return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
-    });
-
-    const showSideBarRef = computed(() => {
-      return (
-        unref(getSplit) ||
-        (unref(getShowMenu) &&
-          unref(getMenuMode) !== MenuModeEnum.HORIZONTAL &&
-          !unref(getFullContent))
-      );
-    });
-
-    function renderFeatures() {
-      return (
-        <>
-          <LayoutLockPage />
-          {/* back top */}
-          {unref(getUseOpenBackTop) && <BackTop target={() => document.body} />}
-          {/* open setting drawer */}
-          {unref(getShowSettingButton) && <SettingBtn />}
-        </>
-      );
-    }
+    const { getShowSidebar } = useMenuSetting();
 
     return () => {
       return (
         <Layout class="default-layout">
           {() => (
             <>
-              {renderFeatures()}
+              <LayoutFeatures />
 
               {unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />}
 
               <Layout>
                 {() => (
                   <>
-                    {unref(showSideBarRef) && <LayoutSideBar />}
+                    {unref(getShowSidebar) && <LayoutSideBar />}
                     <Layout class="default-layout__main">
                       {() => (
                         <>
                           <LayoutMultipleHeader />
                           <LayoutContent />
-                          {unref(getShowLayoutFooter) && <LayoutFooter />}
+                          <LayoutFooter />
                         </>
                       )}
                     </Layout>

+ 95 - 0
src/layouts/default/index.vue

@@ -0,0 +1,95 @@
+<template>
+  <Layout :class="prefixCls">
+    <LayoutFeatures />
+    <LayoutHeader fixed ref="headerRef" v-if="getShowFullHeaderRef" />
+    <Layout>
+      <LayoutSideBar v-if="getShowSidebar" />
+      <Layout :class="`${prefixCls}__main`">
+        <LayoutMultipleHeader />
+        <LayoutContent />
+        <LayoutFooter />
+      </Layout>
+    </Layout>
+  </Layout>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import { Layout } from 'ant-design-vue';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+
+  import LayoutHeader from './header/LayoutHeader';
+  import LayoutContent from './content/index.vue';
+  import LayoutSideBar from './sider';
+  import LayoutMultipleHeader from './header/LayoutMultipleHeader';
+
+  import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
+  import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { createLayoutContext } from './useLayoutContext';
+
+  import { registerGlobComp } from '/@/components/registerGlobComp';
+  import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
+  import { isMobile } from '/@/utils/is';
+
+  export default defineComponent({
+    name: 'DefaultLayout',
+    components: {
+      LayoutFeatures: createAsyncComponent(() => import('/@/layouts/default/feature/index.vue')),
+      LayoutFooter: createAsyncComponent(() => import('/@/layouts/default/footer/index.vue')),
+      LayoutHeader,
+      LayoutContent,
+      LayoutSideBar,
+      LayoutMultipleHeader,
+      Layout,
+    },
+    setup() {
+      const headerRef = ref<ComponentRef>(null);
+      const isMobileRef = ref(false);
+
+      const { prefixCls } = useDesign('default-layout');
+
+      createLayoutContext({ fullHeader: headerRef, isMobile: isMobileRef });
+
+      createBreakpointListen(() => {
+        isMobileRef.value = isMobile();
+      });
+
+      // ! Only register global components here
+      // ! Can reduce the size of the first screen code
+      // default layout It is loaded after login. So it won’t be packaged to the first screen
+      registerGlobComp();
+
+      const { getShowFullHeaderRef } = useHeaderSetting();
+
+      const { getShowSidebar } = useMenuSetting();
+
+      return {
+        getShowFullHeaderRef,
+        getShowSidebar,
+        headerRef,
+        prefixCls,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @import (reference) '../../design/index.less';
+  @prefix-cls: ~'@{namespace}-default-layout';
+
+  .@{prefix-cls} {
+    display: flex;
+    width: 100%;
+    min-height: 100%;
+    background: @content-bg;
+    flex-direction: column;
+
+    > .ant-layout {
+      min-height: 100%;
+    }
+
+    &__main {
+      margin-left: 1px;
+    }
+  }
+</style>

+ 0 - 78
src/layouts/default/multitabs/TabContent.tsx

@@ -1,78 +0,0 @@
-import type { PropType } from 'vue';
-import { Dropdown } from '/@/components/Dropdown/index';
-
-import { defineComponent, unref, FunctionalComponent } from 'vue';
-
-import { TabContentProps } from './types';
-
-import { RightOutlined } from '@ant-design/icons-vue';
-
-import { TabContentEnum } from './types';
-
-import { useTabDropdown } from './useTabDropdown';
-import { useI18n } from '/@/hooks/web/useI18n';
-
-import { RouteLocationNormalized } from 'vue-router';
-
-const { t: titleT } = useI18n();
-
-const ExtraContent: FunctionalComponent = () => {
-  return (
-    <span class={`multiple-tabs-content__extra `}>
-      <RightOutlined />
-    </span>
-  );
-};
-
-const TabContent: FunctionalComponent<{ tabItem: RouteLocationNormalized; handler: Fn }> = (
-  props
-) => {
-  const { tabItem: { meta } = {} } = props;
-
-  return (
-    <div class={`multiple-tabs-content__content `} onContextmenu={props.handler(props.tabItem)}>
-      <span class="ml-1">{meta && titleT(meta.title)}</span>
-    </div>
-  );
-};
-
-export default defineComponent({
-  name: 'TabContent',
-  props: {
-    tabItem: {
-      type: Object as PropType<RouteLocationNormalized>,
-      default: null,
-    },
-
-    type: {
-      type: Number as PropType<TabContentEnum>,
-      default: TabContentEnum.TAB_TYPE,
-    },
-  },
-  setup(props) {
-    const {
-      getDropMenuList,
-      handleMenuEvent,
-      handleContextMenu,
-      getTrigger,
-      isTabs,
-    } = useTabDropdown(props as TabContentProps);
-
-    return () => {
-      return (
-        <Dropdown
-          dropMenuList={unref(getDropMenuList)}
-          trigger={unref(getTrigger)}
-          onMenuEvent={handleMenuEvent}
-        >
-          {() => {
-            if (!unref(isTabs)) {
-              return <ExtraContent />;
-            }
-            return <TabContent handler={handleContextMenu} tabItem={props.tabItem} />;
-          }}
-        </Dropdown>
-      );
-    };
-  },
-});

+ 0 - 114
src/layouts/default/multitabs/index.tsx

@@ -1,114 +0,0 @@
-import './index.less';
-
-import type { TabContentProps } from './types';
-
-import { defineComponent, watch, computed, unref, ref } from 'vue';
-import { useRouter } from 'vue-router';
-
-import { Tabs } from 'ant-design-vue';
-import TabContent from './TabContent';
-
-import { useGo } from '/@/hooks/web/usePage';
-
-import { TabContentEnum } from './types';
-
-import { tabStore } from '/@/store/modules/tab';
-import { userStore } from '/@/store/modules/user';
-
-import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
-import { REDIRECT_NAME } from '/@/router/constant';
-
-export default defineComponent({
-  name: 'MultipleTabs',
-  setup() {
-    const activeKeyRef = ref('');
-
-    const affixTextList = initAffixTabs();
-
-    useTabsDrag(affixTextList);
-
-    const go = useGo();
-
-    const { currentRoute } = useRouter();
-
-    const getTabsState = computed(() => tabStore.getTabsState);
-
-    watch(
-      () => tabStore.getLastChangeRouteState?.path,
-      () => {
-        if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
-          return;
-        }
-        const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
-        if (!lastChangeRoute || !userStore.getTokenState) return;
-        const { path, fullPath } = lastChangeRoute;
-        const p = fullPath || path;
-        if (activeKeyRef.value !== p) {
-          activeKeyRef.value = p;
-        }
-        tabStore.addTabAction(lastChangeRoute);
-      },
-      {
-        immediate: true,
-      }
-    );
-
-    function handleChange(activeKey: any) {
-      activeKeyRef.value = activeKey;
-      go(activeKey, false);
-    }
-
-    // Close the current tab
-    function handleEdit(targetKey: string) {
-      // Added operation to hide, currently only use delete operation
-      tabStore.closeTabByKeyAction(targetKey);
-    }
-
-    function renderQuick() {
-      const tabContentProps: TabContentProps = {
-        tabItem: currentRoute.value,
-        type: TabContentEnum.EXTRA_TYPE,
-      };
-      return <TabContent {...tabContentProps} />;
-    }
-
-    function renderTabs() {
-      return unref(getTabsState).map((item) => {
-        const key = item.query ? item.fullPath : item.path;
-        const closable = !(item && item.meta && item.meta.affix);
-
-        const slots = {
-          tab: () => <TabContent tabItem={item} />,
-        };
-        return (
-          <Tabs.TabPane key={key} closable={closable}>
-            {slots}
-          </Tabs.TabPane>
-        );
-      });
-    }
-
-    return () => {
-      const slots = {
-        default: () => renderTabs(),
-        tabBarExtraContent: () => renderQuick(),
-      };
-      return (
-        <div class="multiple-tabs">
-          <Tabs
-            type="editable-card"
-            size="small"
-            animated={false}
-            hideAdd={true}
-            tabBarGutter={3}
-            activeKey={unref(activeKeyRef)}
-            onChange={handleChange}
-            onEdit={handleEdit}
-          >
-            {slots}
-          </Tabs>
-        </div>
-      );
-    };
-  },
-});

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

@@ -13,7 +13,7 @@
   import { useDesign } from '/@/hooks/web/useDesign';
 
   export default defineComponent({
-    name: 'SettingBtn',
+    name: 'SettingButton',
     components: { SettingOutlined, SettingDrawer },
     setup() {
       const [register, { openDrawer }] = useDrawer();

+ 1 - 1
src/layouts/default/sider/useLayoutSider.tsx

@@ -1,7 +1,7 @@
 import type { Ref } from 'vue';
 
 import { computed, unref, onMounted, nextTick, ref } from 'vue';
-import LayoutTrigger from '/@/layouts/default/LayoutTrigger';
+import LayoutTrigger from '/@/layouts/default/trigger/index.vue';
 
 import { TriggerEnum } from '/@/enums/menuEnum';
 

+ 21 - 0
src/layouts/default/tabs/components/QuickButton.vue

@@ -0,0 +1,21 @@
+<template>
+  <TabContent :type="TabContentEnum.EXTRA_TYPE" :tabItem="$route" />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import { TabContentEnum } from '../types';
+
+  import TabContent from './TabContent.vue';
+  export default defineComponent({
+    name: 'QuickButton',
+    components: {
+      TabContent,
+    },
+    setup() {
+      return {
+        TabContentEnum,
+      };
+    },
+  });
+</script>

+ 72 - 0
src/layouts/default/tabs/components/TabContent.vue

@@ -0,0 +1,72 @@
+<template>
+  <Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent">
+    <div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="isTabs">
+      <span class="ml-1">{{ getTitle }}</span>
+    </div>
+
+    <span :class="`${prefixCls}__extra`" v-else>
+      <RightOutlined />
+    </span>
+  </Dropdown>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+
+  import { defineComponent, computed } from 'vue';
+  import { Dropdown } from '/@/components/Dropdown/index';
+
+  import { TabContentProps, TabContentEnum } from '../types';
+
+  import { RightOutlined } from '@ant-design/icons-vue';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useTabDropdown } from '../useTabDropdown';
+  import { useI18n } from '/@/hooks/web/useI18n';
+
+  import { RouteLocationNormalized } from 'vue-router';
+  export default defineComponent({
+    name: 'TabContent',
+    components: { Dropdown, RightOutlined },
+    props: {
+      tabItem: {
+        type: Object as PropType<RouteLocationNormalized>,
+        default: null,
+      },
+
+      type: {
+        type: Number as PropType<TabContentEnum>,
+        default: TabContentEnum.TAB_TYPE,
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('multiple-tabs-content');
+      const { t } = useI18n();
+
+      const getTitle = computed(() => {
+        const { tabItem: { meta } = {} } = props;
+        return meta && t(meta.title);
+      });
+
+      const {
+        getDropMenuList,
+        handleMenuEvent,
+        handleContextMenu,
+        getTrigger,
+        isTabs,
+      } = useTabDropdown(props as TabContentProps);
+
+      function handleContext(e: ChangeEvent) {
+        props.tabItem && handleContextMenu(props.tabItem)(e);
+      }
+      return {
+        prefixCls,
+        getDropMenuList,
+        handleMenuEvent,
+        handleContext,
+        getTrigger,
+        isTabs,
+        getTitle,
+      };
+    },
+  });
+</script>

+ 58 - 32
src/layouts/default/multitabs/index.less → src/layouts/default/tabs/index.less

@@ -1,10 +1,9 @@
 @import (reference) '../../../design/index.less';
+@prefix-cls: ~'@{namespace}-multiple-tabs';
 
-.multiple-tabs {
+.@{prefix-cls} {
   z-index: 10;
   height: @multiple-height + 2;
-  padding: 0 0 2px 0;
-  margin-left: -1px;
   line-height: @multiple-height + 2;
   background: @white;
   box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
@@ -32,13 +31,33 @@
         line-height: calc(@multiple-height - 2px);
         color: @text-color-call-out;
         background: @white;
-        border: 1px solid darken(@border-color-light, 8%);
+        border: 1px solid darken(@border-color-light, 6%);
         transition: none;
 
+        &:not(.ant-tabs-tab-active)::before {
+          position: absolute;
+          top: -1px;
+          left: 50%;
+          width: 100%;
+          height: 2px;
+          background-color: @primary-color;
+          content: '';
+          opacity: 0;
+          transform: translate(-50%, 0) scaleX(0);
+          transform-origin: center;
+          transition: none;
+        }
+
         &:hover {
           .ant-tabs-close-x {
             opacity: 1;
           }
+
+          &:not(.ant-tabs-tab-active)::before {
+            opacity: 1;
+            transform: translate(-50%, 0) scaleX(1);
+            transition: all 0.3s ease-in-out;
+          }
         }
 
         .ant-tabs-close-x {
@@ -51,7 +70,7 @@
 
           &:hover {
             svg {
-              width: 0.75em;
+              width: 0.8em;
             }
           }
         }
@@ -73,6 +92,7 @@
         color: @white;
         background: fade(@primary-color, 100%);
         border: 0;
+        transition: none;
 
         &::before {
           position: absolute;
@@ -98,7 +118,7 @@
     }
 
     .ant-tabs-nav > div:nth-child(1) {
-      padding: 0 10px;
+      padding: 0 6px;
 
       .ant-tabs-tab {
         margin-right: 3px !important;
@@ -124,36 +144,42 @@
   .ant-dropdown-trigger {
     display: inline-flex;
   }
-}
 
-.multiple-tabs-content {
-  &__extra {
-    display: inline-block;
-    width: @multiple-height;
-    height: @multiple-height;
-    line-height: @multiple-height;
-    color: #999;
-    text-align: center;
-    cursor: pointer;
-    border-left: 1px solid #eee;
-
-    &:hover {
-      color: @text-color-base;
+  &--hide-close {
+    .ant-tabs-close-x {
+      opacity: 0 !important;
     }
+  }
+
+  &-content {
+    &__extra {
+      display: inline-block;
+      width: @multiple-height;
+      height: @multiple-height;
+      line-height: @multiple-height;
+      color: #999;
+      text-align: center;
+      cursor: pointer;
+      border-left: 1px solid #eee;
+
+      &:hover {
+        color: @text-color-base;
+      }
 
-    span[role='img'] {
-      transform: rotate(90deg);
+      span[role='img'] {
+        transform: rotate(90deg);
+      }
     }
-  }
 
-  &__content {
-    display: inline-block;
-    width: 100%;
-    height: @multiple-height - 2;
-    padding-left: 0;
-    margin-left: -10px;
-    font-size: 12px;
-    cursor: pointer;
-    user-select: none;
+    &__info {
+      display: inline-block;
+      width: 100%;
+      height: @multiple-height - 2;
+      padding-left: 0;
+      margin-left: -10px;
+      font-size: 12px;
+      cursor: pointer;
+      user-select: none;
+    }
   }
 }

+ 123 - 0
src/layouts/default/tabs/index.vue

@@ -0,0 +1,123 @@
+<template>
+  <div :class="getWrapClass">
+    <Tabs
+      type="editable-card"
+      size="small"
+      :animated="false"
+      :hideAdd="true"
+      :tabBarGutter="3"
+      :activeKey="activeKeyRef"
+      @change="handleChange"
+      @edit="handleEdit"
+    >
+      <template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">
+        <TabPane :closable="!(item && item.meta && item.meta.affix)">
+          <template #tab>
+            <TabContent :tabItem="item" />
+          </template>
+        </TabPane>
+      </template>
+      <template #tabBarExtraContent>
+        <QuickButton />
+      </template>
+    </Tabs>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, watch, computed, unref, ref } from 'vue';
+
+  import { Tabs } from 'ant-design-vue';
+  import TabContent from './components/TabContent.vue';
+
+  import { useGo } from '/@/hooks/web/usePage';
+
+  import { tabStore } from '/@/store/modules/tab';
+  import { userStore } from '/@/store/modules/user';
+
+  import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
+  import { REDIRECT_NAME } from '/@/router/constant';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+
+  export default defineComponent({
+    name: 'MultipleTabs',
+    components: {
+      QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')),
+      Tabs,
+      TabPane: Tabs.TabPane,
+      TabContent,
+    },
+    setup() {
+      const affixTextList = initAffixTabs();
+
+      const activeKeyRef = ref('');
+
+      useTabsDrag(affixTextList);
+      const { prefixCls } = useDesign('multiple-tabs');
+      const go = useGo();
+
+      const getTabsState = computed(() => tabStore.getTabsState);
+
+      const unClose = computed(() => {
+        return getTabsState.value.length === 1;
+      });
+
+      const getWrapClass = computed(() => {
+        return [
+          prefixCls,
+          {
+            [`${prefixCls}--hide-close`]: unClose,
+          },
+        ];
+      });
+
+      watch(
+        () => tabStore.getLastChangeRouteState?.path,
+        () => {
+          if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
+            return;
+          }
+          const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
+          if (!lastChangeRoute || !userStore.getTokenState) return;
+
+          const { path, fullPath } = lastChangeRoute;
+          const p = fullPath || path;
+
+          if (activeKeyRef.value !== p) {
+            activeKeyRef.value = p;
+          }
+
+          tabStore.addTabAction(lastChangeRoute);
+        },
+        {
+          immediate: true,
+        }
+      );
+
+      function handleChange(activeKey: any) {
+        activeKeyRef.value = activeKey;
+        go(activeKey, false);
+      }
+
+      // Close the current tab
+      function handleEdit(targetKey: string) {
+        // Added operation to hide, currently only use delete operation
+        if (unref(unClose)) return;
+
+        tabStore.closeTabByKeyAction(targetKey);
+      }
+      return {
+        prefixCls,
+        unClose,
+        getWrapClass,
+        handleEdit,
+        handleChange,
+        activeKeyRef,
+        getTabsState,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @import './index.less';
+</style>

+ 0 - 0
src/layouts/default/multitabs/types.ts → src/layouts/default/tabs/types.ts


+ 4 - 3
src/layouts/default/multitabs/useMultipleTabs.ts → src/layouts/default/tabs/useMultipleTabs.ts

@@ -2,6 +2,7 @@ import Sortable from 'sortablejs';
 import { toRaw, ref, nextTick, onMounted } from 'vue';
 import { RouteLocationNormalized } from 'vue-router';
 import { useProjectSetting } from '/@/hooks/setting';
+import { useDesign } from '/@/hooks/web/useDesign';
 import router from '/@/router';
 import { tabStore } from '/@/store/modules/tab';
 import { isNullAndUnDef } from '/@/utils/is';
@@ -48,12 +49,12 @@ export function initAffixTabs(): string[] {
 export function useTabsDrag(affixTextList: string[]) {
   const { multiTabsSetting } = useProjectSetting();
 
+  const { prefixCls } = useDesign('multiple-tabs');
+
   function initSortableTabs() {
     if (!multiTabsSetting.canDrag) return;
     nextTick(() => {
-      const el = document.querySelectorAll(
-        '.multiple-tabs .ant-tabs-nav > div'
-      )?.[0] as HTMLElement;
+      const el = document.querySelectorAll(`.${prefixCls} .ant-tabs-nav > div`)?.[0] as HTMLElement;
 
       if (!el) return;
       Sortable.create(el, {

+ 0 - 0
src/layouts/default/multitabs/useTabDropdown.ts → src/layouts/default/tabs/useTabDropdown.ts


+ 25 - 0
src/layouts/default/trigger/HeaderTrigger.vue

@@ -0,0 +1,25 @@
+<template>
+  <span :class="[prefixCls, theme]" @click="toggleCollapsed">
+    <MenuUnfoldOutlined v-if="getCollapsed" /> <MenuFoldOutlined v-else />
+  </span>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
+  import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { propTypes } from '/@/utils/propTypes';
+
+  export default defineComponent({
+    name: 'SiderTrigger',
+    components: { MenuUnfoldOutlined, MenuFoldOutlined },
+    props: {
+      theme: propTypes.oneOf(['light', 'dark']),
+    },
+    setup() {
+      const { getCollapsed, toggleCollapsed } = useMenuSetting();
+      const { prefixCls } = useDesign('layout-header-trigger');
+      return { getCollapsed, toggleCollapsed, prefixCls };
+    },
+  });
+</script>

+ 18 - 0
src/layouts/default/trigger/SiderTrigger.vue

@@ -0,0 +1,18 @@
+<template>
+  <DoubleRightOutlined v-if="getCollapsed" />
+  <DoubleLeftOutlined v-else />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { DoubleRightOutlined, DoubleLeftOutlined } from '@ant-design/icons-vue';
+  import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+
+  export default defineComponent({
+    name: 'SiderTrigger',
+    components: { DoubleRightOutlined, DoubleLeftOutlined },
+    setup() {
+      const { getCollapsed } = useMenuSetting();
+      return { getCollapsed };
+    },
+  });
+</script>

+ 21 - 0
src/layouts/default/trigger/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <SiderTrigger v-if="sider" />
+  <HeaderTrigger v-else :theme="theme" />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+  import { propTypes } from '/@/utils/propTypes';
+
+  export default defineComponent({
+    name: 'LayoutTrigger',
+    components: {
+      SiderTrigger: createAsyncComponent(() => import('./SiderTrigger.vue')),
+      HeaderTrigger: createAsyncComponent(() => import('./HeaderTrigger.vue'), { loading: true }),
+    },
+    props: {
+      sider: propTypes.bool.def(true),
+      theme: propTypes.oneOf(['light', 'dark']),
+    },
+  });
+</script>

+ 9 - 7
src/layouts/iframe/index.vue

@@ -1,11 +1,13 @@
 <template>
-  <template v-for="frame in getFramePages" :key="frame.path">
-    <FramePage
-      v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
-      v-show="showIframe(frame)"
-      :frameSrc="frame.meta.frameSrc"
-    />
-  </template>
+  <div>
+    <template v-for="frame in getFramePages" :key="frame.path">
+      <FramePage
+        v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
+        v-show="showIframe(frame)"
+        :frameSrc="frame.meta.frameSrc"
+      />
+    </template>
+  </div>
 </template>
 <script lang="ts">
   import { defineComponent } from 'vue';

+ 3 - 3
src/layouts/iframe/useFrameKeepAlive.ts

@@ -1,8 +1,6 @@
 import type { AppRouteRecordRaw } from '/@/router/types';
 
 import { computed, toRaw, unref } from 'vue';
-import { useRouter } from 'vue-router';
-import router from '/@/router';
 
 import { tabStore } from '/@/store/modules/tab';
 
@@ -10,8 +8,10 @@ import { unique } from '/@/utils';
 
 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
 
+import router from '/@/router';
+
 export function useFrameKeepAlive() {
-  const { currentRoute } = useRouter();
+  const { currentRoute } = router;
   const { getShowMultipleTab } = useMultipleTabSetting();
 
   const getFramePages = computed(() => {

+ 2 - 0
src/layouts/page/index.tsx

@@ -10,12 +10,14 @@ import { useRootSetting } from '/@/hooks/setting/useRootSetting';
 import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
 import { useCache } from './useCache';
 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
+// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 
 interface DefaultContext {
   Component: FunctionalComponent & { type: { [key: string]: any } };
   route: RouteLocation;
 }
 
+// const FrameLayout=createAsyncComponent(()=>'/@/layouts/iframe/index.vue')
 export default defineComponent({
   name: 'PageLayout',
   setup() {

+ 0 - 1
src/layouts/page/useCache.ts

@@ -32,7 +32,6 @@ export function useCache(isPage: boolean) {
 
     if (isPage) {
       //  page Layout
-      // not parent layout
       return cached.get(PAGE_LAYOUT_KEY) || [];
     }
     const cacheSet = new Set<string>();

+ 2 - 2
src/locales/lang/en/layout/multipleTab.ts

@@ -1,6 +1,6 @@
 export default {
-  redo: 'Refresh',
-  close: 'Close',
+  redo: 'Refresh current',
+  close: 'Close current',
   closeLeft: 'Close Left',
   closeRight: 'Close Right',
   closeOther: 'Close Other',

+ 2 - 2
src/locales/lang/zh_CN/layout/multipleTab.ts

@@ -1,6 +1,6 @@
 export default {
-  redo: '刷新',
-  close: '关闭',
+  redo: '刷新当前',
+  close: '关闭当前',
   closeLeft: '关闭左侧',
   closeRight: '关闭右侧',
   closeOther: '关闭其他',

+ 1 - 1
src/router/constant.ts

@@ -6,7 +6,7 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception');
 /**
  * @description: default layout
  */
-export const LAYOUT = () => import('/@/layouts/default/index');
+export const LAYOUT = () => import('/@/layouts/default/index.vue');
 
 /**
  * @description: page-layout