35 changed files with 593 additions and 401 deletions
@ -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 }; |
||||
|
|||||
@ -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} />; |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -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> |
|
||||
); |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -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> |
||||
@ -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> |
|
||||
); |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
Loading…
Reference in new issue