123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- <script setup lang="ts">
- import type { CSSProperties } from 'vue';
- import { computed, ref, watch } from 'vue';
- import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
- import {
- LayoutContent,
- LayoutFooter,
- LayoutHeader,
- LayoutSidebar,
- LayoutTabbar,
- } from './components';
- import { VbenLayoutProps } from './vben-layout';
- interface Props extends VbenLayoutProps {}
- defineOptions({
- name: 'VbenLayout',
- });
- const props = withDefaults(defineProps<Props>(), {
- contentCompact: 'wide',
- contentPadding: 0,
- contentPaddingBottom: 0,
- contentPaddingLeft: 0,
- contentPaddingRight: 0,
- contentPaddingTop: 0,
- footerEnable: false,
- footerFixed: true,
- footerHeight: 32,
- headerHeight: 50,
- headerHeightOffset: 10,
- headerHidden: false,
- headerMode: 'fixed',
- headerToggleSidebarButton: true,
- headerVisible: true,
- isMobile: false,
- layout: 'sidebar-nav',
- sideCollapseWidth: 60,
- sidebarCollapseShowTitle: false,
- sidebarHidden: false,
- sidebarMixedWidth: 80,
- sidebarSemiDark: true,
- sidebarTheme: 'dark',
- sidebarWidth: 180,
- tabbarEnable: true,
- tabbarHeight: 38,
- zIndex: 200,
- });
- const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();
- const sidebarCollapse = defineModel<boolean>('sidebarCollapse');
- const sidebarExtraVisible = defineModel<boolean>('sidebarExtraVisible');
- const sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse');
- const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
- const sidebarEnable = defineModel<boolean>('sidebarEnable', { default: true });
- const {
- arrivedState,
- directions,
- isScrolling,
- y: scrollY,
- } = useScroll(document);
- const { y: mouseY } = useMouse({ type: 'client' });
- // side是否处于hover状态展开菜单中
- const sidebarExpandOnHovering = ref(false);
- // const sideHidden = ref(false);
- const headerIsHidden = ref(false);
- const realLayout = computed(() => {
- return props.isMobile ? 'sidebar-nav' : props.layout;
- });
- /**
- * 是否全屏显示content,不需要侧边、底部、顶部、tab区域
- */
- const fullContent = computed(() => realLayout.value === 'full-content');
- /**
- * 是否侧边混合模式
- */
- const isSidebarMixedNav = computed(
- () => realLayout.value === 'sidebar-mixed-nav',
- );
- /**
- * 是否为头部导航模式
- */
- const isHeaderNav = computed(() => realLayout.value === 'header-nav');
- /**
- * 是否为混合导航模式
- */
- const isMixedNav = computed(() => realLayout.value === 'mixed-nav');
- /**
- * 顶栏是否自动隐藏
- */
- const isHeaderAuto = computed(() => props.headerMode === 'auto');
- /**
- * header区域高度
- */
- const getHeaderHeight = computed(() => {
- const { headerHeight, headerHeightOffset } = props;
- // if (!headerVisible) {
- // return 0;
- // }
- // 顶部存在导航时,增加10
- const offset = isMixedNav.value || isHeaderNav.value ? headerHeightOffset : 0;
- return headerHeight + offset;
- });
- const headerWrapperHeight = computed(() => {
- let height = 0;
- if (props.headerVisible && !props.headerHidden) {
- height += getHeaderHeight.value;
- }
- if (props.tabbarEnable) {
- height += props.tabbarHeight;
- }
- return height;
- });
- const getSideCollapseWidth = computed(() => {
- const { sideCollapseWidth, sidebarCollapseShowTitle, sidebarMixedWidth } =
- props;
- return sidebarCollapseShowTitle || isSidebarMixedNav.value
- ? sidebarMixedWidth
- : sideCollapseWidth;
- });
- /**
- * 动态获取侧边区域是否可见
- */
- const sidebarEnableState = computed(() => {
- return !isHeaderNav.value && sidebarEnable.value;
- });
- /**
- * 侧边区域离顶部高度
- */
- const sidePaddingTop = computed(() => {
- const { isMobile } = props;
- return isMixedNav.value && !isMobile ? getHeaderHeight.value : 0;
- });
- /**
- * 动态获取侧边宽度
- */
- const getSidebarWidth = computed(() => {
- const { isMobile, sidebarHidden, sidebarMixedWidth, sidebarWidth } = props;
- let width = 0;
- if (sidebarHidden) {
- return width;
- }
- if (
- !sidebarEnableState.value ||
- (sidebarHidden && !isSidebarMixedNav.value && !isMixedNav.value)
- ) {
- return width;
- }
- if (isSidebarMixedNav.value && !isMobile) {
- width = sidebarMixedWidth;
- } else if (sidebarCollapse.value) {
- width = isMobile ? 0 : getSideCollapseWidth.value;
- } else {
- width = sidebarWidth;
- }
- return width;
- });
- /**
- * 获取扩展区域宽度
- */
- const getExtraWidth = computed(() => {
- const { sidebarWidth } = props;
- return sidebarExtraCollapse.value ? getSideCollapseWidth.value : sidebarWidth;
- });
- /**
- * 是否侧边栏模式,包含混合侧边
- */
- const isSideMode = computed(() =>
- ['mixed-nav', 'sidebar-mixed-nav', 'sidebar-nav'].includes(realLayout.value),
- );
- const showSidebar = computed(() => {
- // if (isMixedNav.value && !props.sideHidden) {
- // return false;
- // }
- return isSideMode.value && sidebarEnable.value;
- });
- const sidebarFace = computed(() => {
- const { sidebarSemiDark, sidebarTheme } = props;
- const isDark = sidebarTheme === 'dark' || sidebarSemiDark;
- return {
- theme: isDark ? 'dark' : 'light',
- };
- });
- /**
- * 遮罩可见性
- */
- const maskVisible = computed(() => !sidebarCollapse.value && props.isMobile);
- /**
- * header fixed值
- */
- const headerFixed = computed(() => {
- const { headerMode } = props;
- return (
- isMixedNav.value ||
- headerMode === 'fixed' ||
- headerMode === 'auto-scroll' ||
- headerMode === 'auto'
- );
- });
- const mainStyle = computed(() => {
- let width = '100%';
- let sidebarAndExtraWidth = 'unset';
- if (
- headerFixed.value &&
- realLayout.value !== 'header-nav' &&
- realLayout.value !== 'mixed-nav' &&
- showSidebar.value &&
- !props.isMobile
- ) {
- // fixed模式下生效
- const isSideNavEffective =
- isSidebarMixedNav.value &&
- sidebarExpandOnHover.value &&
- sidebarExtraVisible.value;
- if (isSideNavEffective) {
- const sideCollapseWidth = sidebarCollapse.value
- ? getSideCollapseWidth.value
- : props.sidebarMixedWidth;
- const sideWidth = sidebarExtraCollapse.value
- ? getSideCollapseWidth.value
- : props.sidebarWidth;
- // 100% - 侧边菜单混合宽度 - 菜单宽度
- sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;
- width = `calc(100% - ${sidebarAndExtraWidth})`;
- } else {
- sidebarAndExtraWidth =
- sidebarExpandOnHovering.value && !sidebarExpandOnHover.value
- ? `${getSideCollapseWidth.value}px`
- : `${getSidebarWidth.value}px`;
- width = `calc(100% - ${sidebarAndExtraWidth})`;
- }
- }
- return {
- sidebarAndExtraWidth,
- width,
- };
- });
- const tabbarStyle = computed((): CSSProperties => {
- let width = '';
- let marginLeft = 0;
- if (!isMixedNav.value) {
- width = '100%';
- } else if (sidebarEnable.value) {
- marginLeft = sidebarCollapse.value
- ? getSideCollapseWidth.value
- : props.sidebarWidth;
- width = `calc(100% - ${getSidebarWidth.value}px)`;
- } else {
- width = '100%';
- }
- return {
- marginLeft: `${marginLeft}px`,
- width,
- };
- });
- const contentStyle = computed((): CSSProperties => {
- const fixed = headerFixed.value;
- const { footerEnable, footerFixed, footerHeight } = props;
- return {
- marginTop:
- fixed &&
- !fullContent.value &&
- !headerIsHidden.value &&
- (!isHeaderAuto.value || scrollY.value < headerWrapperHeight.value)
- ? `${headerWrapperHeight.value}px`
- : 0,
- paddingBottom: `${footerEnable && footerFixed ? footerHeight : 0}px`,
- };
- });
- const headerZIndex = computed(() => {
- const { zIndex } = props;
- const offset = isMixedNav.value ? 1 : 0;
- return zIndex + offset;
- });
- const headerWrapperStyle = computed((): CSSProperties => {
- const fixed = headerFixed.value;
- return {
- height: fullContent.value ? '0' : `${headerWrapperHeight.value}px`,
- left: isMixedNav.value ? 0 : mainStyle.value.sidebarAndExtraWidth,
- position: fixed ? 'fixed' : 'static',
- top:
- headerIsHidden.value || fullContent.value
- ? `-${headerWrapperHeight.value}px`
- : 0,
- width: mainStyle.value.width,
- 'z-index': headerZIndex.value,
- };
- });
- /**
- * 侧边栏z-index
- */
- const sidebarZIndex = computed(() => {
- const { isMobile, zIndex } = props;
- const offset = isMobile || isSideMode.value ? 1 : -1;
- return zIndex + offset;
- });
- const footerWidth = computed(() => {
- if (!props.footerFixed) {
- return '100%';
- }
- return mainStyle.value.width;
- });
- const maskStyle = computed((): CSSProperties => {
- return { zIndex: props.zIndex };
- });
- const showHeaderToggleButton = computed(() => {
- return (
- props.headerToggleSidebarButton &&
- isSideMode.value &&
- !isSidebarMixedNav.value &&
- !isMixedNav.value &&
- !props.isMobile
- );
- });
- const showHeaderLogo = computed(() => {
- return !isSideMode.value || isMixedNav.value || props.isMobile;
- });
- watch(
- () => props.isMobile,
- (val) => {
- sidebarCollapse.value = val;
- },
- );
- {
- const mouseMove = () => {
- mouseY.value > headerWrapperHeight.value
- ? (headerIsHidden.value = true)
- : (headerIsHidden.value = false);
- };
- watch(
- [() => props.headerMode, () => mouseY.value],
- () => {
- if (!isHeaderAuto.value || isMixedNav.value || fullContent.value) {
- return;
- }
- headerIsHidden.value = true;
- mouseMove();
- },
- {
- immediate: true,
- },
- );
- }
- {
- const checkHeaderIsHidden = useThrottleFn((top, bottom, topArrived) => {
- if (scrollY.value < headerWrapperHeight.value) {
- headerIsHidden.value = false;
- return;
- }
- if (topArrived) {
- headerIsHidden.value = false;
- return;
- }
- if (top) {
- headerIsHidden.value = false;
- } else if (bottom) {
- headerIsHidden.value = true;
- }
- }, 300);
- watch(
- () => scrollY.value,
- () => {
- if (
- props.headerMode !== 'auto-scroll' ||
- isMixedNav.value ||
- fullContent.value
- ) {
- return;
- }
- if (isScrolling.value) {
- checkHeaderIsHidden(
- directions.top,
- directions.bottom,
- arrivedState.top,
- );
- }
- },
- );
- }
- function handleClickMask() {
- sidebarCollapse.value = true;
- }
- function handleToggleSidebar() {
- emit('toggleSidebar');
- }
- function handleOpenMenu() {
- sidebarCollapse.value = false;
- }
- </script>
- <template>
- <div class="relative flex min-h-full w-full">
- <slot name="preferences"></slot>
- <slot name="floating-groups"></slot>
- <LayoutSidebar
- v-if="sidebarEnableState"
- v-model:collapse="sidebarCollapse"
- v-model:expand-on-hover="sidebarExpandOnHover"
- v-model:expand-on-hovering="sidebarExpandOnHovering"
- v-model:extra-collapse="sidebarExtraCollapse"
- v-model:extra-visible="sidebarExtraVisible"
- :collapse-width="getSideCollapseWidth"
- :dom-visible="!isMobile"
- :extra-width="getExtraWidth"
- :fixed-extra="sidebarExpandOnHover"
- :header-height="isMixedNav ? 0 : getHeaderHeight"
- :is-sidebar-mixed="isSidebarMixedNav"
- :mixed-width="sidebarMixedWidth"
- :padding-top="sidePaddingTop"
- :show="showSidebar"
- :theme="sidebarFace.theme"
- :width="getSidebarWidth"
- :z-index="sidebarZIndex"
- @leave="() => emit('sideMouseLeave')"
- >
- <template v-if="isSideMode && !isMixedNav" #logo>
- <slot name="logo"></slot>
- </template>
- <template v-if="isSidebarMixedNav">
- <slot name="mixed-menu"></slot>
- </template>
- <template v-else>
- <slot name="menu"></slot>
- </template>
- <template #extra>
- <slot name="side-extra"></slot>
- </template>
- <template #extra-title>
- <slot name="side-extra-title"></slot>
- </template>
- </LayoutSidebar>
- <div
- class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
- >
- <div
- :style="headerWrapperStyle"
- class="overflow-hidden transition-all duration-200"
- >
- <LayoutHeader
- v-if="headerVisible"
- :full-width="!isSideMode"
- :height="getHeaderHeight"
- :is-mixed-nav="isMixedNav"
- :is-mobile="isMobile"
- :show="!fullContent && !headerHidden"
- :show-toggle-btn="showHeaderToggleButton"
- :sidebar-width="sidebarWidth"
- :width="mainStyle.width"
- :z-index="headerZIndex"
- @open-menu="handleOpenMenu"
- @toggle-sidebar="handleToggleSidebar"
- >
- <template v-if="showHeaderLogo" #logo>
- <slot name="logo"></slot>
- </template>
- <slot name="header"></slot>
- </LayoutHeader>
- <LayoutTabbar
- v-if="tabbarEnable"
- :height="tabbarHeight"
- :style="tabbarStyle"
- >
- <slot name="tabbar"></slot>
- </LayoutTabbar>
- </div>
- <!-- </div> -->
- <LayoutContent
- :content-compact="contentCompact"
- :content-compact-width="contentCompactWidth"
- :padding="contentPadding"
- :padding-bottom="contentPaddingBottom"
- :padding-left="contentPaddingLeft"
- :padding-right="contentPaddingRight"
- :padding-top="contentPaddingTop"
- :style="contentStyle"
- class="transition-[margin-top] duration-200"
- >
- <slot name="content"></slot>
- </LayoutContent>
- <LayoutFooter
- v-if="footerEnable"
- :fixed="footerFixed"
- :height="footerHeight"
- :show="!fullContent"
- :width="footerWidth"
- :z-index="zIndex"
- >
- <slot name="footer"></slot>
- </LayoutFooter>
- </div>
- <slot name="extra"></slot>
- <div
- v-if="maskVisible"
- :style="maskStyle"
- class="fixed left-0 top-0 h-full w-full bg-[rgb(0_0_0_/_40%)] transition-[background-color] duration-200"
- @click="handleClickMask"
- ></div>
- </div>
- </template>
|