perf(tabs): perf multiple-tabs
5个文件已删除
9个文件已添加
17个文件已修改
4 文件已重命名
| | |
| | | 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 }; |
| | |
| | | onOpenChange={handleOpenChange} |
| | | class={unref(getMenuClass)} |
| | | onClick={handleMenuClick} |
| | | subMenuOpenDelay={0.2} |
| | | {...unref(getInlineCollapseOptions)} |
| | | > |
| | | {{ |
| | |
| | | |
| | | 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); |
| | |
| | | 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 }); |
| | |
| | | getMenuHidden, |
| | | getIsTopMenu, |
| | | getMenuBgColor, |
| | | getShowSidebar, |
| | | }; |
| | | } |
| | |
| | | :loading="getPageLoading" |
| | | background="rgba(240, 242, 245, 0.6)" |
| | | absolute |
| | | :class="`${prefixCls}__loading`" |
| | | :class="`${prefixCls}-loading`" |
| | | /> |
| | | </transition> |
| | | <PageLayout /> |
| | |
| | | margin: 0 auto; |
| | | } |
| | | |
| | | &__loading { |
| | | &-loading { |
| | | position: absolute; |
| | | top: 200px; |
| | | z-index: @page-loading-z-index; |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
| | |
| | | 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, |
| | |
| | | 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; |
| | |
| | | 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); |
| | | |
| | |
| | | |
| | | 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> |
New file |
| | |
| | | <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 { 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'; |
| | | |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
File was renamed from src/layouts/default/multitabs/index.less |
| | |
| | | @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); |
| | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | &:hover { |
| | | svg { |
| | | width: 0.75em; |
| | | width: 0.8em; |
| | | } |
| | | } |
| | | } |
| | |
| | | color: @white; |
| | | background: fade(@primary-color, 100%); |
| | | border: 0; |
| | | transition: none; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | |
| | | } |
| | | |
| | | .ant-tabs-nav > div:nth-child(1) { |
| | | padding: 0 10px; |
| | | padding: 0 6px; |
| | | |
| | | .ant-tabs-tab { |
| | | margin-right: 3px !important; |
| | |
| | | .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; |
| | | } |
| | | |
| | | span[role='img'] { |
| | | transform: rotate(90deg); |
| | | &--hide-close { |
| | | .ant-tabs-close-x { |
| | | opacity: 0 !important; |
| | | } |
| | | } |
| | | |
| | | &__content { |
| | | display: inline-block; |
| | | width: 100%; |
| | | height: @multiple-height - 2; |
| | | padding-left: 0; |
| | | margin-left: -10px; |
| | | font-size: 12px; |
| | | cursor: pointer; |
| | | user-select: none; |
| | | &-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); |
| | | } |
| | | } |
| | | |
| | | &__info { |
| | | display: inline-block; |
| | | width: 100%; |
| | | height: @multiple-height - 2; |
| | | padding-left: 0; |
| | | margin-left: -10px; |
| | | font-size: 12px; |
| | | cursor: pointer; |
| | | user-select: none; |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | <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> |
File was renamed from src/layouts/default/multitabs/useMultipleTabs.ts |
| | |
| | | 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'; |
| | |
| | | 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, { |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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'; |
| | | |
| | |
| | | |
| | | 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(() => { |
| | |
| | | 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() { |
| | |
| | | |
| | | 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: '关闭其他', |
| | |
| | | /** |
| | | * @description: default layout |
| | | */ |
| | | export const LAYOUT = () => import('/@/layouts/default/index'); |
| | | export const LAYOUT = () => import('/@/layouts/default/index.vue'); |
| | | |
| | | /** |
| | | * @description: page-layout |