提交 27e50b47 编写于 作者: V vben

perf(tabs): perf multiple-tabs

上级 ed41e508
import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export const Dropdown = createAsyncComponent(() => import('./src/Dropdown'));
import Dropdown from './src/Dropdown';
withInstall(Dropdown);
export * from './src/types';
export { Dropdown };
......@@ -243,6 +243,7 @@ export default defineComponent({
onOpenChange={handleOpenChange}
class={unref(getMenuClass)}
onClick={handleMenuClick}
subMenuOpenDelay={0.2}
{...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 { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
import { useFullContent } from '/@/hooks/web/useFullContent';
// Get menu configuration
const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
......@@ -78,6 +79,15 @@ const getCalcContentWidth = computed(() => {
return `calc(100% - ${unref(width)}px)`;
});
const { getFullContent: fullContent } = useFullContent();
const getShowSidebar = computed(() => {
return (
unref(getSplit) ||
(unref(getShowMenu) && unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && !unref(fullContent))
);
});
// Set menu configuration
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
appStore.commitProjectConfigState({ menuSetting });
......@@ -119,5 +129,6 @@ export function useMenuSetting() {
getMenuHidden,
getIsTopMenu,
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 @@
:loading="getPageLoading"
background="rgba(240, 242, 245, 0.6)"
absolute
:class="`${prefixCls}__loading`"
:class="`${prefixCls}-loading`"
/>
</transition>
<PageLayout />
......@@ -53,7 +53,7 @@
margin: 0 auto;
}
&__loading {
&-loading {
position: absolute;
top: 200px;
z-index: @page-loading-z-index;
......
<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';
import LayoutMenu from '../menu';
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
import LockAction from './actions/LockAction';
import LayoutTrigger from '../LayoutTrigger';
import LayoutTrigger from '../trigger/index.vue';
import NoticeAction from './notice/NoticeActionItem.vue';
import {
RedoOutlined,
......
......@@ -3,7 +3,7 @@ import './LayoutMultipleHeader.less';
import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue';
import LayoutHeader from './LayoutHeader';
import MultipleTabs from '../multitabs/index';
import MultipleTabs from '../tabs/index.vue';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
......
@import (reference) '../../../design/index.less';
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
.layout-header {
display: flex;
......@@ -24,7 +25,7 @@
height: 100%;
align-items: center;
.layout-trigger {
.@{header-trigger-prefix-cls} {
display: flex;
height: 100%;
padding: 1px 10px 0 16px;
......
import './index.less';
import { defineComponent, unref, computed, ref } from 'vue';
import { Layout, BackTop } from 'ant-design-vue';
import LayoutHeader from './header/LayoutHeader';
import { defineComponent, unref, ref } from 'vue';
import { Layout } from 'ant-design-vue';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import LayoutHeader from './header/LayoutHeader';
import LayoutContent from './content/index.vue';
import LayoutFooter from './footer';
import LayoutLockPage from '/@/views/sys/lock/index.vue';
import LayoutSideBar from './sider';
import SettingBtn from './setting/index.vue';
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
import { MenuModeEnum } from '/@/enums/menuEnum';
import { useRouter } from 'vue-router';
import { useFullContent } from '/@/hooks/web/useFullContent';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { createLayoutContext } from './useLayoutContext';
import { registerGlobComp } from '/@/components/registerGlobComp';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
import { isMobile } from '/@/utils/is';
const LayoutFeatures = createAsyncComponent(() => import('/@/layouts/default/feature/index.vue'));
const LayoutFooter = createAsyncComponent(() => import('/@/layouts/default/footer/index.vue'));
export default defineComponent({
name: 'DefaultLayout',
setup() {
const { currentRoute } = useRouter();
const headerRef = ref<ComponentRef>(null);
const isMobileRef = ref(false);
......@@ -43,56 +39,27 @@ export default defineComponent({
const { getShowFullHeaderRef } = useHeaderSetting();
const { getUseOpenBackTop, getShowSettingButton, getShowFooter } = useRootSetting();
const { getShowMenu, getMenuMode, getSplit } = useMenuSetting();
const { getFullContent } = useFullContent();
const getShowLayoutFooter = computed(() => {
return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
});
const showSideBarRef = computed(() => {
return (
unref(getSplit) ||
(unref(getShowMenu) &&
unref(getMenuMode) !== MenuModeEnum.HORIZONTAL &&
!unref(getFullContent))
);
});
function renderFeatures() {
return (
<>
<LayoutLockPage />
{/* back top */}
{unref(getUseOpenBackTop) && <BackTop target={() => document.body} />}
{/* open setting drawer */}
{unref(getShowSettingButton) && <SettingBtn />}
</>
);
}
const { getShowSidebar } = useMenuSetting();
return () => {
return (
<Layout class="default-layout">
{() => (
<>
{renderFeatures()}
<LayoutFeatures />
{unref(getShowFullHeaderRef) && <LayoutHeader fixed={true} ref={headerRef} />}
<Layout>
{() => (
<>
{unref(showSideBarRef) && <LayoutSideBar />}
{unref(getShowSidebar) && <LayoutSideBar />}
<Layout class="default-layout__main">
{() => (
<>
<LayoutMultipleHeader />
<LayoutContent />
{unref(getShowLayoutFooter) && <LayoutFooter />}
<LayoutFooter />
</>
)}
</Layout>
......
<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>
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>
);
};
},
});
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';
export default defineComponent({
name: 'SettingBtn',
name: 'SettingButton',
components: { SettingOutlined, SettingDrawer },
setup() {
const [register, { openDrawer }] = useDrawer();
......
import type { Ref } from 'vue';
import { computed, unref, onMounted, nextTick, ref } from 'vue';
import LayoutTrigger from '/@/layouts/default/LayoutTrigger';
import LayoutTrigger from '/@/layouts/default/trigger/index.vue';
import { TriggerEnum } from '/@/enums/menuEnum';
......
<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>
<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>
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-multiple-tabs';
.multiple-tabs {
.@{prefix-cls} {
z-index: 10;
height: @multiple-height + 2;
padding: 0 0 2px 0;
margin-left: -1px;
line-height: @multiple-height + 2;
background: @white;
box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
......@@ -32,13 +31,33 @@
line-height: calc(@multiple-height - 2px);
color: @text-color-call-out;
background: @white;
border: 1px solid darken(@border-color-light, 8%);
border: 1px solid darken(@border-color-light, 6%);
transition: none;
&:not(.ant-tabs-tab-active)::before {
position: absolute;
top: -1px;
left: 50%;
width: 100%;
height: 2px;
background-color: @primary-color;
content: '';
opacity: 0;
transform: translate(-50%, 0) scaleX(0);
transform-origin: center;
transition: none;
}
&:hover {
.ant-tabs-close-x {
opacity: 1;
}
&:not(.ant-tabs-tab-active)::before {
opacity: 1;
transform: translate(-50%, 0) scaleX(1);
transition: all 0.3s ease-in-out;
}
}
.ant-tabs-close-x {
......@@ -51,7 +70,7 @@
&:hover {
svg {
width: 0.75em;
width: 0.8em;
}
}
}
......@@ -73,6 +92,7 @@
color: @white;
background: fade(@primary-color, 100%);
border: 0;
transition: none;
&::before {
position: absolute;
......@@ -98,7 +118,7 @@
}
.ant-tabs-nav > div:nth-child(1) {
padding: 0 10px;
padding: 0 6px;
.ant-tabs-tab {
margin-right: 3px !important;
......@@ -124,36 +144,42 @@
.ant-dropdown-trigger {
display: inline-flex;
}
}
.multiple-tabs-content {
&__extra {
display: inline-block;
width: @multiple-height;
height: @multiple-height;
line-height: @multiple-height;
color: #999;
text-align: center;
cursor: pointer;
border-left: 1px solid #eee;
&:hover {
color: @text-color-base;
&--hide-close {
.ant-tabs-close-x {
opacity: 0 !important;
}
}
&-content {
&__extra {
display: inline-block;
width: @multiple-height;
height: @multiple-height;
line-height: @multiple-height;
color: #999;
text-align: center;
cursor: pointer;
border-left: 1px solid #eee;
&:hover {
color: @text-color-base;
}
span[role='img'] {
transform: rotate(90deg);
span[role='img'] {
transform: rotate(90deg);
}
}
}
&__content {
display: inline-block;
width: 100%;
height: @multiple-height - 2;
padding-left: 0;
margin-left: -10px;
font-size: 12px;
cursor: pointer;
user-select: none;
&__info {
display: inline-block;
width: 100%;
height: @multiple-height - 2;
padding-left: 0;
margin-left: -10px;
font-size: 12px;
cursor: pointer;
user-select: none;
}
}
}
<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 { RouteLocationNormalized } from 'vue-router';
import { useProjectSetting } from '/@/hooks/setting';
import { useDesign } from '/@/hooks/web/useDesign';
import router from '/@/router';
import { tabStore } from '/@/store/modules/tab';
import { isNullAndUnDef } from '/@/utils/is';
......@@ -48,12 +49,12 @@ export function initAffixTabs(): string[] {
export function useTabsDrag(affixTextList: string[]) {
const { multiTabsSetting } = useProjectSetting();
const { prefixCls } = useDesign('multiple-tabs');
function initSortableTabs() {
if (!multiTabsSetting.canDrag) return;
nextTick(() => {
const el = document.querySelectorAll(
'.multiple-tabs .ant-tabs-nav > div'
)?.[0] as HTMLElement;
const el = document.querySelectorAll(`.${prefixCls} .ant-tabs-nav > div`)?.[0] as HTMLElement;
if (!el) return;
Sortable.create(el, {
......
<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 v-for="frame in getFramePages" :key="frame.path">
<FramePage
v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
v-show="showIframe(frame)"
:frameSrc="frame.meta.frameSrc"
/>
</template>
<div>
<template v-for="frame in getFramePages" :key="frame.path">
<FramePage
v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
v-show="showIframe(frame)"
:frameSrc="frame.meta.frameSrc"
/>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
......
import type { AppRouteRecordRaw } from '/@/router/types';
import { computed, toRaw, unref } from 'vue';
import { useRouter } from 'vue-router';
import router from '/@/router';
import { tabStore } from '/@/store/modules/tab';
......@@ -10,8 +8,10 @@ import { unique } from '/@/utils';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import router from '/@/router';
export function useFrameKeepAlive() {
const { currentRoute } = useRouter();
const { currentRoute } = router;
const { getShowMultipleTab } = useMultipleTabSetting();
const getFramePages = computed(() => {
......
......@@ -10,12 +10,14 @@ import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import { useCache } from './useCache';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
interface DefaultContext {
Component: FunctionalComponent & { type: { [key: string]: any } };
route: RouteLocation;
}
// const FrameLayout=createAsyncComponent(()=>'/@/layouts/iframe/index.vue')
export default defineComponent({
name: 'PageLayout',
setup() {
......
......@@ -32,7 +32,6 @@ export function useCache(isPage: boolean) {
if (isPage) {
// page Layout
// not parent layout
return cached.get(PAGE_LAYOUT_KEY) || [];
}
const cacheSet = new Set<string>();
......
export default {
redo: 'Refresh',
close: 'Close',
redo: 'Refresh current',
close: 'Close current',
closeLeft: 'Close Left',
closeRight: 'Close Right',
closeOther: 'Close Other',
......
export default {
redo: '刷新',
close: '关闭',
redo: '刷新当前',
close: '关闭当前',
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeOther: '关闭其他',
......
......@@ -6,7 +6,7 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception');
/**
* @description: default layout
*/
export const LAYOUT = () => import('/@/layouts/default/index');
export const LAYOUT = () => import('/@/layouts/default/index.vue');
/**
* @description: page-layout
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册