mirror of
https://github.com/certd/certd.git
synced 2026-05-18 14:27:36 +08:00
🔱: [client] sync upgrade with 7 commits [trident-sync]
chore: Merge branch 'vben' # Conflicts: # package.json perf: antdv示例改成使用vben框架 chore: vben chore: vben chore: vben
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export { default as LayoutContent } from "./layout-content.vue";
|
||||
export { default as LayoutFooter } from "./layout-footer.vue";
|
||||
export { default as LayoutHeader } from "./layout-header.vue";
|
||||
export { default as LayoutSidebar } from "./layout-sidebar.vue";
|
||||
export { default as LayoutTabbar } from "./layout-tabbar.vue";
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import type { ContentCompactType } from "../../typings";
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
import { useLayoutContentStyle } from "../../composables";
|
||||
import { Slot } from "../../shadcn-ui";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 内容区域定宽
|
||||
*/
|
||||
contentCompact: ContentCompactType;
|
||||
/**
|
||||
* 定宽布局宽度
|
||||
*/
|
||||
contentCompactWidth: number;
|
||||
padding: number;
|
||||
paddingBottom: number;
|
||||
paddingLeft: number;
|
||||
paddingRight: number;
|
||||
paddingTop: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const { contentElement, overlayStyle } = useLayoutContentStyle();
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { contentCompact, padding, paddingBottom, paddingLeft, paddingRight, paddingTop } = props;
|
||||
|
||||
const compactStyle: CSSProperties = contentCompact === "compact" ? { margin: "0 auto", width: `${props.contentCompactWidth}px` } : {};
|
||||
return {
|
||||
...compactStyle,
|
||||
flex: 1,
|
||||
padding: `${padding}px`,
|
||||
paddingBottom: `${paddingBottom}px`,
|
||||
paddingLeft: `${paddingLeft}px`,
|
||||
paddingRight: `${paddingRight}px`,
|
||||
paddingTop: `${paddingTop}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main ref="contentElement" :style="style" class="bg-background-deep relative">
|
||||
<Slot :style="overlayStyle">
|
||||
<slot name="overlay"></slot>
|
||||
</Slot>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 是否固定在底部
|
||||
*/
|
||||
fixed?: boolean;
|
||||
height: number;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
width: string;
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
show: true
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { fixed, height, show, width, zIndex } = props;
|
||||
return {
|
||||
height: `${height}px`,
|
||||
marginBottom: show ? "0" : `-${height}px`,
|
||||
position: fixed ? "fixed" : "static",
|
||||
width,
|
||||
zIndex
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer :style="style" class="bg-background-deep bottom-0 w-full transition-all duration-200">
|
||||
<slot></slot>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import { computed, useSlots } from "vue";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 横屏
|
||||
*/
|
||||
fullWidth: boolean;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* 是否移动端
|
||||
*/
|
||||
isMobile: boolean;
|
||||
/**
|
||||
* 是否显示
|
||||
*/
|
||||
show: boolean;
|
||||
/**
|
||||
* 侧边菜单宽度
|
||||
*/
|
||||
sidebarWidth: number;
|
||||
/**
|
||||
* 主题
|
||||
*/
|
||||
theme: string | undefined;
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
width: string;
|
||||
/**
|
||||
* zIndex
|
||||
*/
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { fullWidth, height, show } = props;
|
||||
const right = !show || !fullWidth ? undefined : 0;
|
||||
|
||||
return {
|
||||
height: `${height}px`,
|
||||
marginTop: show ? 0 : `-${height}px`,
|
||||
right
|
||||
};
|
||||
});
|
||||
|
||||
const logoStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header :class="theme" :style="style" class="border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200">
|
||||
<div v-if="slots.logo" :style="logoStyle">
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
|
||||
<slot name="toggle-button"> </slot>
|
||||
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,291 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import { computed, shallowRef, useSlots, watchEffect } from "vue";
|
||||
|
||||
import { VbenScrollbar } from "../../shadcn-ui";
|
||||
|
||||
import { useScrollLock } from "@vueuse/core";
|
||||
|
||||
import { SidebarCollapseButton, SidebarFixedButton } from "./widgets";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 折叠区域高度
|
||||
* @default 42
|
||||
*/
|
||||
collapseHeight?: number;
|
||||
/**
|
||||
* 折叠宽度
|
||||
* @default 48
|
||||
*/
|
||||
collapseWidth?: number;
|
||||
/**
|
||||
* 隐藏的dom是否可见
|
||||
* @default true
|
||||
*/
|
||||
domVisible?: boolean;
|
||||
/**
|
||||
* 扩展区域宽度
|
||||
*/
|
||||
extraWidth: number;
|
||||
/**
|
||||
* 固定扩展区域
|
||||
* @default false
|
||||
*/
|
||||
fixedExtra?: boolean;
|
||||
/**
|
||||
* 头部高度
|
||||
*/
|
||||
headerHeight: number;
|
||||
/**
|
||||
* 是否侧边混合模式
|
||||
* @default false
|
||||
*/
|
||||
isSidebarMixed?: boolean;
|
||||
/**
|
||||
* 顶部margin
|
||||
* @default 60
|
||||
*/
|
||||
marginTop?: number;
|
||||
/**
|
||||
* 混合菜单宽度
|
||||
* @default 80
|
||||
*/
|
||||
mixedWidth?: number;
|
||||
/**
|
||||
* 顶部padding
|
||||
* @default 60
|
||||
*/
|
||||
paddingTop?: number;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 显示折叠按钮
|
||||
* @default false
|
||||
*/
|
||||
showCollapseButton?: boolean;
|
||||
/**
|
||||
* 主题
|
||||
*/
|
||||
theme: string;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapseHeight: 42,
|
||||
collapseWidth: 48,
|
||||
domVisible: true,
|
||||
fixedExtra: false,
|
||||
isSidebarMixed: false,
|
||||
marginTop: 0,
|
||||
mixedWidth: 70,
|
||||
paddingTop: 0,
|
||||
show: true,
|
||||
showCollapseButton: true,
|
||||
zIndex: 0
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ leave: [] }>();
|
||||
const collapse = defineModel<boolean>("collapse");
|
||||
const extraCollapse = defineModel<boolean>("extraCollapse");
|
||||
const expandOnHovering = defineModel<boolean>("expandOnHovering");
|
||||
const expandOnHover = defineModel<boolean>("expandOnHover");
|
||||
const extraVisible = defineModel<boolean>("extraVisible");
|
||||
|
||||
const isLocked = useScrollLock(document.body);
|
||||
const slots = useSlots();
|
||||
|
||||
const asideRef = shallowRef<HTMLDivElement | null>();
|
||||
|
||||
const hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { isSidebarMixed, marginTop, paddingTop, zIndex } = props;
|
||||
|
||||
return {
|
||||
"--scroll-shadow": "var(--sidebar)",
|
||||
...calcMenuWidthStyle(false),
|
||||
height: `calc(100% - ${marginTop}px)`,
|
||||
marginTop: `${marginTop}px`,
|
||||
paddingTop: `${paddingTop}px`,
|
||||
zIndex,
|
||||
...(isSidebarMixed && extraVisible.value ? { transition: "none" } : {})
|
||||
};
|
||||
});
|
||||
|
||||
const extraStyle = computed((): CSSProperties => {
|
||||
const { extraWidth, show, width, zIndex } = props;
|
||||
|
||||
return {
|
||||
left: `${width}px`,
|
||||
width: extraVisible.value && show ? `${extraWidth}px` : 0,
|
||||
zIndex
|
||||
};
|
||||
});
|
||||
|
||||
const extraTitleStyle = computed((): CSSProperties => {
|
||||
const { headerHeight } = props;
|
||||
|
||||
return {
|
||||
height: `${headerHeight - 1}px`
|
||||
};
|
||||
});
|
||||
|
||||
const contentWidthStyle = computed((): CSSProperties => {
|
||||
const { collapseWidth, fixedExtra, isSidebarMixed, mixedWidth } = props;
|
||||
if (isSidebarMixed && fixedExtra) {
|
||||
return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const contentStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight, headerHeight } = props;
|
||||
|
||||
return {
|
||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
||||
paddingTop: "8px",
|
||||
...contentWidthStyle.value
|
||||
};
|
||||
});
|
||||
|
||||
const headerStyle = computed((): CSSProperties => {
|
||||
const { headerHeight, isSidebarMixed } = props;
|
||||
|
||||
return {
|
||||
...(isSidebarMixed ? { display: "flex", justifyContent: "center" } : {}),
|
||||
height: `${headerHeight - 1}px`,
|
||||
...contentWidthStyle.value
|
||||
};
|
||||
});
|
||||
|
||||
const extraContentStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight, headerHeight } = props;
|
||||
return {
|
||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`
|
||||
};
|
||||
});
|
||||
|
||||
const collapseStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: `${props.collapseHeight}px`
|
||||
};
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
extraVisible.value = props.fixedExtra ? true : extraVisible.value;
|
||||
});
|
||||
|
||||
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
|
||||
const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;
|
||||
|
||||
let widthValue = width === 0 ? "0px" : `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
||||
|
||||
const { collapseWidth } = props;
|
||||
|
||||
if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
|
||||
widthValue = `${collapseWidth}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
...(widthValue === "0px" ? { overflow: "hidden" } : {}),
|
||||
flex: `0 0 ${widthValue}`,
|
||||
marginLeft: show ? 0 : `-${widthValue}`,
|
||||
maxWidth: widthValue,
|
||||
minWidth: widthValue,
|
||||
width: widthValue
|
||||
};
|
||||
}
|
||||
|
||||
function handleMouseenter(e: MouseEvent) {
|
||||
if (e?.offsetX < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 未开启和未折叠状态不生效
|
||||
if (expandOnHover.value) {
|
||||
return;
|
||||
}
|
||||
if (!expandOnHovering.value) {
|
||||
collapse.value = false;
|
||||
}
|
||||
if (props.isSidebarMixed) {
|
||||
isLocked.value = true;
|
||||
}
|
||||
expandOnHovering.value = true;
|
||||
}
|
||||
|
||||
function handleMouseleave() {
|
||||
emit("leave");
|
||||
if (props.isSidebarMixed) {
|
||||
isLocked.value = false;
|
||||
}
|
||||
if (expandOnHover.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
expandOnHovering.value = false;
|
||||
collapse.value = true;
|
||||
extraVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="domVisible" :class="theme" :style="hiddenSideStyle" class="h-full transition-all duration-150"></div>
|
||||
<aside
|
||||
:class="[
|
||||
theme,
|
||||
{
|
||||
'bg-sidebar-deep': isSidebarMixed,
|
||||
'bg-sidebar border-border border-r': !isSidebarMixed
|
||||
}
|
||||
]"
|
||||
:style="style"
|
||||
class="fixed left-0 top-0 h-full transition-all duration-150"
|
||||
@mouseenter="handleMouseenter"
|
||||
@mouseleave="handleMouseleave"
|
||||
>
|
||||
<SidebarFixedButton v-if="!collapse && !isSidebarMixed" v-model:expand-on-hover="expandOnHover" />
|
||||
<div v-if="slots.logo" :style="headerStyle">
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
<VbenScrollbar :style="contentStyle" shadow shadow-border>
|
||||
<slot></slot>
|
||||
</VbenScrollbar>
|
||||
|
||||
<div :style="collapseStyle"></div>
|
||||
<SidebarCollapseButton v-if="showCollapseButton && !isSidebarMixed" v-model:collapsed="collapse" />
|
||||
<div
|
||||
v-if="isSidebarMixed"
|
||||
ref="asideRef"
|
||||
:class="{
|
||||
'border-l': extraVisible
|
||||
}"
|
||||
:style="extraStyle"
|
||||
class="border-border bg-sidebar fixed top-0 h-full overflow-hidden border-r transition-all duration-200"
|
||||
>
|
||||
<SidebarCollapseButton v-if="isSidebarMixed && expandOnHover" v-model:collapsed="extraCollapse" />
|
||||
|
||||
<SidebarFixedButton v-if="!extraCollapse" v-model:expand-on-hover="expandOnHover" />
|
||||
<div v-if="!extraCollapse" :style="extraTitleStyle" class="pl-2">
|
||||
<slot name="extra-title"></slot>
|
||||
</div>
|
||||
<VbenScrollbar :style="extraContentStyle" class="border-border py-2" shadow shadow-border>
|
||||
<slot name="extra"></slot>
|
||||
</VbenScrollbar>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { height } = props;
|
||||
return {
|
||||
height: `${height}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :style="style" class="border-border bg-background flex w-full border-b transition-all">
|
||||
<slot></slot>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as SidebarCollapseButton } from "./sidebar-collapse-button.vue";
|
||||
export { default as SidebarFixedButton } from "./sidebar-fixed-button.vue";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronsLeft, ChevronsRight } from "../../../icons";
|
||||
|
||||
const collapsed = defineModel<boolean>("collapsed");
|
||||
|
||||
function handleCollapsed() {
|
||||
collapsed.value = !collapsed.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 left-3 z-10 cursor-pointer rounded-sm p-1" @click.stop="handleCollapsed">
|
||||
<ChevronsRight v-if="collapsed" class="size-4" />
|
||||
<ChevronsLeft v-else class="size-4" />
|
||||
</div>
|
||||
</template>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Pin, PinOff } from "../../../icons";
|
||||
|
||||
const expandOnHover = defineModel<boolean>("expandOnHover");
|
||||
|
||||
function toggleFixed() {
|
||||
expandOnHover.value = !expandOnHover.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 right-3 z-10 cursor-pointer rounded-sm p-[5px] transition-all duration-300" @click="toggleFixed">
|
||||
<PinOff v-if="!expandOnHover" class="size-3.5" />
|
||||
<Pin v-else class="size-3.5" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { LayoutType } from "/@/vben/typings";
|
||||
|
||||
import type { VbenLayoutProps } from "../vben-layout";
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
export function useLayout(props: VbenLayoutProps) {
|
||||
const currentLayout = computed(() => (props.isMobile ? "sidebar-nav" : (props.layout as LayoutType)));
|
||||
|
||||
/**
|
||||
* 是否全屏显示content,不需要侧边、底部、顶部、tab区域
|
||||
*/
|
||||
const isFullContent = computed(() => currentLayout.value === "full-content");
|
||||
|
||||
/**
|
||||
* 是否侧边混合模式
|
||||
*/
|
||||
const isSidebarMixedNav = computed(() => currentLayout.value === "sidebar-mixed-nav");
|
||||
|
||||
/**
|
||||
* 是否为头部导航模式
|
||||
*/
|
||||
const isHeaderNav = computed(() => currentLayout.value === "header-nav");
|
||||
|
||||
/**
|
||||
* 是否为混合导航模式
|
||||
*/
|
||||
const isMixedNav = computed(() => currentLayout.value === "mixed-nav" || currentLayout.value === "header-sidebar-nav");
|
||||
|
||||
/**
|
||||
* 是否为头部混合模式
|
||||
*/
|
||||
const isHeaderMixedNav = computed(() => currentLayout.value === "header-mixed-nav");
|
||||
|
||||
return {
|
||||
currentLayout,
|
||||
isFullContent,
|
||||
isHeaderMixedNav,
|
||||
isHeaderNav,
|
||||
isMixedNav,
|
||||
isSidebarMixedNav
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type * from './vben-layout';
|
||||
export { default as VbenAdminLayout } from './vben-layout.vue';
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { ContentCompactType, LayoutHeaderModeType, LayoutType, ThemeModeType } from "/@/vben/typings";
|
||||
|
||||
interface VbenLayoutProps {
|
||||
/**
|
||||
* 内容区域定宽
|
||||
* @default 'wide'
|
||||
*/
|
||||
contentCompact?: ContentCompactType;
|
||||
/**
|
||||
* 定宽布局宽度
|
||||
* @default 1200
|
||||
*/
|
||||
contentCompactWidth?: number;
|
||||
/**
|
||||
* padding
|
||||
* @default 16
|
||||
*/
|
||||
contentPadding?: number;
|
||||
/**
|
||||
* paddingBottom
|
||||
* @default 16
|
||||
*/
|
||||
contentPaddingBottom?: number;
|
||||
/**
|
||||
* paddingLeft
|
||||
* @default 16
|
||||
*/
|
||||
contentPaddingLeft?: number;
|
||||
/**
|
||||
* paddingRight
|
||||
* @default 16
|
||||
*/
|
||||
contentPaddingRight?: number;
|
||||
/**
|
||||
* paddingTop
|
||||
* @default 16
|
||||
*/
|
||||
contentPaddingTop?: number;
|
||||
/**
|
||||
* footer 是否可见
|
||||
* @default false
|
||||
*/
|
||||
footerEnable?: boolean;
|
||||
/**
|
||||
* footer 是否固定
|
||||
* @default true
|
||||
*/
|
||||
footerFixed?: boolean;
|
||||
/**
|
||||
* footer 高度
|
||||
* @default 32
|
||||
*/
|
||||
footerHeight?: number;
|
||||
|
||||
/**
|
||||
* header高度
|
||||
* @default 48
|
||||
*/
|
||||
headerHeight?: number;
|
||||
/**
|
||||
* 顶栏是否隐藏
|
||||
* @default false
|
||||
*/
|
||||
headerHidden?: boolean;
|
||||
/**
|
||||
* header 显示模式
|
||||
* @default 'fixed'
|
||||
*/
|
||||
headerMode?: LayoutHeaderModeType;
|
||||
/**
|
||||
* header 顶栏主题
|
||||
*/
|
||||
headerTheme?: ThemeModeType;
|
||||
/**
|
||||
* 是否显示header切换侧边栏按钮
|
||||
* @default
|
||||
*/
|
||||
headerToggleSidebarButton?: boolean;
|
||||
/**
|
||||
* header是否显示
|
||||
* @default true
|
||||
*/
|
||||
headerVisible?: boolean;
|
||||
/**
|
||||
* 是否移动端显示
|
||||
* @default false
|
||||
*/
|
||||
isMobile?: boolean;
|
||||
/**
|
||||
* 布局方式
|
||||
* sidebar-nav 侧边菜单布局
|
||||
* header-nav 顶部菜单布局
|
||||
* mixed-nav 侧边&顶部菜单布局
|
||||
* sidebar-mixed-nav 侧边混合菜单布局
|
||||
* full-content 全屏内容布局
|
||||
* @default sidebar-nav
|
||||
*/
|
||||
layout?: LayoutType;
|
||||
/**
|
||||
* 侧边菜单折叠状态
|
||||
* @default false
|
||||
*/
|
||||
sidebarCollapse?: boolean;
|
||||
/**
|
||||
* 侧边菜单是否折叠时,是否显示title
|
||||
* @default true
|
||||
*/
|
||||
sidebarCollapseShowTitle?: boolean;
|
||||
/**
|
||||
* 侧边栏是否可见
|
||||
* @default true
|
||||
*/
|
||||
sidebarEnable?: boolean;
|
||||
/**
|
||||
* 侧边菜单折叠额外宽度
|
||||
* @default 48
|
||||
*/
|
||||
sidebarExtraCollapsedWidth?: number;
|
||||
/**
|
||||
* 侧边栏是否隐藏
|
||||
* @default false
|
||||
*/
|
||||
sidebarHidden?: boolean;
|
||||
/**
|
||||
* 混合侧边栏宽度
|
||||
* @default 80
|
||||
*/
|
||||
sidebarMixedWidth?: number;
|
||||
/**
|
||||
* 侧边栏
|
||||
* @default dark
|
||||
*/
|
||||
sidebarTheme?: ThemeModeType;
|
||||
/**
|
||||
* 侧边栏宽度
|
||||
* @default 210
|
||||
*/
|
||||
sidebarWidth?: number;
|
||||
/**
|
||||
* 侧边菜单折叠宽度
|
||||
* @default 48
|
||||
*/
|
||||
sideCollapseWidth?: number;
|
||||
/**
|
||||
* tab是否可见
|
||||
* @default true
|
||||
*/
|
||||
tabbarEnable?: boolean;
|
||||
/**
|
||||
* tab高度
|
||||
* @default 30
|
||||
*/
|
||||
tabbarHeight?: number;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 100
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
export type { VbenLayoutProps };
|
||||
@@ -0,0 +1,498 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import type { VbenLayoutProps } from "./vben-layout";
|
||||
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import { SCROLL_FIXED_CLASS, useLayoutFooterStyle, useLayoutHeaderStyle } from "../composables";
|
||||
import { Menu } from "../icons";
|
||||
import { VbenIconButton } from "../shadcn-ui";
|
||||
import { ELEMENT_ID_MAIN_CONTENT } from "../shared/constants";
|
||||
|
||||
import { useMouse, useScroll, useThrottleFn } from "@vueuse/core";
|
||||
|
||||
import { LayoutContent, LayoutFooter, LayoutHeader, LayoutSidebar, LayoutTabbar } from "./components";
|
||||
import { useLayout } from "./hooks/use-layout";
|
||||
|
||||
interface Props extends VbenLayoutProps {}
|
||||
|
||||
defineOptions({
|
||||
name: "VbenLayout"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentCompact: "wide",
|
||||
contentCompactWidth: 1200,
|
||||
contentPadding: 0,
|
||||
contentPaddingBottom: 0,
|
||||
contentPaddingLeft: 0,
|
||||
contentPaddingRight: 0,
|
||||
contentPaddingTop: 0,
|
||||
footerEnable: false,
|
||||
footerFixed: true,
|
||||
footerHeight: 32,
|
||||
headerHeight: 50,
|
||||
headerHidden: false,
|
||||
headerMode: "fixed",
|
||||
headerToggleSidebarButton: true,
|
||||
headerVisible: true,
|
||||
isMobile: false,
|
||||
layout: "sidebar-nav",
|
||||
sidebarCollapseShowTitle: false,
|
||||
sidebarExtraCollapsedWidth: 60,
|
||||
sidebarHidden: false,
|
||||
sidebarMixedWidth: 80,
|
||||
sidebarTheme: "dark",
|
||||
sidebarWidth: 180,
|
||||
sideCollapseWidth: 60,
|
||||
tabbarEnable: true,
|
||||
tabbarHeight: 40,
|
||||
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 });
|
||||
|
||||
// side是否处于hover状态展开菜单中
|
||||
const sidebarExpandOnHovering = ref(false);
|
||||
const headerIsHidden = ref(false);
|
||||
const contentRef = ref();
|
||||
|
||||
const { arrivedState, directions, isScrolling, y: scrollY } = useScroll(document);
|
||||
|
||||
const { setLayoutHeaderHeight } = useLayoutHeaderStyle();
|
||||
const { setLayoutFooterHeight } = useLayoutFooterStyle();
|
||||
|
||||
const { y: mouseY } = useMouse({ target: contentRef, type: "client" });
|
||||
|
||||
const { currentLayout, isFullContent, isHeaderMixedNav, isHeaderNav, isMixedNav, isSidebarMixedNav } = useLayout(props);
|
||||
|
||||
/**
|
||||
* 顶栏是否自动隐藏
|
||||
*/
|
||||
const isHeaderAutoMode = computed(() => props.headerMode === "auto");
|
||||
|
||||
const headerWrapperHeight = computed(() => {
|
||||
let height = 0;
|
||||
if (props.headerVisible && !props.headerHidden) {
|
||||
height += props.headerHeight;
|
||||
}
|
||||
if (props.tabbarEnable) {
|
||||
height += props.tabbarHeight;
|
||||
}
|
||||
return height;
|
||||
});
|
||||
|
||||
const getSideCollapseWidth = computed(() => {
|
||||
const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } = props;
|
||||
|
||||
return sidebarCollapseShowTitle || isSidebarMixedNav.value || isHeaderMixedNav.value ? sidebarMixedWidth : sideCollapseWidth;
|
||||
});
|
||||
|
||||
/**
|
||||
* 动态获取侧边区域是否可见
|
||||
*/
|
||||
const sidebarEnableState = computed(() => {
|
||||
return !isHeaderNav.value && sidebarEnable.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 侧边区域离顶部高度
|
||||
*/
|
||||
const sidebarMarginTop = computed(() => {
|
||||
const { headerHeight, isMobile } = props;
|
||||
return isMixedNav.value && !isMobile ? headerHeight : 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 && !isHeaderMixedNav.value)) {
|
||||
return width;
|
||||
}
|
||||
|
||||
if ((isHeaderMixedNav.value || isSidebarMixedNav.value) && !isMobile) {
|
||||
width = sidebarMixedWidth;
|
||||
} else if (sidebarCollapse.value) {
|
||||
width = isMobile ? 0 : getSideCollapseWidth.value;
|
||||
} else {
|
||||
width = sidebarWidth;
|
||||
}
|
||||
return width;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取扩展区域宽度
|
||||
*/
|
||||
const sidebarExtraWidth = computed(() => {
|
||||
const { sidebarExtraCollapsedWidth, sidebarWidth } = props;
|
||||
|
||||
return sidebarExtraCollapse.value ? sidebarExtraCollapsedWidth : sidebarWidth;
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否侧边栏模式,包含混合侧边
|
||||
*/
|
||||
const isSideMode = computed(
|
||||
() =>
|
||||
currentLayout.value === "mixed-nav" ||
|
||||
currentLayout.value === "sidebar-mixed-nav" ||
|
||||
currentLayout.value === "sidebar-nav" ||
|
||||
currentLayout.value === "header-mixed-nav" ||
|
||||
currentLayout.value === "header-sidebar-nav"
|
||||
);
|
||||
|
||||
/**
|
||||
* header fixed值
|
||||
*/
|
||||
const headerFixed = computed(() => {
|
||||
const { headerMode } = props;
|
||||
return isMixedNav.value || headerMode === "fixed" || headerMode === "auto-scroll" || headerMode === "auto";
|
||||
});
|
||||
|
||||
const showSidebar = computed(() => {
|
||||
return isSideMode.value && sidebarEnable.value && !props.sidebarHidden;
|
||||
});
|
||||
|
||||
/**
|
||||
* 遮罩可见性
|
||||
*/
|
||||
const maskVisible = computed(() => !sidebarCollapse.value && props.isMobile);
|
||||
|
||||
const mainStyle = computed(() => {
|
||||
let width = "100%";
|
||||
let sidebarAndExtraWidth = "unset";
|
||||
if (headerFixed.value && currentLayout.value !== "header-nav" && currentLayout.value !== "mixed-nav" && currentLayout.value !== "header-sidebar-nav" && showSidebar.value && !props.isMobile) {
|
||||
// fixed模式下生效
|
||||
const isSideNavEffective = (isSidebarMixedNav.value || isHeaderMixedNav.value) && sidebarExpandOnHover.value && sidebarExtraVisible.value;
|
||||
|
||||
if (isSideNavEffective) {
|
||||
const sideCollapseWidth = sidebarCollapse.value ? getSideCollapseWidth.value : props.sidebarMixedWidth;
|
||||
const sideWidth = sidebarExtraCollapse.value ? props.sidebarExtraCollapsedWidth : 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
|
||||
};
|
||||
});
|
||||
|
||||
// 计算 tabbar 的样式
|
||||
const tabbarStyle = computed((): CSSProperties => {
|
||||
let width = "";
|
||||
let marginLeft = 0;
|
||||
|
||||
// 如果不是混合导航,tabbar 的宽度为 100%
|
||||
if (!isMixedNav.value || props.sidebarHidden) {
|
||||
width = "100%";
|
||||
} else if (sidebarEnable.value) {
|
||||
// 鼠标在侧边栏上时,且侧边栏展开时的宽度
|
||||
const onHoveringWidth = sidebarExpandOnHover.value ? props.sidebarWidth : getSideCollapseWidth.value;
|
||||
|
||||
// 设置 marginLeft,根据侧边栏是否折叠来决定
|
||||
marginLeft = sidebarCollapse.value ? getSideCollapseWidth.value : onHoveringWidth;
|
||||
|
||||
// 设置 tabbar 的宽度,计算方式为 100% 减去侧边栏的宽度
|
||||
width = `calc(100% - ${sidebarCollapse.value ? getSidebarWidth.value : onHoveringWidth}px)`;
|
||||
} else {
|
||||
// 默认情况下,tabbar 的宽度为 100%
|
||||
width = "100%";
|
||||
}
|
||||
|
||||
return {
|
||||
marginLeft: `${marginLeft}px`,
|
||||
width
|
||||
};
|
||||
});
|
||||
|
||||
const contentStyle = computed((): CSSProperties => {
|
||||
const fixed = headerFixed.value;
|
||||
|
||||
const { footerEnable, footerFixed, footerHeight } = props;
|
||||
return {
|
||||
marginTop: fixed && !isFullContent.value && !headerIsHidden.value && (!isHeaderAutoMode.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: isFullContent.value ? "0" : `${headerWrapperHeight.value}px`,
|
||||
left: isMixedNav.value ? 0 : mainStyle.value.sidebarAndExtraWidth,
|
||||
position: fixed ? "fixed" : "static",
|
||||
top: headerIsHidden.value || isFullContent.value ? `-${headerWrapperHeight.value}px` : 0,
|
||||
width: mainStyle.value.width,
|
||||
"z-index": headerZIndex.value
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 侧边栏z-index
|
||||
*/
|
||||
const sidebarZIndex = computed(() => {
|
||||
const { isMobile, zIndex } = props;
|
||||
let offset = isMobile || isSideMode.value ? 1 : -1;
|
||||
|
||||
if (isMixedNav.value) {
|
||||
offset += 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.isMobile || (props.headerToggleSidebarButton && isSideMode.value && !isSidebarMixedNav.value && !isMixedNav.value && !props.isMobile);
|
||||
});
|
||||
|
||||
const showHeaderLogo = computed(() => {
|
||||
return !isSideMode.value || isMixedNav.value || props.isMobile;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isMobile,
|
||||
(val) => {
|
||||
if (val) {
|
||||
sidebarCollapse.value = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
[() => headerWrapperHeight.value, () => isFullContent.value],
|
||||
([height]) => {
|
||||
setLayoutHeaderHeight(isFullContent.value ? 0 : height);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.footerHeight,
|
||||
(height: number) => {
|
||||
setLayoutFooterHeight(height);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
{
|
||||
const mouseMove = () => {
|
||||
mouseY.value > headerWrapperHeight.value ? (headerIsHidden.value = true) : (headerIsHidden.value = false);
|
||||
};
|
||||
watch(
|
||||
[() => props.headerMode, () => mouseY.value],
|
||||
() => {
|
||||
if (!isHeaderAutoMode.value || isMixedNav.value || isFullContent.value) {
|
||||
if (props.headerMode !== "auto-scroll") {
|
||||
headerIsHidden.value = false;
|
||||
}
|
||||
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 || isFullContent.value) {
|
||||
return;
|
||||
}
|
||||
if (isScrolling.value) {
|
||||
checkHeaderIsHidden(directions.top, directions.bottom, arrivedState.top);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleClickMask() {
|
||||
sidebarCollapse.value = true;
|
||||
}
|
||||
|
||||
function handleHeaderToggle() {
|
||||
if (props.isMobile) {
|
||||
sidebarCollapse.value = false;
|
||||
} else {
|
||||
emit("toggleSidebar");
|
||||
}
|
||||
}
|
||||
|
||||
const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex min-h-full w-full">
|
||||
<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="sidebarExtraWidth"
|
||||
:fixed-extra="sidebarExpandOnHover"
|
||||
:header-height="isMixedNav ? 0 : headerHeight"
|
||||
:is-sidebar-mixed="isSidebarMixedNav || isHeaderMixedNav"
|
||||
:margin-top="sidebarMarginTop"
|
||||
:mixed-width="sidebarMixedWidth"
|
||||
:show="showSidebar"
|
||||
:theme="sidebarTheme"
|
||||
:width="getSidebarWidth"
|
||||
:z-index="sidebarZIndex"
|
||||
@leave="() => emit('sideMouseLeave')"
|
||||
>
|
||||
<template v-if="isSideMode && !isMixedNav" #logo>
|
||||
<slot name="logo"></slot>
|
||||
</template>
|
||||
|
||||
<template v-if="isSidebarMixedNav || isHeaderMixedNav">
|
||||
<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 ref="contentRef" class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in">
|
||||
<div
|
||||
:class="[
|
||||
{
|
||||
'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20
|
||||
},
|
||||
SCROLL_FIXED_CLASS
|
||||
]"
|
||||
:style="headerWrapperStyle"
|
||||
class="overflow-hidden transition-all duration-200"
|
||||
>
|
||||
<LayoutHeader
|
||||
v-if="headerVisible"
|
||||
:full-width="!isSideMode"
|
||||
:height="headerHeight"
|
||||
:is-mobile="isMobile"
|
||||
:show="!isFullContent && !headerHidden"
|
||||
:sidebar-width="sidebarWidth"
|
||||
:theme="headerTheme"
|
||||
:width="mainStyle.width"
|
||||
:z-index="headerZIndex"
|
||||
>
|
||||
<template v-if="showHeaderLogo" #logo>
|
||||
<slot name="logo"></slot>
|
||||
</template>
|
||||
|
||||
<template #toggle-button>
|
||||
<VbenIconButton v-if="showHeaderToggleButton" class="my-0 mr-1 rounded-md" @click="handleHeaderToggle">
|
||||
<Menu class="size-4" />
|
||||
</VbenIconButton>
|
||||
</template>
|
||||
<slot name="header"></slot>
|
||||
</LayoutHeader>
|
||||
|
||||
<LayoutTabbar v-if="tabbarEnable" :height="tabbarHeight" :style="tabbarStyle">
|
||||
<slot name="tabbar"></slot>
|
||||
</LayoutTabbar>
|
||||
</div>
|
||||
|
||||
<!-- </div> -->
|
||||
<LayoutContent
|
||||
:id="idMainContent"
|
||||
: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>
|
||||
|
||||
<template #overlay>
|
||||
<slot name="content-overlay"></slot>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<LayoutFooter v-if="footerEnable" :fixed="footerFixed" :height="footerHeight" :show="!isFullContent" :width="footerWidth" :z-index="zIndex">
|
||||
<slot name="footer"></slot>
|
||||
</LayoutFooter>
|
||||
</div>
|
||||
<slot name="extra"></slot>
|
||||
<div v-if="maskVisible" :style="maskStyle" class="bg-overlay fixed left-0 top-0 h-full w-full transition-[background-color] duration-200" @click="handleClickMask"></div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user