vben
2020-12-13 27e50b47479af8eaeb4be020aeb0fcbdb4308295
perf(tabs): perf multiple-tabs
5个文件已删除
9个文件已添加
17个文件已修改
4 文件已重命名
996 ■■■■■ 已修改文件
src/components/Dropdown/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/BasicMenu.tsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/setting/useMenuSetting.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/LayoutTrigger.tsx 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/content/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/feature/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/footer/index.less 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/footer/index.tsx 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/footer/index.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/LayoutHeader.tsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/LayoutMultipleHeader.tsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/index.less 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/index.tsx 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/index.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/TabContent.tsx 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/index.tsx 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/setting/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/sider/useLayoutSider.tsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/components/QuickButton.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/components/TabContent.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/index.less 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/index.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/types.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/useMultipleTabs.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/tabs/useTabDropdown.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/trigger/HeaderTrigger.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/trigger/SiderTrigger.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/trigger/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/iframe/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/iframe/useFrameKeepAlive.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/page/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/page/useCache.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/en/layout/multipleTab.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/zh_CN/layout/multipleTab.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/constant.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 };
src/components/Menu/src/BasicMenu.tsx
@@ -243,6 +243,7 @@
          onOpenChange={handleOpenChange}
          class={unref(getMenuClass)}
          onClick={handleMenuClick}
          subMenuOpenDelay={0.2}
          {...unref(getInlineCollapseOptions)}
        >
          {{
src/hooks/setting/useMenuSetting.ts
@@ -6,6 +6,7 @@
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 @@
  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 @@
    getMenuHidden,
    getIsTopMenu,
    getMenuBgColor,
    getShowSidebar,
  };
}
src/layouts/default/LayoutTrigger.tsx
File was deleted
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;
src/layouts/default/feature/index.vue
New file
@@ -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>
src/layouts/default/footer/index.less
File was deleted
src/layouts/default/footer/index.tsx
File was deleted
src/layouts/default/footer/index.vue
New file
@@ -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>
src/layouts/default/header/LayoutHeader.tsx
@@ -19,7 +19,7 @@
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,
src/layouts/default/header/LayoutMultipleHeader.tsx
@@ -3,7 +3,7 @@
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';
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;
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 @@
    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>
src/layouts/default/index.vue
New file
@@ -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>
src/layouts/default/multitabs/TabContent.tsx
File was deleted
src/layouts/default/multitabs/index.tsx
File was deleted
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();
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';
src/layouts/default/tabs/components/QuickButton.vue
New file
@@ -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>
src/layouts/default/tabs/components/TabContent.vue
New file
@@ -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>
src/layouts/default/tabs/index.less
File was renamed from src/layouts/default/multitabs/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,12 +31,32 @@
        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;
          }
        }
@@ -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;
    }
    span[role='img'] {
      transform: rotate(90deg);
  &--hide-close {
    .ant-tabs-close-x {
      opacity: 0 !important;
    }
  }
  &__content {
    display: inline-block;
    width: 100%;
    height: @multiple-height - 2;
    padding-left: 0;
    margin-left: -10px;
    font-size: 12px;
    cursor: pointer;
    user-select: none;
  &-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);
      }
    }
    &__info {
      display: inline-block;
      width: 100%;
      height: @multiple-height - 2;
      padding-left: 0;
      margin-left: -10px;
      font-size: 12px;
      cursor: pointer;
      user-select: none;
    }
  }
}
src/layouts/default/tabs/index.vue
New file
@@ -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>
src/layouts/default/tabs/types.ts
src/layouts/default/tabs/useMultipleTabs.ts
File was renamed from src/layouts/default/multitabs/useMultipleTabs.ts
@@ -2,6 +2,7 @@
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 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, {
src/layouts/default/tabs/useTabDropdown.ts
src/layouts/default/trigger/HeaderTrigger.vue
New file
@@ -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>
src/layouts/default/trigger/SiderTrigger.vue
New file
@@ -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>
src/layouts/default/trigger/index.vue
New file
@@ -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>
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';
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 { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import router from '/@/router';
export function useFrameKeepAlive() {
  const { currentRoute } = useRouter();
  const { currentRoute } = router;
  const { getShowMultipleTab } = useMultipleTabSetting();
  const getFramePages = computed(() => {
src/layouts/page/index.tsx
@@ -10,12 +10,14 @@
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() {
src/layouts/page/useCache.ts
@@ -32,7 +32,6 @@
    if (isPage) {
      //  page Layout
      // not parent layout
      return cached.get(PAGE_LAYOUT_KEY) || [];
    }
    const cacheSet = new Set<string>();
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',
src/locales/lang/zh_CN/layout/multipleTab.ts
@@ -1,6 +1,6 @@
export default {
  redo: '刷新',
  close: '关闭',
  redo: '刷新当前',
  close: '关闭当前',
  closeLeft: '关闭左侧',
  closeRight: '关闭右侧',
  closeOther: '关闭其他',
src/router/constant.ts
@@ -6,7 +6,7 @@
/**
 * @description: default layout
 */
export const LAYOUT = () => import('/@/layouts/default/index');
export const LAYOUT = () => import('/@/layouts/default/index.vue');
/**
 * @description: page-layout