mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 14:13:40 +08:00
perf(tabs): perf multiple-tabs
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { withInstall } from '../util';
|
import { withInstall } from '../util';
|
||||||
|
|
||||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
import Dropdown from './src/Dropdown';
|
||||||
export const Dropdown = createAsyncComponent(() => import('./src/Dropdown'));
|
|
||||||
|
|
||||||
withInstall(Dropdown);
|
withInstall(Dropdown);
|
||||||
export * from './src/types';
|
export * from './src/types';
|
||||||
|
export { Dropdown };
|
||||||
|
@@ -243,6 +243,7 @@ export default defineComponent({
|
|||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
class={unref(getMenuClass)}
|
class={unref(getMenuClass)}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
|
subMenuOpenDelay={0.2}
|
||||||
{...unref(getInlineCollapseOptions)}
|
{...unref(getInlineCollapseOptions)}
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
|
@@ -6,6 +6,7 @@ import { appStore } from '/@/store/modules/app';
|
|||||||
|
|
||||||
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
|
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
|
||||||
import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
|
import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
|
||||||
|
import { useFullContent } from '/@/hooks/web/useFullContent';
|
||||||
|
|
||||||
// Get menu configuration
|
// Get menu configuration
|
||||||
const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
|
const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
|
||||||
@@ -78,6 +79,15 @@ const getCalcContentWidth = computed(() => {
|
|||||||
return `calc(100% - ${unref(width)}px)`;
|
return `calc(100% - ${unref(width)}px)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { getFullContent: fullContent } = useFullContent();
|
||||||
|
|
||||||
|
const getShowSidebar = computed(() => {
|
||||||
|
return (
|
||||||
|
unref(getSplit) ||
|
||||||
|
(unref(getShowMenu) && unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && !unref(fullContent))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Set menu configuration
|
// Set menu configuration
|
||||||
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
|
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
|
||||||
appStore.commitProjectConfigState({ menuSetting });
|
appStore.commitProjectConfigState({ menuSetting });
|
||||||
@@ -119,5 +129,6 @@ export function useMenuSetting() {
|
|||||||
getMenuHidden,
|
getMenuHidden,
|
||||||
getIsTopMenu,
|
getIsTopMenu,
|
||||||
getMenuBgColor,
|
getMenuBgColor,
|
||||||
|
getShowSidebar,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,42 +0,0 @@
|
|||||||
import type { FunctionalComponent } from 'vue';
|
|
||||||
|
|
||||||
import { defineComponent, unref } from 'vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DoubleRightOutlined,
|
|
||||||
DoubleLeftOutlined,
|
|
||||||
MenuUnfoldOutlined,
|
|
||||||
MenuFoldOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
|
||||||
import { propTypes } from '/@/utils/propTypes';
|
|
||||||
|
|
||||||
const SiderTrigger: FunctionalComponent = () => {
|
|
||||||
const { getCollapsed } = useMenuSetting();
|
|
||||||
return unref(getCollapsed) ? <DoubleRightOutlined /> : <DoubleLeftOutlined />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeaderTrigger: FunctionalComponent<{
|
|
||||||
theme?: string;
|
|
||||||
}> = (props) => {
|
|
||||||
const { toggleCollapsed, getCollapsed } = useMenuSetting();
|
|
||||||
return (
|
|
||||||
<span class={['layout-trigger', props.theme]} onClick={toggleCollapsed}>
|
|
||||||
{unref(getCollapsed) ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'LayoutTrigger',
|
|
||||||
props: {
|
|
||||||
sider: propTypes.bool.def(true),
|
|
||||||
theme: propTypes.oneOf(['light', 'dark']),
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return () => {
|
|
||||||
return props.sider ? <SiderTrigger /> : <HeaderTrigger theme={props.theme} />;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
@@ -6,7 +6,7 @@
|
|||||||
:loading="getPageLoading"
|
:loading="getPageLoading"
|
||||||
background="rgba(240, 242, 245, 0.6)"
|
background="rgba(240, 242, 245, 0.6)"
|
||||||
absolute
|
absolute
|
||||||
:class="`${prefixCls}__loading`"
|
:class="`${prefixCls}-loading`"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<PageLayout />
|
<PageLayout />
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__loading {
|
&-loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 200px;
|
top: 200px;
|
||||||
z-index: @page-loading-z-index;
|
z-index: @page-loading-z-index;
|
||||||
|
29
src/layouts/default/feature/index.vue
Normal file
29
src/layouts/default/feature/index.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<LayoutLockPage />
|
||||||
|
<BackTop v-if="getUseOpenBackTop" :target="getTarget" />
|
||||||
|
<SettingDrawer v-if="getShowSettingButton" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
import { BackTop } from 'ant-design-vue';
|
||||||
|
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LayoutFeatures',
|
||||||
|
components: {
|
||||||
|
BackTop,
|
||||||
|
LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')),
|
||||||
|
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')),
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { getUseOpenBackTop, getShowSettingButton } = useRootSetting();
|
||||||
|
|
||||||
|
return {
|
||||||
|
getTarget: () => document.body,
|
||||||
|
getUseOpenBackTop,
|
||||||
|
getShowSettingButton,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@@ -1,28 +0,0 @@
|
|||||||
@normal-color: rgba(0, 0, 0, 0.45);
|
|
||||||
|
|
||||||
@hover-color: rgba(0, 0, 0, 0.85);
|
|
||||||
|
|
||||||
.layout-footer {
|
|
||||||
color: @normal-color;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&__links {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: @normal-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: @hover-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.github {
|
|
||||||
margin: 0 30px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: @hover-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
import './index.less';
|
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { Layout } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { GithubFilled } from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
|
|
||||||
import { openWindow } from '/@/utils';
|
|
||||||
|
|
||||||
import { useI18n } from '/@/hooks/web/useI18n';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'LayoutContent',
|
|
||||||
setup() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<Layout.Footer class="layout-footer">
|
|
||||||
{() => (
|
|
||||||
<>
|
|
||||||
<div class="layout-footer__links">
|
|
||||||
<a onClick={() => openWindow(SITE_URL)}>{t('layout.footer.onlinePreview')}</a>
|
|
||||||
<GithubFilled onClick={() => openWindow(GITHUB_URL)} class="github" />
|
|
||||||
<a onClick={() => openWindow(DOC_URL)}>{t('layout.footer.onlineDocument')}</a>
|
|
||||||
</div>
|
|
||||||
<div>Copyright ©2020 Vben Admin</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Layout.Footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
74
src/layouts/default/footer/index.vue
Normal file
74
src/layouts/default/footer/index.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<Footer :class="prefixCls" v-if="getShowLayoutFooter">
|
||||||
|
<div :class="`${prefixCls}__links`">
|
||||||
|
<a @click="openWindow(SITE_URL)">{{ t('layout.footer.onlinePreview') }}</a>
|
||||||
|
<GithubFilled @click="openWindow(GITHUB_URL)" :class="`${prefixCls}__github`" />
|
||||||
|
<a @click="openWindow(DOC_URL)">{{ t('layout.footer.onlineDocument') }}</a>
|
||||||
|
</div>
|
||||||
|
<div>Copyright ©2020 Vben Admin</div>
|
||||||
|
</Footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, unref } from 'vue';
|
||||||
|
import { Layout } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { GithubFilled } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
|
||||||
|
import { openWindow } from '/@/utils';
|
||||||
|
|
||||||
|
import { useI18n } from '/@/hooks/web/useI18n';
|
||||||
|
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LayoutFooter',
|
||||||
|
components: { Footer: Layout.Footer, GithubFilled },
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { getShowFooter } = useRootSetting();
|
||||||
|
const { currentRoute } = useRouter();
|
||||||
|
const { prefixCls } = useDesign('layout-footer');
|
||||||
|
|
||||||
|
const getShowLayoutFooter = computed(() => {
|
||||||
|
return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
|
||||||
|
});
|
||||||
|
return { getShowLayoutFooter, prefixCls, t, DOC_URL, GITHUB_URL, SITE_URL, openWindow };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import (reference) '../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-layout-footer';
|
||||||
|
|
||||||
|
@normal-color: rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
|
@hover-color: rgba(0, 0, 0, 0.85);
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
color: @normal-color;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__links {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: @normal-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__github {
|
||||||
|
margin: 0 30px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -19,7 +19,7 @@ import UserDropdown from './UserDropdown';
|
|||||||
import LayoutMenu from '../menu';
|
import LayoutMenu from '../menu';
|
||||||
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
|
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
|
||||||
import LockAction from './actions/LockAction';
|
import LockAction from './actions/LockAction';
|
||||||
import LayoutTrigger from '../LayoutTrigger';
|
import LayoutTrigger from '../trigger/index.vue';
|
||||||
import NoticeAction from './notice/NoticeActionItem.vue';
|
import NoticeAction from './notice/NoticeActionItem.vue';
|
||||||
import {
|
import {
|
||||||
RedoOutlined,
|
RedoOutlined,
|
||||||
|
@@ -3,7 +3,7 @@ import './LayoutMultipleHeader.less';
|
|||||||
import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue';
|
import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue';
|
||||||
|
|
||||||
import LayoutHeader from './LayoutHeader';
|
import LayoutHeader from './LayoutHeader';
|
||||||
import MultipleTabs from '../multitabs/index';
|
import MultipleTabs from '../tabs/index.vue';
|
||||||
|
|
||||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
@import (reference) '../../../design/index.less';
|
@import (reference) '../../../design/index.less';
|
||||||
|
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
|
||||||
|
|
||||||
.layout-header {
|
.layout-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.layout-trigger {
|
.@{header-trigger-prefix-cls} {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1px 10px 0 16px;
|
padding: 1px 10px 0 16px;
|
||||||
|
@@ -1,32 +1,28 @@
|
|||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
import { defineComponent, unref, computed, ref } from 'vue';
|
import { defineComponent, unref, ref } from 'vue';
|
||||||
import { Layout, BackTop } from 'ant-design-vue';
|
import { Layout } from 'ant-design-vue';
|
||||||
import LayoutHeader from './header/LayoutHeader';
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
|
||||||
|
import LayoutHeader from './header/LayoutHeader';
|
||||||
import LayoutContent from './content/index.vue';
|
import LayoutContent from './content/index.vue';
|
||||||
import LayoutFooter from './footer';
|
|
||||||
import LayoutLockPage from '/@/views/sys/lock/index.vue';
|
|
||||||
import LayoutSideBar from './sider';
|
import LayoutSideBar from './sider';
|
||||||
import SettingBtn from './setting/index.vue';
|
|
||||||
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
|
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
|
||||||
|
|
||||||
import { MenuModeEnum } from '/@/enums/menuEnum';
|
|
||||||
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useFullContent } from '/@/hooks/web/useFullContent';
|
|
||||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
|
||||||
import { createLayoutContext } from './useLayoutContext';
|
import { createLayoutContext } from './useLayoutContext';
|
||||||
|
|
||||||
import { registerGlobComp } from '/@/components/registerGlobComp';
|
import { registerGlobComp } from '/@/components/registerGlobComp';
|
||||||
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
|
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
|
||||||
import { isMobile } from '/@/utils/is';
|
import { isMobile } from '/@/utils/is';
|
||||||
|
|
||||||
|
const LayoutFeatures = createAsyncComponent(() => import('/@/layouts/default/feature/index.vue'));
|
||||||
|
const LayoutFooter = createAsyncComponent(() => import('/@/layouts/default/footer/index.vue'));
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'DefaultLayout',
|
name: 'DefaultLayout',
|
||||||
setup() {
|
setup() {
|
||||||
const { currentRoute } = useRouter();
|
|
||||||
const headerRef = ref<ComponentRef>(null);
|
const headerRef = ref<ComponentRef>(null);
|
||||||
const isMobileRef = ref(false);
|
const isMobileRef = ref(false);
|
||||||
|
|
||||||
@@ -43,56 +39,27 @@ export default defineComponent({
|
|||||||
|
|
||||||
const { getShowFullHeaderRef } = useHeaderSetting();
|
const { getShowFullHeaderRef } = useHeaderSetting();
|
||||||
|
|
||||||
const { getUseOpenBackTop, getShowSettingButton, getShowFooter } = useRootSetting();
|
const { getShowSidebar } = useMenuSetting();
|
||||||
|
|
||||||
const { getShowMenu, getMenuMode, getSplit } = useMenuSetting();
|
|
||||||
|
|
||||||
const { getFullContent } = useFullContent();
|
|
||||||
|
|
||||||
const getShowLayoutFooter = computed(() => {
|
|
||||||
return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
|
|
||||||
});
|
|
||||||
|
|
||||||
const showSideBarRef = computed(() => {
|
|
||||||
return (
|
|
||||||
unref(getSplit) ||
|
|
||||||
(unref(getShowMenu) &&
|
|
||||||
unref(getMenuMode) !== MenuModeEnum.HORIZONTAL &&
|
|
||||||
!unref(getFullContent))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderFeatures() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LayoutLockPage />
|
|
||||||
{/* back top */}
|
|
||||||
{unref(getUseOpenBackTop) && <BackTop target={() => document.body} />}
|
|
||||||
{/* open setting drawer */}
|
|
||||||
{unref(getShowSettingButton) && <SettingBtn />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return (
|
return (
|
||||||
<Layout class="default-layout">
|
<Layout class="default-layout">
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
{renderFeatures()}
|
<LayoutFeatures />
|
||||||
|
|
||||||
{unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />}
|
{unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />}
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
{unref(showSideBarRef) && <LayoutSideBar />}
|
{unref(getShowSidebar) && <LayoutSideBar />}
|
||||||
<Layout class="default-layout__main">
|
<Layout class="default-layout__main">
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
<LayoutMultipleHeader />
|
<LayoutMultipleHeader />
|
||||||
<LayoutContent />
|
<LayoutContent />
|
||||||
{unref(getShowLayoutFooter) && <LayoutFooter />}
|
<LayoutFooter />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
95
src/layouts/default/index.vue
Normal file
95
src/layouts/default/index.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<Layout :class="prefixCls">
|
||||||
|
<LayoutFeatures />
|
||||||
|
<LayoutHeader fixed ref="headerRef" v-if="getShowFullHeaderRef" />
|
||||||
|
<Layout>
|
||||||
|
<LayoutSideBar v-if="getShowSidebar" />
|
||||||
|
<Layout :class="`${prefixCls}__main`">
|
||||||
|
<LayoutMultipleHeader />
|
||||||
|
<LayoutContent />
|
||||||
|
<LayoutFooter />
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue';
|
||||||
|
import { Layout } from 'ant-design-vue';
|
||||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
|
||||||
|
import LayoutHeader from './header/LayoutHeader';
|
||||||
|
import LayoutContent from './content/index.vue';
|
||||||
|
import LayoutSideBar from './sider';
|
||||||
|
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
|
||||||
|
|
||||||
|
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||||
|
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
import { createLayoutContext } from './useLayoutContext';
|
||||||
|
|
||||||
|
import { registerGlobComp } from '/@/components/registerGlobComp';
|
||||||
|
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
|
||||||
|
import { isMobile } from '/@/utils/is';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'DefaultLayout',
|
||||||
|
components: {
|
||||||
|
LayoutFeatures: createAsyncComponent(() => import('/@/layouts/default/feature/index.vue')),
|
||||||
|
LayoutFooter: createAsyncComponent(() => import('/@/layouts/default/footer/index.vue')),
|
||||||
|
LayoutHeader,
|
||||||
|
LayoutContent,
|
||||||
|
LayoutSideBar,
|
||||||
|
LayoutMultipleHeader,
|
||||||
|
Layout,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const headerRef = ref<ComponentRef>(null);
|
||||||
|
const isMobileRef = ref(false);
|
||||||
|
|
||||||
|
const { prefixCls } = useDesign('default-layout');
|
||||||
|
|
||||||
|
createLayoutContext({ fullHeader: headerRef, isMobile: isMobileRef });
|
||||||
|
|
||||||
|
createBreakpointListen(() => {
|
||||||
|
isMobileRef.value = isMobile();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ! Only register global components here
|
||||||
|
// ! Can reduce the size of the first screen code
|
||||||
|
// default layout It is loaded after login. So it won’t be packaged to the first screen
|
||||||
|
registerGlobComp();
|
||||||
|
|
||||||
|
const { getShowFullHeaderRef } = useHeaderSetting();
|
||||||
|
|
||||||
|
const { getShowSidebar } = useMenuSetting();
|
||||||
|
|
||||||
|
return {
|
||||||
|
getShowFullHeaderRef,
|
||||||
|
getShowSidebar,
|
||||||
|
headerRef,
|
||||||
|
prefixCls,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
@import (reference) '../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-default-layout';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
background: @content-bg;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .ant-layout {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,78 +0,0 @@
|
|||||||
import type { PropType } from 'vue';
|
|
||||||
import { Dropdown } from '/@/components/Dropdown/index';
|
|
||||||
|
|
||||||
import { defineComponent, unref, FunctionalComponent } from 'vue';
|
|
||||||
|
|
||||||
import { TabContentProps } from './types';
|
|
||||||
|
|
||||||
import { RightOutlined } from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
import { TabContentEnum } from './types';
|
|
||||||
|
|
||||||
import { useTabDropdown } from './useTabDropdown';
|
|
||||||
import { useI18n } from '/@/hooks/web/useI18n';
|
|
||||||
|
|
||||||
import { RouteLocationNormalized } from 'vue-router';
|
|
||||||
|
|
||||||
const { t: titleT } = useI18n();
|
|
||||||
|
|
||||||
const ExtraContent: FunctionalComponent = () => {
|
|
||||||
return (
|
|
||||||
<span class={`multiple-tabs-content__extra `}>
|
|
||||||
<RightOutlined />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TabContent: FunctionalComponent<{ tabItem: RouteLocationNormalized; handler: Fn }> = (
|
|
||||||
props
|
|
||||||
) => {
|
|
||||||
const { tabItem: { meta } = {} } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={`multiple-tabs-content__content `} onContextmenu={props.handler(props.tabItem)}>
|
|
||||||
<span class="ml-1">{meta && titleT(meta.title)}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'TabContent',
|
|
||||||
props: {
|
|
||||||
tabItem: {
|
|
||||||
type: Object as PropType<RouteLocationNormalized>,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
type: {
|
|
||||||
type: Number as PropType<TabContentEnum>,
|
|
||||||
default: TabContentEnum.TAB_TYPE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const {
|
|
||||||
getDropMenuList,
|
|
||||||
handleMenuEvent,
|
|
||||||
handleContextMenu,
|
|
||||||
getTrigger,
|
|
||||||
isTabs,
|
|
||||||
} = useTabDropdown(props as TabContentProps);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
dropMenuList={unref(getDropMenuList)}
|
|
||||||
trigger={unref(getTrigger)}
|
|
||||||
onMenuEvent={handleMenuEvent}
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
if (!unref(isTabs)) {
|
|
||||||
return <ExtraContent />;
|
|
||||||
}
|
|
||||||
return <TabContent handler={handleContextMenu} tabItem={props.tabItem} />;
|
|
||||||
}}
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,114 +0,0 @@
|
|||||||
import './index.less';
|
|
||||||
|
|
||||||
import type { TabContentProps } from './types';
|
|
||||||
|
|
||||||
import { defineComponent, watch, computed, unref, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { Tabs } from 'ant-design-vue';
|
|
||||||
import TabContent from './TabContent';
|
|
||||||
|
|
||||||
import { useGo } from '/@/hooks/web/usePage';
|
|
||||||
|
|
||||||
import { TabContentEnum } from './types';
|
|
||||||
|
|
||||||
import { tabStore } from '/@/store/modules/tab';
|
|
||||||
import { userStore } from '/@/store/modules/user';
|
|
||||||
|
|
||||||
import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
|
|
||||||
import { REDIRECT_NAME } from '/@/router/constant';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'MultipleTabs',
|
|
||||||
setup() {
|
|
||||||
const activeKeyRef = ref('');
|
|
||||||
|
|
||||||
const affixTextList = initAffixTabs();
|
|
||||||
|
|
||||||
useTabsDrag(affixTextList);
|
|
||||||
|
|
||||||
const go = useGo();
|
|
||||||
|
|
||||||
const { currentRoute } = useRouter();
|
|
||||||
|
|
||||||
const getTabsState = computed(() => tabStore.getTabsState);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => tabStore.getLastChangeRouteState?.path,
|
|
||||||
() => {
|
|
||||||
if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
|
|
||||||
if (!lastChangeRoute || !userStore.getTokenState) return;
|
|
||||||
const { path, fullPath } = lastChangeRoute;
|
|
||||||
const p = fullPath || path;
|
|
||||||
if (activeKeyRef.value !== p) {
|
|
||||||
activeKeyRef.value = p;
|
|
||||||
}
|
|
||||||
tabStore.addTabAction(lastChangeRoute);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleChange(activeKey: any) {
|
|
||||||
activeKeyRef.value = activeKey;
|
|
||||||
go(activeKey, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the current tab
|
|
||||||
function handleEdit(targetKey: string) {
|
|
||||||
// Added operation to hide, currently only use delete operation
|
|
||||||
tabStore.closeTabByKeyAction(targetKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderQuick() {
|
|
||||||
const tabContentProps: TabContentProps = {
|
|
||||||
tabItem: currentRoute.value,
|
|
||||||
type: TabContentEnum.EXTRA_TYPE,
|
|
||||||
};
|
|
||||||
return <TabContent {...tabContentProps} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTabs() {
|
|
||||||
return unref(getTabsState).map((item) => {
|
|
||||||
const key = item.query ? item.fullPath : item.path;
|
|
||||||
const closable = !(item && item.meta && item.meta.affix);
|
|
||||||
|
|
||||||
const slots = {
|
|
||||||
tab: () => <TabContent tabItem={item} />,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Tabs.TabPane key={key} closable={closable}>
|
|
||||||
{slots}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const slots = {
|
|
||||||
default: () => renderTabs(),
|
|
||||||
tabBarExtraContent: () => renderQuick(),
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div class="multiple-tabs">
|
|
||||||
<Tabs
|
|
||||||
type="editable-card"
|
|
||||||
size="small"
|
|
||||||
animated={false}
|
|
||||||
hideAdd={true}
|
|
||||||
tabBarGutter={3}
|
|
||||||
activeKey={unref(activeKeyRef)}
|
|
||||||
onChange={handleChange}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
>
|
|
||||||
{slots}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
@@ -13,7 +13,7 @@
|
|||||||
import { useDesign } from '/@/hooks/web/useDesign';
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SettingBtn',
|
name: 'SettingButton',
|
||||||
components: { SettingOutlined, SettingDrawer },
|
components: { SettingOutlined, SettingDrawer },
|
||||||
setup() {
|
setup() {
|
||||||
const [register, { openDrawer }] = useDrawer();
|
const [register, { openDrawer }] = useDrawer();
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
import { computed, unref, onMounted, nextTick, ref } from 'vue';
|
import { computed, unref, onMounted, nextTick, ref } from 'vue';
|
||||||
import LayoutTrigger from '/@/layouts/default/LayoutTrigger';
|
import LayoutTrigger from '/@/layouts/default/trigger/index.vue';
|
||||||
|
|
||||||
import { TriggerEnum } from '/@/enums/menuEnum';
|
import { TriggerEnum } from '/@/enums/menuEnum';
|
||||||
|
|
||||||
|
21
src/layouts/default/tabs/components/QuickButton.vue
Normal file
21
src/layouts/default/tabs/components/QuickButton.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<TabContent :type="TabContentEnum.EXTRA_TYPE" :tabItem="$route" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import { TabContentEnum } from '../types';
|
||||||
|
|
||||||
|
import TabContent from './TabContent.vue';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'QuickButton',
|
||||||
|
components: {
|
||||||
|
TabContent,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
TabContentEnum,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
72
src/layouts/default/tabs/components/TabContent.vue
Normal file
72
src/layouts/default/tabs/components/TabContent.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent">
|
||||||
|
<div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="isTabs">
|
||||||
|
<span class="ml-1">{{ getTitle }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span :class="`${prefixCls}__extra`" v-else>
|
||||||
|
<RightOutlined />
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { defineComponent, computed } from 'vue';
|
||||||
|
import { Dropdown } from '/@/components/Dropdown/index';
|
||||||
|
|
||||||
|
import { TabContentProps, TabContentEnum } from '../types';
|
||||||
|
|
||||||
|
import { RightOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
import { useTabDropdown } from '../useTabDropdown';
|
||||||
|
import { useI18n } from '/@/hooks/web/useI18n';
|
||||||
|
|
||||||
|
import { RouteLocationNormalized } from 'vue-router';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'TabContent',
|
||||||
|
components: { Dropdown, RightOutlined },
|
||||||
|
props: {
|
||||||
|
tabItem: {
|
||||||
|
type: Object as PropType<RouteLocationNormalized>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
type: {
|
||||||
|
type: Number as PropType<TabContentEnum>,
|
||||||
|
default: TabContentEnum.TAB_TYPE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { prefixCls } = useDesign('multiple-tabs-content');
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
const { tabItem: { meta } = {} } = props;
|
||||||
|
return meta && t(meta.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
getDropMenuList,
|
||||||
|
handleMenuEvent,
|
||||||
|
handleContextMenu,
|
||||||
|
getTrigger,
|
||||||
|
isTabs,
|
||||||
|
} = useTabDropdown(props as TabContentProps);
|
||||||
|
|
||||||
|
function handleContext(e: ChangeEvent) {
|
||||||
|
props.tabItem && handleContextMenu(props.tabItem)(e);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
prefixCls,
|
||||||
|
getDropMenuList,
|
||||||
|
handleMenuEvent,
|
||||||
|
handleContext,
|
||||||
|
getTrigger,
|
||||||
|
isTabs,
|
||||||
|
getTitle,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@@ -1,10 +1,9 @@
|
|||||||
@import (reference) '../../../design/index.less';
|
@import (reference) '../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-multiple-tabs';
|
||||||
|
|
||||||
.multiple-tabs {
|
.@{prefix-cls} {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
height: @multiple-height + 2;
|
height: @multiple-height + 2;
|
||||||
padding: 0 0 2px 0;
|
|
||||||
margin-left: -1px;
|
|
||||||
line-height: @multiple-height + 2;
|
line-height: @multiple-height + 2;
|
||||||
background: @white;
|
background: @white;
|
||||||
box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
|
box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
|
||||||
@@ -32,13 +31,33 @@
|
|||||||
line-height: calc(@multiple-height - 2px);
|
line-height: calc(@multiple-height - 2px);
|
||||||
color: @text-color-call-out;
|
color: @text-color-call-out;
|
||||||
background: @white;
|
background: @white;
|
||||||
border: 1px solid darken(@border-color-light, 8%);
|
border: 1px solid darken(@border-color-light, 6%);
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|
||||||
|
&:not(.ant-tabs-tab-active)::before {
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: @primary-color;
|
||||||
|
content: '';
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, 0) scaleX(0);
|
||||||
|
transform-origin: center;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.ant-tabs-close-x {
|
.ant-tabs-close-x {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.ant-tabs-tab-active)::before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, 0) scaleX(1);
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs-close-x {
|
.ant-tabs-close-x {
|
||||||
@@ -51,7 +70,7 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
svg {
|
svg {
|
||||||
width: 0.75em;
|
width: 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +92,7 @@
|
|||||||
color: @white;
|
color: @white;
|
||||||
background: fade(@primary-color, 100%);
|
background: fade(@primary-color, 100%);
|
||||||
border: 0;
|
border: 0;
|
||||||
|
transition: none;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -98,7 +118,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs-nav > div:nth-child(1) {
|
.ant-tabs-nav > div:nth-child(1) {
|
||||||
padding: 0 10px;
|
padding: 0 6px;
|
||||||
|
|
||||||
.ant-tabs-tab {
|
.ant-tabs-tab {
|
||||||
margin-right: 3px !important;
|
margin-right: 3px !important;
|
||||||
@@ -124,9 +144,14 @@
|
|||||||
.ant-dropdown-trigger {
|
.ant-dropdown-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.multiple-tabs-content {
|
&--hide-close {
|
||||||
|
.ant-tabs-close-x {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
&__extra {
|
&__extra {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: @multiple-height;
|
width: @multiple-height;
|
||||||
@@ -146,7 +171,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__info {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: @multiple-height - 2;
|
height: @multiple-height - 2;
|
||||||
@@ -156,4 +181,5 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
123
src/layouts/default/tabs/index.vue
Normal file
123
src/layouts/default/tabs/index.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="getWrapClass">
|
||||||
|
<Tabs
|
||||||
|
type="editable-card"
|
||||||
|
size="small"
|
||||||
|
:animated="false"
|
||||||
|
:hideAdd="true"
|
||||||
|
:tabBarGutter="3"
|
||||||
|
:activeKey="activeKeyRef"
|
||||||
|
@change="handleChange"
|
||||||
|
@edit="handleEdit"
|
||||||
|
>
|
||||||
|
<template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">
|
||||||
|
<TabPane :closable="!(item && item.meta && item.meta.affix)">
|
||||||
|
<template #tab>
|
||||||
|
<TabContent :tabItem="item" />
|
||||||
|
</template>
|
||||||
|
</TabPane>
|
||||||
|
</template>
|
||||||
|
<template #tabBarExtraContent>
|
||||||
|
<QuickButton />
|
||||||
|
</template>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, watch, computed, unref, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Tabs } from 'ant-design-vue';
|
||||||
|
import TabContent from './components/TabContent.vue';
|
||||||
|
|
||||||
|
import { useGo } from '/@/hooks/web/usePage';
|
||||||
|
|
||||||
|
import { tabStore } from '/@/store/modules/tab';
|
||||||
|
import { userStore } from '/@/store/modules/user';
|
||||||
|
|
||||||
|
import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
|
||||||
|
import { REDIRECT_NAME } from '/@/router/constant';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MultipleTabs',
|
||||||
|
components: {
|
||||||
|
QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')),
|
||||||
|
Tabs,
|
||||||
|
TabPane: Tabs.TabPane,
|
||||||
|
TabContent,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const affixTextList = initAffixTabs();
|
||||||
|
|
||||||
|
const activeKeyRef = ref('');
|
||||||
|
|
||||||
|
useTabsDrag(affixTextList);
|
||||||
|
const { prefixCls } = useDesign('multiple-tabs');
|
||||||
|
const go = useGo();
|
||||||
|
|
||||||
|
const getTabsState = computed(() => tabStore.getTabsState);
|
||||||
|
|
||||||
|
const unClose = computed(() => {
|
||||||
|
return getTabsState.value.length === 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWrapClass = computed(() => {
|
||||||
|
return [
|
||||||
|
prefixCls,
|
||||||
|
{
|
||||||
|
[`${prefixCls}--hide-close`]: unClose,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tabStore.getLastChangeRouteState?.path,
|
||||||
|
() => {
|
||||||
|
if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
|
||||||
|
if (!lastChangeRoute || !userStore.getTokenState) return;
|
||||||
|
|
||||||
|
const { path, fullPath } = lastChangeRoute;
|
||||||
|
const p = fullPath || path;
|
||||||
|
|
||||||
|
if (activeKeyRef.value !== p) {
|
||||||
|
activeKeyRef.value = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabStore.addTabAction(lastChangeRoute);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleChange(activeKey: any) {
|
||||||
|
activeKeyRef.value = activeKey;
|
||||||
|
go(activeKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the current tab
|
||||||
|
function handleEdit(targetKey: string) {
|
||||||
|
// Added operation to hide, currently only use delete operation
|
||||||
|
if (unref(unClose)) return;
|
||||||
|
|
||||||
|
tabStore.closeTabByKeyAction(targetKey);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
prefixCls,
|
||||||
|
unClose,
|
||||||
|
getWrapClass,
|
||||||
|
handleEdit,
|
||||||
|
handleChange,
|
||||||
|
activeKeyRef,
|
||||||
|
getTabsState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
@import './index.less';
|
||||||
|
</style>
|
@@ -2,6 +2,7 @@ import Sortable from 'sortablejs';
|
|||||||
import { toRaw, ref, nextTick, onMounted } from 'vue';
|
import { toRaw, ref, nextTick, onMounted } from 'vue';
|
||||||
import { RouteLocationNormalized } from 'vue-router';
|
import { RouteLocationNormalized } from 'vue-router';
|
||||||
import { useProjectSetting } from '/@/hooks/setting';
|
import { useProjectSetting } from '/@/hooks/setting';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
import router from '/@/router';
|
import router from '/@/router';
|
||||||
import { tabStore } from '/@/store/modules/tab';
|
import { tabStore } from '/@/store/modules/tab';
|
||||||
import { isNullAndUnDef } from '/@/utils/is';
|
import { isNullAndUnDef } from '/@/utils/is';
|
||||||
@@ -48,12 +49,12 @@ export function initAffixTabs(): string[] {
|
|||||||
export function useTabsDrag(affixTextList: string[]) {
|
export function useTabsDrag(affixTextList: string[]) {
|
||||||
const { multiTabsSetting } = useProjectSetting();
|
const { multiTabsSetting } = useProjectSetting();
|
||||||
|
|
||||||
|
const { prefixCls } = useDesign('multiple-tabs');
|
||||||
|
|
||||||
function initSortableTabs() {
|
function initSortableTabs() {
|
||||||
if (!multiTabsSetting.canDrag) return;
|
if (!multiTabsSetting.canDrag) return;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const el = document.querySelectorAll(
|
const el = document.querySelectorAll(`.${prefixCls} .ant-tabs-nav > div`)?.[0] as HTMLElement;
|
||||||
'.multiple-tabs .ant-tabs-nav > div'
|
|
||||||
)?.[0] as HTMLElement;
|
|
||||||
|
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
Sortable.create(el, {
|
Sortable.create(el, {
|
25
src/layouts/default/trigger/HeaderTrigger.vue
Normal file
25
src/layouts/default/trigger/HeaderTrigger.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<span :class="[prefixCls, theme]" @click="toggleCollapsed">
|
||||||
|
<MenuUnfoldOutlined v-if="getCollapsed" /> <MenuFoldOutlined v-else />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'SiderTrigger',
|
||||||
|
components: { MenuUnfoldOutlined, MenuFoldOutlined },
|
||||||
|
props: {
|
||||||
|
theme: propTypes.oneOf(['light', 'dark']),
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { getCollapsed, toggleCollapsed } = useMenuSetting();
|
||||||
|
const { prefixCls } = useDesign('layout-header-trigger');
|
||||||
|
return { getCollapsed, toggleCollapsed, prefixCls };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
18
src/layouts/default/trigger/SiderTrigger.vue
Normal file
18
src/layouts/default/trigger/SiderTrigger.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<DoubleRightOutlined v-if="getCollapsed" />
|
||||||
|
<DoubleLeftOutlined v-else />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { DoubleRightOutlined, DoubleLeftOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'SiderTrigger',
|
||||||
|
components: { DoubleRightOutlined, DoubleLeftOutlined },
|
||||||
|
setup() {
|
||||||
|
const { getCollapsed } = useMenuSetting();
|
||||||
|
return { getCollapsed };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
21
src/layouts/default/trigger/index.vue
Normal file
21
src/layouts/default/trigger/index.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<SiderTrigger v-if="sider" />
|
||||||
|
<HeaderTrigger v-else :theme="theme" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LayoutTrigger',
|
||||||
|
components: {
|
||||||
|
SiderTrigger: createAsyncComponent(() => import('./SiderTrigger.vue')),
|
||||||
|
HeaderTrigger: createAsyncComponent(() => import('./HeaderTrigger.vue'), { loading: true }),
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
sider: propTypes.bool.def(true),
|
||||||
|
theme: propTypes.oneOf(['light', 'dark']),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<template v-for="frame in getFramePages" :key="frame.path">
|
<template v-for="frame in getFramePages" :key="frame.path">
|
||||||
<FramePage
|
<FramePage
|
||||||
v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
|
v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
|
||||||
@@ -6,6 +7,7 @@
|
|||||||
:frameSrc="frame.meta.frameSrc"
|
:frameSrc="frame.meta.frameSrc"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import type { AppRouteRecordRaw } from '/@/router/types';
|
import type { AppRouteRecordRaw } from '/@/router/types';
|
||||||
|
|
||||||
import { computed, toRaw, unref } from 'vue';
|
import { computed, toRaw, unref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import router from '/@/router';
|
|
||||||
|
|
||||||
import { tabStore } from '/@/store/modules/tab';
|
import { tabStore } from '/@/store/modules/tab';
|
||||||
|
|
||||||
@@ -10,8 +8,10 @@ import { unique } from '/@/utils';
|
|||||||
|
|
||||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||||
|
|
||||||
|
import router from '/@/router';
|
||||||
|
|
||||||
export function useFrameKeepAlive() {
|
export function useFrameKeepAlive() {
|
||||||
const { currentRoute } = useRouter();
|
const { currentRoute } = router;
|
||||||
const { getShowMultipleTab } = useMultipleTabSetting();
|
const { getShowMultipleTab } = useMultipleTabSetting();
|
||||||
|
|
||||||
const getFramePages = computed(() => {
|
const getFramePages = computed(() => {
|
||||||
|
@@ -10,12 +10,14 @@ import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
|||||||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
|
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
|
||||||
import { useCache } from './useCache';
|
import { useCache } from './useCache';
|
||||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||||
|
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
|
||||||
interface DefaultContext {
|
interface DefaultContext {
|
||||||
Component: FunctionalComponent & { type: { [key: string]: any } };
|
Component: FunctionalComponent & { type: { [key: string]: any } };
|
||||||
route: RouteLocation;
|
route: RouteLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const FrameLayout=createAsyncComponent(()=>'/@/layouts/iframe/index.vue')
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'PageLayout',
|
name: 'PageLayout',
|
||||||
setup() {
|
setup() {
|
||||||
|
@@ -32,7 +32,6 @@ export function useCache(isPage: boolean) {
|
|||||||
|
|
||||||
if (isPage) {
|
if (isPage) {
|
||||||
// page Layout
|
// page Layout
|
||||||
// not parent layout
|
|
||||||
return cached.get(PAGE_LAYOUT_KEY) || [];
|
return cached.get(PAGE_LAYOUT_KEY) || [];
|
||||||
}
|
}
|
||||||
const cacheSet = new Set<string>();
|
const cacheSet = new Set<string>();
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
redo: 'Refresh',
|
redo: 'Refresh current',
|
||||||
close: 'Close',
|
close: 'Close current',
|
||||||
closeLeft: 'Close Left',
|
closeLeft: 'Close Left',
|
||||||
closeRight: 'Close Right',
|
closeRight: 'Close Right',
|
||||||
closeOther: 'Close Other',
|
closeOther: 'Close Other',
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
redo: '刷新',
|
redo: '刷新当前',
|
||||||
close: '关闭',
|
close: '关闭当前',
|
||||||
closeLeft: '关闭左侧',
|
closeLeft: '关闭左侧',
|
||||||
closeRight: '关闭右侧',
|
closeRight: '关闭右侧',
|
||||||
closeOther: '关闭其他',
|
closeOther: '关闭其他',
|
||||||
|
@@ -6,7 +6,7 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception');
|
|||||||
/**
|
/**
|
||||||
* @description: default layout
|
* @description: default layout
|
||||||
*/
|
*/
|
||||||
export const LAYOUT = () => import('/@/layouts/default/index');
|
export const LAYOUT = () => import('/@/layouts/default/index.vue');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: page-layout
|
* @description: page-layout
|
||||||
|
Reference in New Issue
Block a user