Commit 27e50b47479af8eaeb4be020aeb0fcbdb4308295

Authored by vben
1 parent ed41e508

perf(tabs): perf multiple-tabs

Showing 33 changed files with 575 additions and 383 deletions
src/components/Dropdown/index.ts
1 1 import { withInstall } from '../util';
2 2  
3   -import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
4   -export const Dropdown = createAsyncComponent(() => import('./src/Dropdown'));
  3 +import Dropdown from './src/Dropdown';
5 4  
6 5 withInstall(Dropdown);
7 6 export * from './src/types';
  7 +export { Dropdown };
... ...
src/components/Menu/src/BasicMenu.tsx
... ... @@ -243,6 +243,7 @@ export default defineComponent({
243 243 onOpenChange={handleOpenChange}
244 244 class={unref(getMenuClass)}
245 245 onClick={handleMenuClick}
  246 + subMenuOpenDelay={0.2}
246 247 {...unref(getInlineCollapseOptions)}
247 248 >
248 249 {{
... ...
src/hooks/setting/useMenuSetting.ts
... ... @@ -6,6 +6,7 @@ import { appStore } from '/@/store/modules/app';
6 6  
7 7 import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
8 8 import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
  9 +import { useFullContent } from '/@/hooks/web/useFullContent';
9 10  
10 11 // Get menu configuration
11 12 const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
... ... @@ -78,6 +79,15 @@ const getCalcContentWidth = computed(() => {
78 79 return `calc(100% - ${unref(width)}px)`;
79 80 });
80 81  
  82 +const { getFullContent: fullContent } = useFullContent();
  83 +
  84 +const getShowSidebar = computed(() => {
  85 + return (
  86 + unref(getSplit) ||
  87 + (unref(getShowMenu) && unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && !unref(fullContent))
  88 + );
  89 +});
  90 +
81 91 // Set menu configuration
82 92 function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
83 93 appStore.commitProjectConfigState({ menuSetting });
... ... @@ -119,5 +129,6 @@ export function useMenuSetting() {
119 129 getMenuHidden,
120 130 getIsTopMenu,
121 131 getMenuBgColor,
  132 + getShowSidebar,
122 133 };
123 134 }
... ...
src/layouts/default/LayoutTrigger.tsx deleted 100644 → 0
1   -import type { FunctionalComponent } from 'vue';
2   -
3   -import { defineComponent, unref } from 'vue';
4   -
5   -import {
6   - DoubleRightOutlined,
7   - DoubleLeftOutlined,
8   - MenuUnfoldOutlined,
9   - MenuFoldOutlined,
10   -} from '@ant-design/icons-vue';
11   -
12   -import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
13   -import { propTypes } from '/@/utils/propTypes';
14   -
15   -const SiderTrigger: FunctionalComponent = () => {
16   - const { getCollapsed } = useMenuSetting();
17   - return unref(getCollapsed) ? <DoubleRightOutlined /> : <DoubleLeftOutlined />;
18   -};
19   -
20   -const HeaderTrigger: FunctionalComponent<{
21   - theme?: string;
22   -}> = (props) => {
23   - const { toggleCollapsed, getCollapsed } = useMenuSetting();
24   - return (
25   - <span class={['layout-trigger', props.theme]} onClick={toggleCollapsed}>
26   - {unref(getCollapsed) ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
27   - </span>
28   - );
29   -};
30   -
31   -export default defineComponent({
32   - name: 'LayoutTrigger',
33   - props: {
34   - sider: propTypes.bool.def(true),
35   - theme: propTypes.oneOf(['light', 'dark']),
36   - },
37   - setup(props) {
38   - return () => {
39   - return props.sider ? <SiderTrigger /> : <HeaderTrigger theme={props.theme} />;
40   - };
41   - },
42   -});
src/layouts/default/content/index.vue
... ... @@ -6,7 +6,7 @@
6 6 :loading="getPageLoading"
7 7 background="rgba(240, 242, 245, 0.6)"
8 8 absolute
9   - :class="`${prefixCls}__loading`"
  9 + :class="`${prefixCls}-loading`"
10 10 />
11 11 </transition>
12 12 <PageLayout />
... ... @@ -53,7 +53,7 @@
53 53 margin: 0 auto;
54 54 }
55 55  
56   - &__loading {
  56 + &-loading {
57 57 position: absolute;
58 58 top: 200px;
59 59 z-index: @page-loading-z-index;
... ...
src/layouts/default/feature/index.vue 0 → 100644
  1 +<template>
  2 + <LayoutLockPage />
  3 + <BackTop v-if="getUseOpenBackTop" :target="getTarget" />
  4 + <SettingDrawer v-if="getShowSettingButton" />
  5 +</template>
  6 +<script lang="ts">
  7 + import { defineComponent } from 'vue';
  8 + import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
  9 + import { BackTop } from 'ant-design-vue';
  10 + import { useRootSetting } from '/@/hooks/setting/useRootSetting';
  11 +
  12 + export default defineComponent({
  13 + name: 'LayoutFeatures',
  14 + components: {
  15 + BackTop,
  16 + LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')),
  17 + SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')),
  18 + },
  19 + setup() {
  20 + const { getUseOpenBackTop, getShowSettingButton } = useRootSetting();
  21 +
  22 + return {
  23 + getTarget: () => document.body,
  24 + getUseOpenBackTop,
  25 + getShowSettingButton,
  26 + };
  27 + },
  28 + });
  29 +</script>
... ...
src/layouts/default/footer/index.less deleted 100644 → 0
1   -@normal-color: rgba(0, 0, 0, 0.45);
2   -
3   -@hover-color: rgba(0, 0, 0, 0.85);
4   -
5   -.layout-footer {
6   - color: @normal-color;
7   - text-align: center;
8   -
9   - &__links {
10   - margin-bottom: 8px;
11   -
12   - a {
13   - color: @normal-color;
14   -
15   - &:hover {
16   - color: @hover-color;
17   - }
18   - }
19   -
20   - .github {
21   - margin: 0 30px;
22   -
23   - &:hover {
24   - color: @hover-color;
25   - }
26   - }
27   - }
28   -}
src/layouts/default/footer/index.tsx deleted 100644 → 0
1   -import './index.less';
2   -
3   -import { defineComponent } from 'vue';
4   -import { Layout } from 'ant-design-vue';
5   -
6   -import { GithubFilled } from '@ant-design/icons-vue';
7   -
8   -import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
9   -import { openWindow } from '/@/utils';
10   -
11   -import { useI18n } from '/@/hooks/web/useI18n';
12   -
13   -export default defineComponent({
14   - name: 'LayoutContent',
15   - setup() {
16   - const { t } = useI18n();
17   - return () => {
18   - return (
19   - <Layout.Footer class="layout-footer">
20   - {() => (
21   - <>
22   - <div class="layout-footer__links">
23   - <a onClick={() => openWindow(SITE_URL)}>{t('layout.footer.onlinePreview')}</a>
24   - <GithubFilled onClick={() => openWindow(GITHUB_URL)} class="github" />
25   - <a onClick={() => openWindow(DOC_URL)}>{t('layout.footer.onlineDocument')}</a>
26   - </div>
27   - <div>Copyright &copy;2020 Vben Admin</div>
28   - </>
29   - )}
30   - </Layout.Footer>
31   - );
32   - };
33   - },
34   -});
src/layouts/default/footer/index.vue 0 → 100644
  1 +<template>
  2 + <Footer :class="prefixCls" v-if="getShowLayoutFooter">
  3 + <div :class="`${prefixCls}__links`">
  4 + <a @click="openWindow(SITE_URL)">{{ t('layout.footer.onlinePreview') }}</a>
  5 + <GithubFilled @click="openWindow(GITHUB_URL)" :class="`${prefixCls}__github`" />
  6 + <a @click="openWindow(DOC_URL)">{{ t('layout.footer.onlineDocument') }}</a>
  7 + </div>
  8 + <div>Copyright &copy;2020 Vben Admin</div>
  9 + </Footer>
  10 +</template>
  11 +
  12 +<script lang="ts">
  13 + import { computed, defineComponent, unref } from 'vue';
  14 + import { Layout } from 'ant-design-vue';
  15 +
  16 + import { GithubFilled } from '@ant-design/icons-vue';
  17 +
  18 + import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
  19 + import { openWindow } from '/@/utils';
  20 +
  21 + import { useI18n } from '/@/hooks/web/useI18n';
  22 + import { useRootSetting } from '/@/hooks/setting/useRootSetting';
  23 + import { useRouter } from 'vue-router';
  24 + import { useDesign } from '/@/hooks/web/useDesign';
  25 +
  26 + export default defineComponent({
  27 + name: 'LayoutFooter',
  28 + components: { Footer: Layout.Footer, GithubFilled },
  29 + setup() {
  30 + const { t } = useI18n();
  31 + const { getShowFooter } = useRootSetting();
  32 + const { currentRoute } = useRouter();
  33 + const { prefixCls } = useDesign('layout-footer');
  34 +
  35 + const getShowLayoutFooter = computed(() => {
  36 + return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
  37 + });
  38 + return { getShowLayoutFooter, prefixCls, t, DOC_URL, GITHUB_URL, SITE_URL, openWindow };
  39 + },
  40 + });
  41 +</script>
  42 +<style lang="less" scoped>
  43 + @import (reference) '../../../design/index.less';
  44 + @prefix-cls: ~'@{namespace}-layout-footer';
  45 +
  46 + @normal-color: rgba(0, 0, 0, 0.45);
  47 +
  48 + @hover-color: rgba(0, 0, 0, 0.85);
  49 +
  50 + .@{prefix-cls} {
  51 + color: @normal-color;
  52 + text-align: center;
  53 +
  54 + &__links {
  55 + margin-bottom: 8px;
  56 +
  57 + a {
  58 + color: @normal-color;
  59 +
  60 + &:hover {
  61 + color: @hover-color;
  62 + }
  63 + }
  64 + }
  65 +
  66 + &__github {
  67 + margin: 0 30px;
  68 +
  69 + &:hover {
  70 + color: @hover-color;
  71 + }
  72 + }
  73 + }
  74 +</style>
... ...
src/layouts/default/header/LayoutHeader.tsx
... ... @@ -19,7 +19,7 @@ import UserDropdown from &#39;./UserDropdown&#39;;
19 19 import LayoutMenu from '../menu';
20 20 import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
21 21 import LockAction from './actions/LockAction';
22   -import LayoutTrigger from '../LayoutTrigger';
  22 +import LayoutTrigger from '../trigger/index.vue';
23 23 import NoticeAction from './notice/NoticeActionItem.vue';
24 24 import {
25 25 RedoOutlined,
... ...
src/layouts/default/header/LayoutMultipleHeader.tsx
... ... @@ -3,7 +3,7 @@ import &#39;./LayoutMultipleHeader.less&#39;;
3 3 import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue';
4 4  
5 5 import LayoutHeader from './LayoutHeader';
6   -import MultipleTabs from '../multitabs/index';
  6 +import MultipleTabs from '../tabs/index.vue';
7 7  
8 8 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
9 9 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
... ...
src/layouts/default/header/index.less
1 1 @import (reference) '../../../design/index.less';
  2 +@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
2 3  
3 4 .layout-header {
4 5 display: flex;
... ... @@ -24,7 +25,7 @@
24 25 height: 100%;
25 26 align-items: center;
26 27  
27   - .layout-trigger {
  28 + .@{header-trigger-prefix-cls} {
28 29 display: flex;
29 30 height: 100%;
30 31 padding: 1px 10px 0 16px;
... ...
src/layouts/default/index.tsx
1 1 import './index.less';
2 2  
3   -import { defineComponent, unref, computed, ref } from 'vue';
4   -import { Layout, BackTop } from 'ant-design-vue';
5   -import LayoutHeader from './header/LayoutHeader';
  3 +import { defineComponent, unref, ref } from 'vue';
  4 +import { Layout } from 'ant-design-vue';
  5 +import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
6 6  
  7 +import LayoutHeader from './header/LayoutHeader';
7 8 import LayoutContent from './content/index.vue';
8   -import LayoutFooter from './footer';
9   -import LayoutLockPage from '/@/views/sys/lock/index.vue';
10 9 import LayoutSideBar from './sider';
11   -import SettingBtn from './setting/index.vue';
12 10 import LayoutMultipleHeader from './header/LayoutMultipleHeader';
13 11  
14   -import { MenuModeEnum } from '/@/enums/menuEnum';
15   -
16   -import { useRouter } from 'vue-router';
17   -import { useFullContent } from '/@/hooks/web/useFullContent';
18 12 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
19 13 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
20   -import { useRootSetting } from '/@/hooks/setting/useRootSetting';
21 14 import { createLayoutContext } from './useLayoutContext';
22 15  
23 16 import { registerGlobComp } from '/@/components/registerGlobComp';
24 17 import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
25 18 import { isMobile } from '/@/utils/is';
  19 +
  20 +const LayoutFeatures = createAsyncComponent(() => import('/@/layouts/default/feature/index.vue'));
  21 +const LayoutFooter = createAsyncComponent(() => import('/@/layouts/default/footer/index.vue'));
  22 +
26 23 export default defineComponent({
27 24 name: 'DefaultLayout',
28 25 setup() {
29   - const { currentRoute } = useRouter();
30 26 const headerRef = ref<ComponentRef>(null);
31 27 const isMobileRef = ref(false);
32 28  
... ... @@ -43,56 +39,27 @@ export default defineComponent({
43 39  
44 40 const { getShowFullHeaderRef } = useHeaderSetting();
45 41  
46   - const { getUseOpenBackTop, getShowSettingButton, getShowFooter } = useRootSetting();
47   -
48   - const { getShowMenu, getMenuMode, getSplit } = useMenuSetting();
49   -
50   - const { getFullContent } = useFullContent();
51   -
52   - const getShowLayoutFooter = computed(() => {
53   - return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
54   - });
55   -
56   - const showSideBarRef = computed(() => {
57   - return (
58   - unref(getSplit) ||
59   - (unref(getShowMenu) &&
60   - unref(getMenuMode) !== MenuModeEnum.HORIZONTAL &&
61   - !unref(getFullContent))
62   - );
63   - });
64   -
65   - function renderFeatures() {
66   - return (
67   - <>
68   - <LayoutLockPage />
69   - {/* back top */}
70   - {unref(getUseOpenBackTop) && <BackTop target={() => document.body} />}
71   - {/* open setting drawer */}
72   - {unref(getShowSettingButton) && <SettingBtn />}
73   - </>
74   - );
75   - }
  42 + const { getShowSidebar } = useMenuSetting();
76 43  
77 44 return () => {
78 45 return (
79 46 <Layout class="default-layout">
80 47 {() => (
81 48 <>
82   - {renderFeatures()}
  49 + <LayoutFeatures />
83 50  
84 51 {unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />}
85 52  
86 53 <Layout>
87 54 {() => (
88 55 <>
89   - {unref(showSideBarRef) && <LayoutSideBar />}
  56 + {unref(getShowSidebar) && <LayoutSideBar />}
90 57 <Layout class="default-layout__main">
91 58 {() => (
92 59 <>
93 60 <LayoutMultipleHeader />
94 61 <LayoutContent />
95   - {unref(getShowLayoutFooter) && <LayoutFooter />}
  62 + <LayoutFooter />
96 63 </>
97 64 )}
98 65 </Layout>
... ...
src/layouts/default/index.vue 0 → 100644
  1 +<template>
  2 + <Layout :class="prefixCls">
  3 + <LayoutFeatures />
  4 + <LayoutHeader fixed ref="headerRef" v-if="getShowFullHeaderRef" />
  5 + <Layout>
  6 + <LayoutSideBar v-if="getShowSidebar" />
  7 + <Layout :class="`${prefixCls}__main`">
  8 + <LayoutMultipleHeader />
  9 + <LayoutContent />
  10 + <LayoutFooter />
  11 + </Layout>
  12 + </Layout>
  13 + </Layout>
  14 +</template>
  15 +
  16 +<script lang="ts">
  17 + import { defineComponent, ref } from 'vue';
  18 + import { Layout } from 'ant-design-vue';
  19 + import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
  20 +
  21 + import LayoutHeader from './header/LayoutHeader';
  22 + import LayoutContent from './content/index.vue';
  23 + import LayoutSideBar from './sider';
  24 + import LayoutMultipleHeader from './header/LayoutMultipleHeader';
  25 +
  26 + import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
  27 + import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
  28 + import { useDesign } from '/@/hooks/web/useDesign';
  29 + import { createLayoutContext } from './useLayoutContext';
  30 +
  31 + import { registerGlobComp } from '/@/components/registerGlobComp';
  32 + import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
  33 + import { isMobile } from '/@/utils/is';
  34 +
  35 + export default defineComponent({
  36 + name: 'DefaultLayout',
  37 + components: {
  38 + LayoutFeatures: createAsyncComponent(() => import('/@/layouts/default/feature/index.vue')),
  39 + LayoutFooter: createAsyncComponent(() => import('/@/layouts/default/footer/index.vue')),
  40 + LayoutHeader,
  41 + LayoutContent,
  42 + LayoutSideBar,
  43 + LayoutMultipleHeader,
  44 + Layout,
  45 + },
  46 + setup() {
  47 + const headerRef = ref<ComponentRef>(null);
  48 + const isMobileRef = ref(false);
  49 +
  50 + const { prefixCls } = useDesign('default-layout');
  51 +
  52 + createLayoutContext({ fullHeader: headerRef, isMobile: isMobileRef });
  53 +
  54 + createBreakpointListen(() => {
  55 + isMobileRef.value = isMobile();
  56 + });
  57 +
  58 + // ! Only register global components here
  59 + // ! Can reduce the size of the first screen code
  60 + // default layout It is loaded after login. So it won’t be packaged to the first screen
  61 + registerGlobComp();
  62 +
  63 + const { getShowFullHeaderRef } = useHeaderSetting();
  64 +
  65 + const { getShowSidebar } = useMenuSetting();
  66 +
  67 + return {
  68 + getShowFullHeaderRef,
  69 + getShowSidebar,
  70 + headerRef,
  71 + prefixCls,
  72 + };
  73 + },
  74 + });
  75 +</script>
  76 +<style lang="less">
  77 + @import (reference) '../../design/index.less';
  78 + @prefix-cls: ~'@{namespace}-default-layout';
  79 +
  80 + .@{prefix-cls} {
  81 + display: flex;
  82 + width: 100%;
  83 + min-height: 100%;
  84 + background: @content-bg;
  85 + flex-direction: column;
  86 +
  87 + > .ant-layout {
  88 + min-height: 100%;
  89 + }
  90 +
  91 + &__main {
  92 + margin-left: 1px;
  93 + }
  94 + }
  95 +</style>
... ...
src/layouts/default/setting/index.vue
... ... @@ -13,7 +13,7 @@
13 13 import { useDesign } from '/@/hooks/web/useDesign';
14 14  
15 15 export default defineComponent({
16   - name: 'SettingBtn',
  16 + name: 'SettingButton',
17 17 components: { SettingOutlined, SettingDrawer },
18 18 setup() {
19 19 const [register, { openDrawer }] = useDrawer();
... ...
src/layouts/default/sider/useLayoutSider.tsx
1 1 import type { Ref } from 'vue';
2 2  
3 3 import { computed, unref, onMounted, nextTick, ref } from 'vue';
4   -import LayoutTrigger from '/@/layouts/default/LayoutTrigger';
  4 +import LayoutTrigger from '/@/layouts/default/trigger/index.vue';
5 5  
6 6 import { TriggerEnum } from '/@/enums/menuEnum';
7 7  
... ...
src/layouts/default/tabs/components/QuickButton.vue 0 → 100644
  1 +<template>
  2 + <TabContent :type="TabContentEnum.EXTRA_TYPE" :tabItem="$route" />
  3 +</template>
  4 +<script lang="ts">
  5 + import { defineComponent } from 'vue';
  6 +
  7 + import { TabContentEnum } from '../types';
  8 +
  9 + import TabContent from './TabContent.vue';
  10 + export default defineComponent({
  11 + name: 'QuickButton',
  12 + components: {
  13 + TabContent,
  14 + },
  15 + setup() {
  16 + return {
  17 + TabContentEnum,
  18 + };
  19 + },
  20 + });
  21 +</script>
... ...
src/layouts/default/multitabs/TabContent.tsx renamed to src/layouts/default/tabs/components/TabContent.vue
1   -import type { PropType } from 'vue';
2   -import { Dropdown } from '/@/components/Dropdown/index';
3   -
4   -import { defineComponent, unref, FunctionalComponent } from 'vue';
  1 +<template>
  2 + <Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent">
  3 + <div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="isTabs">
  4 + <span class="ml-1">{{ getTitle }}</span>
  5 + </div>
5 6  
6   -import { TabContentProps } from './types';
  7 + <span :class="`${prefixCls}__extra`" v-else>
  8 + <RightOutlined />
  9 + </span>
  10 + </Dropdown>
  11 +</template>
  12 +<script lang="ts">
  13 + import type { PropType } from 'vue';
7 14  
8   -import { RightOutlined } from '@ant-design/icons-vue';
  15 + import { defineComponent, computed } from 'vue';
  16 + import { Dropdown } from '/@/components/Dropdown/index';
9 17  
10   -import { TabContentEnum } from './types';
  18 + import { TabContentProps, TabContentEnum } from '../types';
11 19  
12   -import { useTabDropdown } from './useTabDropdown';
13   -import { useI18n } from '/@/hooks/web/useI18n';
  20 + import { RightOutlined } from '@ant-design/icons-vue';
14 21  
15   -import { RouteLocationNormalized } from 'vue-router';
  22 + import { useDesign } from '/@/hooks/web/useDesign';
  23 + import { useTabDropdown } from '../useTabDropdown';
  24 + import { useI18n } from '/@/hooks/web/useI18n';
16 25  
17   -const { t: titleT } = useI18n();
  26 + import { RouteLocationNormalized } from 'vue-router';
  27 + export default defineComponent({
  28 + name: 'TabContent',
  29 + components: { Dropdown, RightOutlined },
  30 + props: {
  31 + tabItem: {
  32 + type: Object as PropType<RouteLocationNormalized>,
  33 + default: null,
  34 + },
18 35  
19   -const ExtraContent: FunctionalComponent = () => {
20   - return (
21   - <span class={`multiple-tabs-content__extra `}>
22   - <RightOutlined />
23   - </span>
24   - );
25   -};
  36 + type: {
  37 + type: Number as PropType<TabContentEnum>,
  38 + default: TabContentEnum.TAB_TYPE,
  39 + },
  40 + },
  41 + setup(props) {
  42 + const { prefixCls } = useDesign('multiple-tabs-content');
  43 + const { t } = useI18n();
26 44  
27   -const TabContent: FunctionalComponent<{ tabItem: RouteLocationNormalized; handler: Fn }> = (
28   - props
29   -) => {
30   - const { tabItem: { meta } = {} } = props;
  45 + const getTitle = computed(() => {
  46 + const { tabItem: { meta } = {} } = props;
  47 + return meta && t(meta.title);
  48 + });
31 49  
32   - return (
33   - <div class={`multiple-tabs-content__content `} onContextmenu={props.handler(props.tabItem)}>
34   - <span class="ml-1">{meta && titleT(meta.title)}</span>
35   - </div>
36   - );
37   -};
  50 + const {
  51 + getDropMenuList,
  52 + handleMenuEvent,
  53 + handleContextMenu,
  54 + getTrigger,
  55 + isTabs,
  56 + } = useTabDropdown(props as TabContentProps);
38 57  
39   -export default defineComponent({
40   - name: 'TabContent',
41   - props: {
42   - tabItem: {
43   - type: Object as PropType<RouteLocationNormalized>,
44   - default: null,
  58 + function handleContext(e: ChangeEvent) {
  59 + props.tabItem && handleContextMenu(props.tabItem)(e);
  60 + }
  61 + return {
  62 + prefixCls,
  63 + getDropMenuList,
  64 + handleMenuEvent,
  65 + handleContext,
  66 + getTrigger,
  67 + isTabs,
  68 + getTitle,
  69 + };
45 70 },
46   -
47   - type: {
48   - type: Number as PropType<TabContentEnum>,
49   - default: TabContentEnum.TAB_TYPE,
50   - },
51   - },
52   - setup(props) {
53   - const {
54   - getDropMenuList,
55   - handleMenuEvent,
56   - handleContextMenu,
57   - getTrigger,
58   - isTabs,
59   - } = useTabDropdown(props as TabContentProps);
60   -
61   - return () => {
62   - return (
63   - <Dropdown
64   - dropMenuList={unref(getDropMenuList)}
65   - trigger={unref(getTrigger)}
66   - onMenuEvent={handleMenuEvent}
67   - >
68   - {() => {
69   - if (!unref(isTabs)) {
70   - return <ExtraContent />;
71   - }
72   - return <TabContent handler={handleContextMenu} tabItem={props.tabItem} />;
73   - }}
74   - </Dropdown>
75   - );
76   - };
77   - },
78   -});
  71 + });
  72 +</script>
... ...
src/layouts/default/multitabs/index.less renamed to src/layouts/default/tabs/index.less
1 1 @import (reference) '../../../design/index.less';
  2 +@prefix-cls: ~'@{namespace}-multiple-tabs';
2 3  
3   -.multiple-tabs {
  4 +.@{prefix-cls} {
4 5 z-index: 10;
5 6 height: @multiple-height + 2;
6   - padding: 0 0 2px 0;
7   - margin-left: -1px;
8 7 line-height: @multiple-height + 2;
9 8 background: @white;
10 9 box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
... ... @@ -32,13 +31,33 @@
32 31 line-height: calc(@multiple-height - 2px);
33 32 color: @text-color-call-out;
34 33 background: @white;
35   - border: 1px solid darken(@border-color-light, 8%);
  34 + border: 1px solid darken(@border-color-light, 6%);
36 35 transition: none;
37 36  
  37 + &:not(.ant-tabs-tab-active)::before {
  38 + position: absolute;
  39 + top: -1px;
  40 + left: 50%;
  41 + width: 100%;
  42 + height: 2px;
  43 + background-color: @primary-color;
  44 + content: '';
  45 + opacity: 0;
  46 + transform: translate(-50%, 0) scaleX(0);
  47 + transform-origin: center;
  48 + transition: none;
  49 + }
  50 +
38 51 &:hover {
39 52 .ant-tabs-close-x {
40 53 opacity: 1;
41 54 }
  55 +
  56 + &:not(.ant-tabs-tab-active)::before {
  57 + opacity: 1;
  58 + transform: translate(-50%, 0) scaleX(1);
  59 + transition: all 0.3s ease-in-out;
  60 + }
42 61 }
43 62  
44 63 .ant-tabs-close-x {
... ... @@ -51,7 +70,7 @@
51 70  
52 71 &:hover {
53 72 svg {
54   - width: 0.75em;
  73 + width: 0.8em;
55 74 }
56 75 }
57 76 }
... ... @@ -73,6 +92,7 @@
73 92 color: @white;
74 93 background: fade(@primary-color, 100%);
75 94 border: 0;
  95 + transition: none;
76 96  
77 97 &::before {
78 98 position: absolute;
... ... @@ -98,7 +118,7 @@
98 118 }
99 119  
100 120 .ant-tabs-nav > div:nth-child(1) {
101   - padding: 0 10px;
  121 + padding: 0 6px;
102 122  
103 123 .ant-tabs-tab {
104 124 margin-right: 3px !important;
... ... @@ -124,36 +144,42 @@
124 144 .ant-dropdown-trigger {
125 145 display: inline-flex;
126 146 }
127   -}
128 147  
129   -.multiple-tabs-content {
130   - &__extra {
131   - display: inline-block;
132   - width: @multiple-height;
133   - height: @multiple-height;
134   - line-height: @multiple-height;
135   - color: #999;
136   - text-align: center;
137   - cursor: pointer;
138   - border-left: 1px solid #eee;
139   -
140   - &:hover {
141   - color: @text-color-base;
  148 + &--hide-close {
  149 + .ant-tabs-close-x {
  150 + opacity: 0 !important;
142 151 }
  152 + }
  153 +
  154 + &-content {
  155 + &__extra {
  156 + display: inline-block;
  157 + width: @multiple-height;
  158 + height: @multiple-height;
  159 + line-height: @multiple-height;
  160 + color: #999;
  161 + text-align: center;
  162 + cursor: pointer;
  163 + border-left: 1px solid #eee;
  164 +
  165 + &:hover {
  166 + color: @text-color-base;
  167 + }
143 168  
144   - span[role='img'] {
145   - transform: rotate(90deg);
  169 + span[role='img'] {
  170 + transform: rotate(90deg);
  171 + }
146 172 }
147   - }
148 173  
149   - &__content {
150   - display: inline-block;
151   - width: 100%;
152   - height: @multiple-height - 2;
153   - padding-left: 0;
154   - margin-left: -10px;
155   - font-size: 12px;
156   - cursor: pointer;
157   - user-select: none;
  174 + &__info {
  175 + display: inline-block;
  176 + width: 100%;
  177 + height: @multiple-height - 2;
  178 + padding-left: 0;
  179 + margin-left: -10px;
  180 + font-size: 12px;
  181 + cursor: pointer;
  182 + user-select: none;
  183 + }
158 184 }
159 185 }
... ...
src/layouts/default/multitabs/index.tsx renamed to src/layouts/default/tabs/index.vue
1   -import './index.less';
2   -
3   -import type { TabContentProps } from './types';
4   -
5   -import { defineComponent, watch, computed, unref, ref } from 'vue';
6   -import { useRouter } from 'vue-router';
7   -
8   -import { Tabs } from 'ant-design-vue';
9   -import TabContent from './TabContent';
10   -
11   -import { useGo } from '/@/hooks/web/usePage';
12   -
13   -import { TabContentEnum } from './types';
14   -
15   -import { tabStore } from '/@/store/modules/tab';
16   -import { userStore } from '/@/store/modules/user';
17   -
18   -import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
19   -import { REDIRECT_NAME } from '/@/router/constant';
20   -
21   -export default defineComponent({
22   - name: 'MultipleTabs',
23   - setup() {
24   - const activeKeyRef = ref('');
25   -
26   - const affixTextList = initAffixTabs();
  1 +<template>
  2 + <div :class="getWrapClass">
  3 + <Tabs
  4 + type="editable-card"
  5 + size="small"
  6 + :animated="false"
  7 + :hideAdd="true"
  8 + :tabBarGutter="3"
  9 + :activeKey="activeKeyRef"
  10 + @change="handleChange"
  11 + @edit="handleEdit"
  12 + >
  13 + <template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">
  14 + <TabPane :closable="!(item && item.meta && item.meta.affix)">
  15 + <template #tab>
  16 + <TabContent :tabItem="item" />
  17 + </template>
  18 + </TabPane>
  19 + </template>
  20 + <template #tabBarExtraContent>
  21 + <QuickButton />
  22 + </template>
  23 + </Tabs>
  24 + </div>
  25 +</template>
  26 +<script lang="ts">
  27 + import { defineComponent, watch, computed, unref, ref } from 'vue';
  28 +
  29 + import { Tabs } from 'ant-design-vue';
  30 + import TabContent from './components/TabContent.vue';
  31 +
  32 + import { useGo } from '/@/hooks/web/usePage';
  33 +
  34 + import { tabStore } from '/@/store/modules/tab';
  35 + import { userStore } from '/@/store/modules/user';
  36 +
  37 + import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
  38 + import { REDIRECT_NAME } from '/@/router/constant';
  39 + import { useDesign } from '/@/hooks/web/useDesign';
  40 + import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
  41 +
  42 + export default defineComponent({
  43 + name: 'MultipleTabs',
  44 + components: {
  45 + QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')),
  46 + Tabs,
  47 + TabPane: Tabs.TabPane,
  48 + TabContent,
  49 + },
  50 + setup() {
  51 + const affixTextList = initAffixTabs();
  52 +
  53 + const activeKeyRef = ref('');
  54 +
  55 + useTabsDrag(affixTextList);
  56 + const { prefixCls } = useDesign('multiple-tabs');
  57 + const go = useGo();
  58 +
  59 + const getTabsState = computed(() => tabStore.getTabsState);
  60 +
  61 + const unClose = computed(() => {
  62 + return getTabsState.value.length === 1;
  63 + });
27 64  
28   - useTabsDrag(affixTextList);
  65 + const getWrapClass = computed(() => {
  66 + return [
  67 + prefixCls,
  68 + {
  69 + [`${prefixCls}--hide-close`]: unClose,
  70 + },
  71 + ];
  72 + });
29 73  
30   - const go = useGo();
  74 + watch(
  75 + () => tabStore.getLastChangeRouteState?.path,
  76 + () => {
  77 + if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
  78 + return;
  79 + }
  80 + const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
  81 + if (!lastChangeRoute || !userStore.getTokenState) return;
  82 +
  83 + const { path, fullPath } = lastChangeRoute;
  84 + const p = fullPath || path;
  85 +
  86 + if (activeKeyRef.value !== p) {
  87 + activeKeyRef.value = p;
  88 + }
  89 +
  90 + tabStore.addTabAction(lastChangeRoute);
  91 + },
  92 + {
  93 + immediate: true,
  94 + }
  95 + );
31 96  
32   - const { currentRoute } = useRouter();
  97 + function handleChange(activeKey: any) {
  98 + activeKeyRef.value = activeKey;
  99 + go(activeKey, false);
  100 + }
33 101  
34   - const getTabsState = computed(() => tabStore.getTabsState);
  102 + // Close the current tab
  103 + function handleEdit(targetKey: string) {
  104 + // Added operation to hide, currently only use delete operation
  105 + if (unref(unClose)) return;
35 106  
36   - watch(
37   - () => tabStore.getLastChangeRouteState?.path,
38   - () => {
39   - if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
40   - return;
41   - }
42   - const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
43   - if (!lastChangeRoute || !userStore.getTokenState) return;
44   - const { path, fullPath } = lastChangeRoute;
45   - const p = fullPath || path;
46   - if (activeKeyRef.value !== p) {
47   - activeKeyRef.value = p;
48   - }
49   - tabStore.addTabAction(lastChangeRoute);
50   - },
51   - {
52   - immediate: true,
  107 + tabStore.closeTabByKeyAction(targetKey);
53 108 }
54   - );
55   -
56   - function handleChange(activeKey: any) {
57   - activeKeyRef.value = activeKey;
58   - go(activeKey, false);
59   - }
60   -
61   - // Close the current tab
62   - function handleEdit(targetKey: string) {
63   - // Added operation to hide, currently only use delete operation
64   - tabStore.closeTabByKeyAction(targetKey);
65   - }
66   -
67   - function renderQuick() {
68   - const tabContentProps: TabContentProps = {
69   - tabItem: currentRoute.value,
70   - type: TabContentEnum.EXTRA_TYPE,
  109 + return {
  110 + prefixCls,
  111 + unClose,
  112 + getWrapClass,
  113 + handleEdit,
  114 + handleChange,
  115 + activeKeyRef,
  116 + getTabsState,
71 117 };
72   - return <TabContent {...tabContentProps} />;
73   - }
74   -
75   - function renderTabs() {
76   - return unref(getTabsState).map((item) => {
77   - const key = item.query ? item.fullPath : item.path;
78   - const closable = !(item && item.meta && item.meta.affix);
79   -
80   - const slots = {
81   - tab: () => <TabContent tabItem={item} />,
82   - };
83   - return (
84   - <Tabs.TabPane key={key} closable={closable}>
85   - {slots}
86   - </Tabs.TabPane>
87   - );
88   - });
89   - }
90   -
91   - return () => {
92   - const slots = {
93   - default: () => renderTabs(),
94   - tabBarExtraContent: () => renderQuick(),
95   - };
96   - return (
97   - <div class="multiple-tabs">
98   - <Tabs
99   - type="editable-card"
100   - size="small"
101   - animated={false}
102   - hideAdd={true}
103   - tabBarGutter={3}
104   - activeKey={unref(activeKeyRef)}
105   - onChange={handleChange}
106   - onEdit={handleEdit}
107   - >
108   - {slots}
109   - </Tabs>
110   - </div>
111   - );
112   - };
113   - },
114   -});
  118 + },
  119 + });
  120 +</script>
  121 +<style lang="less">
  122 + @import './index.less';
  123 +</style>
... ...
src/layouts/default/multitabs/types.ts renamed to src/layouts/default/tabs/types.ts
src/layouts/default/multitabs/useMultipleTabs.ts renamed to src/layouts/default/tabs/useMultipleTabs.ts
... ... @@ -2,6 +2,7 @@ import Sortable from &#39;sortablejs&#39;;
2 2 import { toRaw, ref, nextTick, onMounted } from 'vue';
3 3 import { RouteLocationNormalized } from 'vue-router';
4 4 import { useProjectSetting } from '/@/hooks/setting';
  5 +import { useDesign } from '/@/hooks/web/useDesign';
5 6 import router from '/@/router';
6 7 import { tabStore } from '/@/store/modules/tab';
7 8 import { isNullAndUnDef } from '/@/utils/is';
... ... @@ -48,12 +49,12 @@ export function initAffixTabs(): string[] {
48 49 export function useTabsDrag(affixTextList: string[]) {
49 50 const { multiTabsSetting } = useProjectSetting();
50 51  
  52 + const { prefixCls } = useDesign('multiple-tabs');
  53 +
51 54 function initSortableTabs() {
52 55 if (!multiTabsSetting.canDrag) return;
53 56 nextTick(() => {
54   - const el = document.querySelectorAll(
55   - '.multiple-tabs .ant-tabs-nav > div'
56   - )?.[0] as HTMLElement;
  57 + const el = document.querySelectorAll(`.${prefixCls} .ant-tabs-nav > div`)?.[0] as HTMLElement;
57 58  
58 59 if (!el) return;
59 60 Sortable.create(el, {
... ...
src/layouts/default/multitabs/useTabDropdown.ts renamed to src/layouts/default/tabs/useTabDropdown.ts
src/layouts/default/trigger/HeaderTrigger.vue 0 → 100644
  1 +<template>
  2 + <span :class="[prefixCls, theme]" @click="toggleCollapsed">
  3 + <MenuUnfoldOutlined v-if="getCollapsed" /> <MenuFoldOutlined v-else />
  4 + </span>
  5 +</template>
  6 +<script lang="ts">
  7 + import { defineComponent } from 'vue';
  8 + import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
  9 + import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
  10 + import { useDesign } from '/@/hooks/web/useDesign';
  11 + import { propTypes } from '/@/utils/propTypes';
  12 +
  13 + export default defineComponent({
  14 + name: 'SiderTrigger',
  15 + components: { MenuUnfoldOutlined, MenuFoldOutlined },
  16 + props: {
  17 + theme: propTypes.oneOf(['light', 'dark']),
  18 + },
  19 + setup() {
  20 + const { getCollapsed, toggleCollapsed } = useMenuSetting();
  21 + const { prefixCls } = useDesign('layout-header-trigger');
  22 + return { getCollapsed, toggleCollapsed, prefixCls };
  23 + },
  24 + });
  25 +</script>
... ...
src/layouts/default/trigger/SiderTrigger.vue 0 → 100644
  1 +<template>
  2 + <DoubleRightOutlined v-if="getCollapsed" />
  3 + <DoubleLeftOutlined v-else />
  4 +</template>
  5 +<script lang="ts">
  6 + import { defineComponent } from 'vue';
  7 + import { DoubleRightOutlined, DoubleLeftOutlined } from '@ant-design/icons-vue';
  8 + import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
  9 +
  10 + export default defineComponent({
  11 + name: 'SiderTrigger',
  12 + components: { DoubleRightOutlined, DoubleLeftOutlined },
  13 + setup() {
  14 + const { getCollapsed } = useMenuSetting();
  15 + return { getCollapsed };
  16 + },
  17 + });
  18 +</script>
... ...
src/layouts/default/trigger/index.vue 0 → 100644
  1 +<template>
  2 + <SiderTrigger v-if="sider" />
  3 + <HeaderTrigger v-else :theme="theme" />
  4 +</template>
  5 +<script lang="ts">
  6 + import { defineComponent } from 'vue';
  7 + import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
  8 + import { propTypes } from '/@/utils/propTypes';
  9 +
  10 + export default defineComponent({
  11 + name: 'LayoutTrigger',
  12 + components: {
  13 + SiderTrigger: createAsyncComponent(() => import('./SiderTrigger.vue')),
  14 + HeaderTrigger: createAsyncComponent(() => import('./HeaderTrigger.vue'), { loading: true }),
  15 + },
  16 + props: {
  17 + sider: propTypes.bool.def(true),
  18 + theme: propTypes.oneOf(['light', 'dark']),
  19 + },
  20 + });
  21 +</script>
... ...
src/layouts/iframe/index.vue
1 1 <template>
2   - <template v-for="frame in getFramePages" :key="frame.path">
3   - <FramePage
4   - v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
5   - v-show="showIframe(frame)"
6   - :frameSrc="frame.meta.frameSrc"
7   - />
8   - </template>
  2 + <div>
  3 + <template v-for="frame in getFramePages" :key="frame.path">
  4 + <FramePage
  5 + v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
  6 + v-show="showIframe(frame)"
  7 + :frameSrc="frame.meta.frameSrc"
  8 + />
  9 + </template>
  10 + </div>
9 11 </template>
10 12 <script lang="ts">
11 13 import { defineComponent } from 'vue';
... ...
src/layouts/iframe/useFrameKeepAlive.ts
1 1 import type { AppRouteRecordRaw } from '/@/router/types';
2 2  
3 3 import { computed, toRaw, unref } from 'vue';
4   -import { useRouter } from 'vue-router';
5   -import router from '/@/router';
6 4  
7 5 import { tabStore } from '/@/store/modules/tab';
8 6  
... ... @@ -10,8 +8,10 @@ import { unique } from &#39;/@/utils&#39;;
10 8  
11 9 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
12 10  
  11 +import router from '/@/router';
  12 +
13 13 export function useFrameKeepAlive() {
14   - const { currentRoute } = useRouter();
  14 + const { currentRoute } = router;
15 15 const { getShowMultipleTab } = useMultipleTabSetting();
16 16  
17 17 const getFramePages = computed(() => {
... ...
src/layouts/page/index.tsx
... ... @@ -10,12 +10,14 @@ import { useRootSetting } from &#39;/@/hooks/setting/useRootSetting&#39;;
10 10 import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
11 11 import { useCache } from './useCache';
12 12 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
  13 +// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
13 14  
14 15 interface DefaultContext {
15 16 Component: FunctionalComponent & { type: { [key: string]: any } };
16 17 route: RouteLocation;
17 18 }
18 19  
  20 +// const FrameLayout=createAsyncComponent(()=>'/@/layouts/iframe/index.vue')
19 21 export default defineComponent({
20 22 name: 'PageLayout',
21 23 setup() {
... ...
src/layouts/page/useCache.ts
... ... @@ -32,7 +32,6 @@ export function useCache(isPage: boolean) {
32 32  
33 33 if (isPage) {
34 34 // page Layout
35   - // not parent layout
36 35 return cached.get(PAGE_LAYOUT_KEY) || [];
37 36 }
38 37 const cacheSet = new Set<string>();
... ...
src/locales/lang/en/layout/multipleTab.ts
1 1 export default {
2   - redo: 'Refresh',
3   - close: 'Close',
  2 + redo: 'Refresh current',
  3 + close: 'Close current',
4 4 closeLeft: 'Close Left',
5 5 closeRight: 'Close Right',
6 6 closeOther: 'Close Other',
... ...
src/locales/lang/zh_CN/layout/multipleTab.ts
1 1 export default {
2   - redo: '刷新',
3   - close: '关闭',
  2 + redo: '刷新当前',
  3 + close: '关闭当前',
4 4 closeLeft: '关闭左侧',
5 5 closeRight: '关闭右侧',
6 6 closeOther: '关闭其他',
... ...
src/router/constant.ts
... ... @@ -6,7 +6,7 @@ const EXCEPTION_COMPONENT = () =&gt; import(&#39;../views/sys/exception/Exception&#39;);
6 6 /**
7 7 * @description: default layout
8 8 */
9   -export const LAYOUT = () => import('/@/layouts/default/index');
  9 +export const LAYOUT = () => import('/@/layouts/default/index.vue');
10 10  
11 11 /**
12 12 * @description: page-layout
... ...