提交 27e50b47 作者: vben

perf(tabs): perf multiple-tabs

上级 ed41e508
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({ ...@@ -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'; ...@@ -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(() => { ...@@ -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() { ...@@ -119,5 +129,6 @@ export function useMenuSetting() {
getMenuHidden, getMenuHidden,
getIsTopMenu, getIsTopMenu,
getMenuBgColor, getMenuBgColor,
getShowSidebar,
}; };
} }
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 @@ ...@@ -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 @@ ...@@ -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;
......
<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>
@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;
}
}
}
}
import './index.less';
import { defineComponent } from 'vue';
import { Layout } from 'ant-design-vue';
import { GithubFilled } from '@ant-design/icons-vue';
import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
import { openWindow } from '/@/utils';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'LayoutContent',
setup() {
const { t } = useI18n();
return () => {
return (
<Layout.Footer class="layout-footer">
{() => (
<>
<div class="layout-footer__links">
<a onClick={() => openWindow(SITE_URL)}>{t('layout.footer.onlinePreview')}</a>
<GithubFilled onClick={() => openWindow(GITHUB_URL)} class="github" />
<a onClick={() => openWindow(DOC_URL)}>{t('layout.footer.onlineDocument')}</a>
</div>
<div>Copyright &copy;2020 Vben Admin</div>
</>
)}
</Layout.Footer>
);
};
},
});
<template>
<Footer :class="prefixCls" v-if="getShowLayoutFooter">
<div :class="`${prefixCls}__links`">
<a @click="openWindow(SITE_URL)">{{ t('layout.footer.onlinePreview') }}</a>
<GithubFilled @click="openWindow(GITHUB_URL)" :class="`${prefixCls}__github`" />
<a @click="openWindow(DOC_URL)">{{ t('layout.footer.onlineDocument') }}</a>
</div>
<div>Copyright &copy;2020 Vben Admin</div>
</Footer>
</template>
<script lang="ts">
import { computed, defineComponent, unref } from 'vue';
import { Layout } from 'ant-design-vue';
import { GithubFilled } from '@ant-design/icons-vue';
import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
import { openWindow } from '/@/utils';
import { useI18n } from '/@/hooks/web/useI18n';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useRouter } from 'vue-router';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'LayoutFooter',
components: { Footer: Layout.Footer, GithubFilled },
setup() {
const { t } = useI18n();
const { getShowFooter } = useRootSetting();
const { currentRoute } = useRouter();
const { prefixCls } = useDesign('layout-footer');
const getShowLayoutFooter = computed(() => {
return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
});
return { getShowLayoutFooter, prefixCls, t, DOC_URL, GITHUB_URL, SITE_URL, openWindow };
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-layout-footer';
@normal-color: rgba(0, 0, 0, 0.45);
@hover-color: rgba(0, 0, 0, 0.85);
.@{prefix-cls} {
color: @normal-color;
text-align: center;
&__links {
margin-bottom: 8px;
a {
color: @normal-color;
&:hover {
color: @hover-color;
}
}
}
&__github {
margin: 0 30px;
&:hover {
color: @hover-color;
}
}
}
</style>
...@@ -19,7 +19,7 @@ import UserDropdown from './UserDropdown'; ...@@ -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'; ...@@ -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';
......
@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 @@ ...@@ -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;
......
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({ ...@@ -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>
......
<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>
...@@ -13,7 +13,7 @@ ...@@ -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();
......
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';
......
<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>
import type { PropType } from 'vue'; <template>
import { Dropdown } from '/@/components/Dropdown/index'; <Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent">
<div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="isTabs">
import { defineComponent, unref, FunctionalComponent } from 'vue'; <span class="ml-1">{{ getTitle }}</span>
</div>
import { TabContentProps } from './types'; <span :class="`${prefixCls}__extra`" v-else>
<RightOutlined />
</span>
</Dropdown>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { RightOutlined } from '@ant-design/icons-vue'; import { defineComponent, computed } from 'vue';
import { Dropdown } from '/@/components/Dropdown/index';
import { TabContentEnum } from './types'; import { TabContentProps, TabContentEnum } from '../types';
import { useTabDropdown } from './useTabDropdown'; import { RightOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { RouteLocationNormalized } from 'vue-router'; import { useDesign } from '/@/hooks/web/useDesign';
import { useTabDropdown } from '../useTabDropdown';
import { useI18n } from '/@/hooks/web/useI18n';
const { t: titleT } = useI18n(); import { RouteLocationNormalized } from 'vue-router';
export default defineComponent({
name: 'TabContent',
components: { Dropdown, RightOutlined },
props: {
tabItem: {
type: Object as PropType<RouteLocationNormalized>,
default: null,
},
const ExtraContent: FunctionalComponent = () => { type: {
return ( type: Number as PropType<TabContentEnum>,
<span class={`multiple-tabs-content__extra `}> default: TabContentEnum.TAB_TYPE,
<RightOutlined /> },
</span> },
); setup(props) {
}; const { prefixCls } = useDesign('multiple-tabs-content');
const { t } = useI18n();
const TabContent: FunctionalComponent<{ tabItem: RouteLocationNormalized; handler: Fn }> = ( const getTitle = computed(() => {
props const { tabItem: { meta } = {} } = props;
) => { return meta && t(meta.title);
const { tabItem: { meta } = {} } = props; });
return ( const {
<div class={`multiple-tabs-content__content `} onContextmenu={props.handler(props.tabItem)}> getDropMenuList,
<span class="ml-1">{meta && titleT(meta.title)}</span> handleMenuEvent,
</div> handleContextMenu,
); getTrigger,
}; isTabs,
} = useTabDropdown(props as TabContentProps);
export default defineComponent({ function handleContext(e: ChangeEvent) {
name: 'TabContent', props.tabItem && handleContextMenu(props.tabItem)(e);
props: { }
tabItem: { return {
type: Object as PropType<RouteLocationNormalized>, prefixCls,
default: null, getDropMenuList,
handleMenuEvent,
handleContext,
getTrigger,
isTabs,
getTitle,
};
}, },
});
type: { </script>
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>
);
};
},
});
@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 @@ ...@@ -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 @@ ...@@ -51,7 +70,7 @@
&:hover { &:hover {
svg { svg {
width: 0.75em; width: 0.8em;
} }
} }
} }
...@@ -73,6 +92,7 @@ ...@@ -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 @@ ...@@ -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,36 +144,42 @@ ...@@ -124,36 +144,42 @@
.ant-dropdown-trigger { .ant-dropdown-trigger {
display: inline-flex; display: inline-flex;
} }
}
.multiple-tabs-content { &--hide-close {
&__extra { .ant-tabs-close-x {
display: inline-block; opacity: 0 !important;
width: @multiple-height;
height: @multiple-height;
line-height: @multiple-height;
color: #999;
text-align: center;
cursor: pointer;
border-left: 1px solid #eee;
&:hover {
color: @text-color-base;
} }
}
&-content {
&__extra {
display: inline-block;
width: @multiple-height;
height: @multiple-height;
line-height: @multiple-height;
color: #999;
text-align: center;
cursor: pointer;
border-left: 1px solid #eee;
&:hover {
color: @text-color-base;
}
span[role='img'] { span[role='img'] {
transform: rotate(90deg); transform: rotate(90deg);
}
} }
}
&__content { &__info {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
height: @multiple-height - 2; height: @multiple-height - 2;
padding-left: 0; padding-left: 0;
margin-left: -10px; margin-left: -10px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
}
} }
} }
import './index.less'; <template>
<div :class="getWrapClass">
import type { TabContentProps } from './types'; <Tabs
type="editable-card"
import { defineComponent, watch, computed, unref, ref } from 'vue'; size="small"
import { useRouter } from 'vue-router'; :animated="false"
:hideAdd="true"
import { Tabs } from 'ant-design-vue'; :tabBarGutter="3"
import TabContent from './TabContent'; :activeKey="activeKeyRef"
@change="handleChange"
import { useGo } from '/@/hooks/web/usePage'; @edit="handleEdit"
>
import { TabContentEnum } from './types'; <template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">
<TabPane :closable="!(item && item.meta && item.meta.affix)">
import { tabStore } from '/@/store/modules/tab'; <template #tab>
import { userStore } from '/@/store/modules/user'; <TabContent :tabItem="item" />
</template>
import { initAffixTabs, useTabsDrag } from './useMultipleTabs'; </TabPane>
import { REDIRECT_NAME } from '/@/router/constant'; </template>
<template #tabBarExtraContent>
export default defineComponent({ <QuickButton />
name: 'MultipleTabs', </template>
setup() { </Tabs>
const activeKeyRef = ref(''); </div>
</template>
const affixTextList = initAffixTabs(); <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;
});
useTabsDrag(affixTextList); const getWrapClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--hide-close`]: unClose,
},
];
});
const go = useGo(); 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,
}
);
const { currentRoute } = useRouter(); function handleChange(activeKey: any) {
activeKeyRef.value = activeKey;
go(activeKey, false);
}
const getTabsState = computed(() => tabStore.getTabsState); // Close the current tab
function handleEdit(targetKey: string) {
// Added operation to hide, currently only use delete operation
if (unref(unClose)) return;
watch( tabStore.closeTabByKeyAction(targetKey);
() => 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,
} }
); return {
prefixCls,
function handleChange(activeKey: any) { unClose,
activeKeyRef.value = activeKey; getWrapClass,
go(activeKey, false); handleEdit,
} handleChange,
activeKeyRef,
// Close the current tab getTabsState,
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} />; },
} });
</script>
function renderTabs() { <style lang="less">
return unref(getTabsState).map((item) => { @import './index.less';
const key = item.query ? item.fullPath : item.path; </style>
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>
);
};
},
});
...@@ -2,6 +2,7 @@ import Sortable from 'sortablejs'; ...@@ -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[] { ...@@ -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, {
......
<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>
<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>
<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>
<template> <template>
<template v-for="frame in getFramePages" :key="frame.path"> <div>
<FramePage <template v-for="frame in getFramePages" :key="frame.path">
v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)" <FramePage
v-show="showIframe(frame)" v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
:frameSrc="frame.meta.frameSrc" v-show="showIframe(frame)"
/> :frameSrc="frame.meta.frameSrc"
</template> />
</template>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
......
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'; ...@@ -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'; ...@@ -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) { ...@@ -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>();
......
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',
......
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'); ...@@ -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
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论