Commit 27e50b47479af8eaeb4be020aeb0fcbdb4308295
1 parent
ed41e508
perf(tabs): perf multiple-tabs
Showing
33 changed files
with
575 additions
and
383 deletions
src/components/Dropdown/index.ts
1 | import { withInstall } from '../util'; | 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 | withInstall(Dropdown); | 5 | withInstall(Dropdown); |
7 | export * from './src/types'; | 6 | export * from './src/types'; |
7 | +export { Dropdown }; |
src/components/Menu/src/BasicMenu.tsx
@@ -243,6 +243,7 @@ export default defineComponent({ | @@ -243,6 +243,7 @@ export default defineComponent({ | ||
243 | onOpenChange={handleOpenChange} | 243 | onOpenChange={handleOpenChange} |
244 | class={unref(getMenuClass)} | 244 | class={unref(getMenuClass)} |
245 | onClick={handleMenuClick} | 245 | onClick={handleMenuClick} |
246 | + subMenuOpenDelay={0.2} | ||
246 | {...unref(getInlineCollapseOptions)} | 247 | {...unref(getInlineCollapseOptions)} |
247 | > | 248 | > |
248 | {{ | 249 | {{ |
src/hooks/setting/useMenuSetting.ts
@@ -6,6 +6,7 @@ import { appStore } from '/@/store/modules/app'; | @@ -6,6 +6,7 @@ import { appStore } from '/@/store/modules/app'; | ||
6 | 6 | ||
7 | import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum'; | 7 | import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum'; |
8 | import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum'; | 8 | import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum'; |
9 | +import { useFullContent } from '/@/hooks/web/useFullContent'; | ||
9 | 10 | ||
10 | // Get menu configuration | 11 | // Get menu configuration |
11 | const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting); | 12 | const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting); |
@@ -78,6 +79,15 @@ const getCalcContentWidth = computed(() => { | @@ -78,6 +79,15 @@ const getCalcContentWidth = computed(() => { | ||
78 | return `calc(100% - ${unref(width)}px)`; | 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 | // Set menu configuration | 91 | // Set menu configuration |
82 | function setMenuSetting(menuSetting: Partial<MenuSetting>): void { | 92 | function setMenuSetting(menuSetting: Partial<MenuSetting>): void { |
83 | appStore.commitProjectConfigState({ menuSetting }); | 93 | appStore.commitProjectConfigState({ menuSetting }); |
@@ -119,5 +129,6 @@ export function useMenuSetting() { | @@ -119,5 +129,6 @@ export function useMenuSetting() { | ||
119 | getMenuHidden, | 129 | getMenuHidden, |
120 | getIsTopMenu, | 130 | getIsTopMenu, |
121 | getMenuBgColor, | 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,7 +6,7 @@ | ||
6 | :loading="getPageLoading" | 6 | :loading="getPageLoading" |
7 | background="rgba(240, 242, 245, 0.6)" | 7 | background="rgba(240, 242, 245, 0.6)" |
8 | absolute | 8 | absolute |
9 | - :class="`${prefixCls}__loading`" | 9 | + :class="`${prefixCls}-loading`" |
10 | /> | 10 | /> |
11 | </transition> | 11 | </transition> |
12 | <PageLayout /> | 12 | <PageLayout /> |
@@ -53,7 +53,7 @@ | @@ -53,7 +53,7 @@ | ||
53 | margin: 0 auto; | 53 | margin: 0 auto; |
54 | } | 54 | } |
55 | 55 | ||
56 | - &__loading { | 56 | + &-loading { |
57 | position: absolute; | 57 | position: absolute; |
58 | top: 200px; | 58 | top: 200px; |
59 | z-index: @page-loading-z-index; | 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 ©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 ©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 './UserDropdown'; | @@ -19,7 +19,7 @@ import UserDropdown from './UserDropdown'; | ||
19 | import LayoutMenu from '../menu'; | 19 | import LayoutMenu from '../menu'; |
20 | import LayoutBreadcrumb from './LayoutBreadcrumb.vue'; | 20 | import LayoutBreadcrumb from './LayoutBreadcrumb.vue'; |
21 | import LockAction from './actions/LockAction'; | 21 | import LockAction from './actions/LockAction'; |
22 | -import LayoutTrigger from '../LayoutTrigger'; | 22 | +import LayoutTrigger from '../trigger/index.vue'; |
23 | import NoticeAction from './notice/NoticeActionItem.vue'; | 23 | import NoticeAction from './notice/NoticeActionItem.vue'; |
24 | import { | 24 | import { |
25 | RedoOutlined, | 25 | RedoOutlined, |
src/layouts/default/header/LayoutMultipleHeader.tsx
@@ -3,7 +3,7 @@ import './LayoutMultipleHeader.less'; | @@ -3,7 +3,7 @@ import './LayoutMultipleHeader.less'; | ||
3 | import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue'; | 3 | import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue'; |
4 | 4 | ||
5 | import LayoutHeader from './LayoutHeader'; | 5 | import LayoutHeader from './LayoutHeader'; |
6 | -import MultipleTabs from '../multitabs/index'; | 6 | +import MultipleTabs from '../tabs/index.vue'; |
7 | 7 | ||
8 | import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; | 8 | import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; |
9 | import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; | 9 | import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
src/layouts/default/header/index.less
1 | @import (reference) '../../../design/index.less'; | 1 | @import (reference) '../../../design/index.less'; |
2 | +@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger'; | ||
2 | 3 | ||
3 | .layout-header { | 4 | .layout-header { |
4 | display: flex; | 5 | display: flex; |
@@ -24,7 +25,7 @@ | @@ -24,7 +25,7 @@ | ||
24 | height: 100%; | 25 | height: 100%; |
25 | align-items: center; | 26 | align-items: center; |
26 | 27 | ||
27 | - .layout-trigger { | 28 | + .@{header-trigger-prefix-cls} { |
28 | display: flex; | 29 | display: flex; |
29 | height: 100%; | 30 | height: 100%; |
30 | padding: 1px 10px 0 16px; | 31 | padding: 1px 10px 0 16px; |
src/layouts/default/index.tsx
1 | import './index.less'; | 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 | import LayoutContent from './content/index.vue'; | 8 | import LayoutContent from './content/index.vue'; |
8 | -import LayoutFooter from './footer'; | ||
9 | -import LayoutLockPage from '/@/views/sys/lock/index.vue'; | ||
10 | import LayoutSideBar from './sider'; | 9 | import LayoutSideBar from './sider'; |
11 | -import SettingBtn from './setting/index.vue'; | ||
12 | import LayoutMultipleHeader from './header/LayoutMultipleHeader'; | 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 | import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; | 12 | import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; |
19 | import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; | 13 | import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
20 | -import { useRootSetting } from '/@/hooks/setting/useRootSetting'; | ||
21 | import { createLayoutContext } from './useLayoutContext'; | 14 | import { createLayoutContext } from './useLayoutContext'; |
22 | 15 | ||
23 | import { registerGlobComp } from '/@/components/registerGlobComp'; | 16 | import { registerGlobComp } from '/@/components/registerGlobComp'; |
24 | import { createBreakpointListen } from '/@/hooks/event/useBreakpoint'; | 17 | import { createBreakpointListen } from '/@/hooks/event/useBreakpoint'; |
25 | import { isMobile } from '/@/utils/is'; | 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 | export default defineComponent({ | 23 | export default defineComponent({ |
27 | name: 'DefaultLayout', | 24 | name: 'DefaultLayout', |
28 | setup() { | 25 | setup() { |
29 | - const { currentRoute } = useRouter(); | ||
30 | const headerRef = ref<ComponentRef>(null); | 26 | const headerRef = ref<ComponentRef>(null); |
31 | const isMobileRef = ref(false); | 27 | const isMobileRef = ref(false); |
32 | 28 | ||
@@ -43,56 +39,27 @@ export default defineComponent({ | @@ -43,56 +39,27 @@ export default defineComponent({ | ||
43 | 39 | ||
44 | const { getShowFullHeaderRef } = useHeaderSetting(); | 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 | return () => { | 44 | return () => { |
78 | return ( | 45 | return ( |
79 | <Layout class="default-layout"> | 46 | <Layout class="default-layout"> |
80 | {() => ( | 47 | {() => ( |
81 | <> | 48 | <> |
82 | - {renderFeatures()} | 49 | + <LayoutFeatures /> |
83 | 50 | ||
84 | {unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />} | 51 | {unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />} |
85 | 52 | ||
86 | <Layout> | 53 | <Layout> |
87 | {() => ( | 54 | {() => ( |
88 | <> | 55 | <> |
89 | - {unref(showSideBarRef) && <LayoutSideBar />} | 56 | + {unref(getShowSidebar) && <LayoutSideBar />} |
90 | <Layout class="default-layout__main"> | 57 | <Layout class="default-layout__main"> |
91 | {() => ( | 58 | {() => ( |
92 | <> | 59 | <> |
93 | <LayoutMultipleHeader /> | 60 | <LayoutMultipleHeader /> |
94 | <LayoutContent /> | 61 | <LayoutContent /> |
95 | - {unref(getShowLayoutFooter) && <LayoutFooter />} | 62 | + <LayoutFooter /> |
96 | </> | 63 | </> |
97 | )} | 64 | )} |
98 | </Layout> | 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,7 +13,7 @@ | ||
13 | import { useDesign } from '/@/hooks/web/useDesign'; | 13 | import { useDesign } from '/@/hooks/web/useDesign'; |
14 | 14 | ||
15 | export default defineComponent({ | 15 | export default defineComponent({ |
16 | - name: 'SettingBtn', | 16 | + name: 'SettingButton', |
17 | components: { SettingOutlined, SettingDrawer }, | 17 | components: { SettingOutlined, SettingDrawer }, |
18 | setup() { | 18 | setup() { |
19 | const [register, { openDrawer }] = useDrawer(); | 19 | const [register, { openDrawer }] = useDrawer(); |
src/layouts/default/sider/useLayoutSider.tsx
1 | import type { Ref } from 'vue'; | 1 | import type { Ref } from 'vue'; |
2 | 2 | ||
3 | import { computed, unref, onMounted, nextTick, ref } from 'vue'; | 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 | import { TriggerEnum } from '/@/enums/menuEnum'; | 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 | @import (reference) '../../../design/index.less'; | 1 | @import (reference) '../../../design/index.less'; |
2 | +@prefix-cls: ~'@{namespace}-multiple-tabs'; | ||
2 | 3 | ||
3 | -.multiple-tabs { | 4 | +.@{prefix-cls} { |
4 | z-index: 10; | 5 | z-index: 10; |
5 | height: @multiple-height + 2; | 6 | height: @multiple-height + 2; |
6 | - padding: 0 0 2px 0; | ||
7 | - margin-left: -1px; | ||
8 | line-height: @multiple-height + 2; | 7 | line-height: @multiple-height + 2; |
9 | background: @white; | 8 | background: @white; |
10 | box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05); | 9 | box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05); |
@@ -32,13 +31,33 @@ | @@ -32,13 +31,33 @@ | ||
32 | line-height: calc(@multiple-height - 2px); | 31 | line-height: calc(@multiple-height - 2px); |
33 | color: @text-color-call-out; | 32 | color: @text-color-call-out; |
34 | background: @white; | 33 | background: @white; |
35 | - border: 1px solid darken(@border-color-light, 8%); | 34 | + border: 1px solid darken(@border-color-light, 6%); |
36 | transition: none; | 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 | &:hover { | 51 | &:hover { |
39 | .ant-tabs-close-x { | 52 | .ant-tabs-close-x { |
40 | opacity: 1; | 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 | .ant-tabs-close-x { | 63 | .ant-tabs-close-x { |
@@ -51,7 +70,7 @@ | @@ -51,7 +70,7 @@ | ||
51 | 70 | ||
52 | &:hover { | 71 | &:hover { |
53 | svg { | 72 | svg { |
54 | - width: 0.75em; | 73 | + width: 0.8em; |
55 | } | 74 | } |
56 | } | 75 | } |
57 | } | 76 | } |
@@ -73,6 +92,7 @@ | @@ -73,6 +92,7 @@ | ||
73 | color: @white; | 92 | color: @white; |
74 | background: fade(@primary-color, 100%); | 93 | background: fade(@primary-color, 100%); |
75 | border: 0; | 94 | border: 0; |
95 | + transition: none; | ||
76 | 96 | ||
77 | &::before { | 97 | &::before { |
78 | position: absolute; | 98 | position: absolute; |
@@ -98,7 +118,7 @@ | @@ -98,7 +118,7 @@ | ||
98 | } | 118 | } |
99 | 119 | ||
100 | .ant-tabs-nav > div:nth-child(1) { | 120 | .ant-tabs-nav > div:nth-child(1) { |
101 | - padding: 0 10px; | 121 | + padding: 0 6px; |
102 | 122 | ||
103 | .ant-tabs-tab { | 123 | .ant-tabs-tab { |
104 | margin-right: 3px !important; | 124 | margin-right: 3px !important; |
@@ -124,36 +144,42 @@ | @@ -124,36 +144,42 @@ | ||
124 | .ant-dropdown-trigger { | 144 | .ant-dropdown-trigger { |
125 | display: inline-flex; | 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 'sortablejs'; | @@ -2,6 +2,7 @@ import Sortable from 'sortablejs'; | ||
2 | import { toRaw, ref, nextTick, onMounted } from 'vue'; | 2 | import { toRaw, ref, nextTick, onMounted } from 'vue'; |
3 | import { RouteLocationNormalized } from 'vue-router'; | 3 | import { RouteLocationNormalized } from 'vue-router'; |
4 | import { useProjectSetting } from '/@/hooks/setting'; | 4 | import { useProjectSetting } from '/@/hooks/setting'; |
5 | +import { useDesign } from '/@/hooks/web/useDesign'; | ||
5 | import router from '/@/router'; | 6 | import router from '/@/router'; |
6 | import { tabStore } from '/@/store/modules/tab'; | 7 | import { tabStore } from '/@/store/modules/tab'; |
7 | import { isNullAndUnDef } from '/@/utils/is'; | 8 | import { isNullAndUnDef } from '/@/utils/is'; |
@@ -48,12 +49,12 @@ export function initAffixTabs(): string[] { | @@ -48,12 +49,12 @@ export function initAffixTabs(): string[] { | ||
48 | export function useTabsDrag(affixTextList: string[]) { | 49 | export function useTabsDrag(affixTextList: string[]) { |
49 | const { multiTabsSetting } = useProjectSetting(); | 50 | const { multiTabsSetting } = useProjectSetting(); |
50 | 51 | ||
52 | + const { prefixCls } = useDesign('multiple-tabs'); | ||
53 | + | ||
51 | function initSortableTabs() { | 54 | function initSortableTabs() { |
52 | if (!multiTabsSetting.canDrag) return; | 55 | if (!multiTabsSetting.canDrag) return; |
53 | nextTick(() => { | 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 | if (!el) return; | 59 | if (!el) return; |
59 | Sortable.create(el, { | 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 | <template> | 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 | </template> | 11 | </template> |
10 | <script lang="ts"> | 12 | <script lang="ts"> |
11 | import { defineComponent } from 'vue'; | 13 | import { defineComponent } from 'vue'; |
src/layouts/iframe/useFrameKeepAlive.ts
1 | import type { AppRouteRecordRaw } from '/@/router/types'; | 1 | import type { AppRouteRecordRaw } from '/@/router/types'; |
2 | 2 | ||
3 | import { computed, toRaw, unref } from 'vue'; | 3 | import { computed, toRaw, unref } from 'vue'; |
4 | -import { useRouter } from 'vue-router'; | ||
5 | -import router from '/@/router'; | ||
6 | 4 | ||
7 | import { tabStore } from '/@/store/modules/tab'; | 5 | import { tabStore } from '/@/store/modules/tab'; |
8 | 6 | ||
@@ -10,8 +8,10 @@ import { unique } from '/@/utils'; | @@ -10,8 +8,10 @@ import { unique } from '/@/utils'; | ||
10 | 8 | ||
11 | import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; | 9 | import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; |
12 | 10 | ||
11 | +import router from '/@/router'; | ||
12 | + | ||
13 | export function useFrameKeepAlive() { | 13 | export function useFrameKeepAlive() { |
14 | - const { currentRoute } = useRouter(); | 14 | + const { currentRoute } = router; |
15 | const { getShowMultipleTab } = useMultipleTabSetting(); | 15 | const { getShowMultipleTab } = useMultipleTabSetting(); |
16 | 16 | ||
17 | const getFramePages = computed(() => { | 17 | const getFramePages = computed(() => { |
src/layouts/page/index.tsx
@@ -10,12 +10,14 @@ import { useRootSetting } from '/@/hooks/setting/useRootSetting'; | @@ -10,12 +10,14 @@ import { useRootSetting } from '/@/hooks/setting/useRootSetting'; | ||
10 | import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; | 10 | import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; |
11 | import { useCache } from './useCache'; | 11 | import { useCache } from './useCache'; |
12 | import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; | 12 | import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; |
13 | +// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; | ||
13 | 14 | ||
14 | interface DefaultContext { | 15 | interface DefaultContext { |
15 | Component: FunctionalComponent & { type: { [key: string]: any } }; | 16 | Component: FunctionalComponent & { type: { [key: string]: any } }; |
16 | route: RouteLocation; | 17 | route: RouteLocation; |
17 | } | 18 | } |
18 | 19 | ||
20 | +// const FrameLayout=createAsyncComponent(()=>'/@/layouts/iframe/index.vue') | ||
19 | export default defineComponent({ | 21 | export default defineComponent({ |
20 | name: 'PageLayout', | 22 | name: 'PageLayout', |
21 | setup() { | 23 | setup() { |
src/layouts/page/useCache.ts
@@ -32,7 +32,6 @@ export function useCache(isPage: boolean) { | @@ -32,7 +32,6 @@ export function useCache(isPage: boolean) { | ||
32 | 32 | ||
33 | if (isPage) { | 33 | if (isPage) { |
34 | // page Layout | 34 | // page Layout |
35 | - // not parent layout | ||
36 | return cached.get(PAGE_LAYOUT_KEY) || []; | 35 | return cached.get(PAGE_LAYOUT_KEY) || []; |
37 | } | 36 | } |
38 | const cacheSet = new Set<string>(); | 37 | const cacheSet = new Set<string>(); |
src/locales/lang/en/layout/multipleTab.ts
1 | export default { | 1 | export default { |
2 | - redo: 'Refresh', | ||
3 | - close: 'Close', | 2 | + redo: 'Refresh current', |
3 | + close: 'Close current', | ||
4 | closeLeft: 'Close Left', | 4 | closeLeft: 'Close Left', |
5 | closeRight: 'Close Right', | 5 | closeRight: 'Close Right', |
6 | closeOther: 'Close Other', | 6 | closeOther: 'Close Other', |
src/locales/lang/zh_CN/layout/multipleTab.ts
src/router/constant.ts
@@ -6,7 +6,7 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception'); | @@ -6,7 +6,7 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception'); | ||
6 | /** | 6 | /** |
7 | * @description: default layout | 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 | * @description: page-layout | 12 | * @description: page-layout |