chore: update uikit -> ui-kit
This commit is contained in:
46
packages/@core/ui-kit/layout-ui/package.json
Normal file
46
packages/@core/ui-kit/layout-ui/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@vben-core/layout-ui",
|
||||
"version": "5.0.0",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/uikit/layout-ui"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"vue": "^3.4.30"
|
||||
}
|
||||
}
|
1
packages/@core/ui-kit/layout-ui/postcss.config.mjs
Normal file
1
packages/@core/ui-kit/layout-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
5
packages/@core/ui-kit/layout-ui/src/components/index.ts
Normal file
5
packages/@core/ui-kit/layout-ui/src/components/index.ts
Normal file
@@ -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,103 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentCompactType } from '@vben-core/typings';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useCssVar, useDebounceFn, useWindowSize } from '@vueuse/core';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 内容区域定宽
|
||||
* @default 'wide'
|
||||
*/
|
||||
contentCompact?: ContentCompactType;
|
||||
/**
|
||||
* 定宽布局宽度
|
||||
* @default 1200
|
||||
*/
|
||||
contentCompactWidth?: number;
|
||||
/**
|
||||
* padding
|
||||
* @default 16
|
||||
*/
|
||||
padding?: number;
|
||||
/**
|
||||
* paddingBottom
|
||||
* @default 16
|
||||
*/
|
||||
paddingBottom?: number;
|
||||
/**
|
||||
* paddingLeft
|
||||
* @default 16
|
||||
*/
|
||||
paddingLeft?: number;
|
||||
/**
|
||||
* paddingRight
|
||||
* @default 16
|
||||
*/
|
||||
paddingRight?: number;
|
||||
/**
|
||||
* paddingTop
|
||||
* @default 16
|
||||
*/
|
||||
paddingTop?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
padding: 16,
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 16,
|
||||
});
|
||||
|
||||
const domElement = ref<HTMLDivElement | null>();
|
||||
|
||||
const { height, width } = useWindowSize();
|
||||
const contentClientHeight = useCssVar('--vben-content-client-height');
|
||||
const debouncedCalcHeight = useDebounceFn(() => {
|
||||
contentClientHeight.value = `${domElement.value?.clientHeight ?? window.innerHeight}px`;
|
||||
}, 200);
|
||||
|
||||
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`,
|
||||
};
|
||||
});
|
||||
|
||||
watch([height, width], () => {
|
||||
debouncedCalcHeight();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
debouncedCalcHeight();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main ref="domElement" :style="style">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* 是否固定在顶部
|
||||
* @default true
|
||||
*/
|
||||
fixed?: boolean;
|
||||
/**
|
||||
* 高度
|
||||
* @default 32
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 高度
|
||||
* @default 100%
|
||||
*/
|
||||
width?: string;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--background))',
|
||||
fixed: true,
|
||||
height: 32,
|
||||
show: true,
|
||||
width: '100%',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor, fixed, height, show, width, zIndex } = props;
|
||||
return {
|
||||
backgroundColor,
|
||||
height: `${height}px`,
|
||||
marginBottom: show ? '0' : `-${height}px`,
|
||||
position: fixed ? 'fixed' : 'static',
|
||||
width,
|
||||
zIndex,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer :style="style" class="bottom-0 w-full transition-all duration-200">
|
||||
<slot></slot>
|
||||
</footer>
|
||||
</template>
|
123
packages/@core/ui-kit/layout-ui/src/components/layout-header.vue
Normal file
123
packages/@core/ui-kit/layout-ui/src/components/layout-header.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed, useSlots } from 'vue';
|
||||
|
||||
import { IcRoundMenu } from '@vben-core/iconify';
|
||||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
|
||||
/**
|
||||
* 横屏
|
||||
* @default false
|
||||
*/
|
||||
fullWidth?: boolean;
|
||||
/**
|
||||
* 高度
|
||||
* @default 60
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* 是否混合导航
|
||||
* @default false
|
||||
*/
|
||||
isMixedNav?: boolean;
|
||||
/**
|
||||
* 是否移动端
|
||||
* @default false
|
||||
*/
|
||||
isMobile?: boolean;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 是否显示关闭菜单按钮
|
||||
* @default true
|
||||
*/
|
||||
showToggleBtn?: boolean;
|
||||
|
||||
/**
|
||||
* 侧边菜单宽度
|
||||
* @default 0
|
||||
*/
|
||||
sidebarWidth?: number;
|
||||
/**
|
||||
* 宽度
|
||||
* @default 100%
|
||||
*/
|
||||
width?: string;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--background))',
|
||||
// fixed: true,
|
||||
height: 60,
|
||||
isMixedNav: false,
|
||||
show: true,
|
||||
showToggleBtn: false,
|
||||
sidebarWidth: 0,
|
||||
width: '100%',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ openMenu: []; toggleSidebar: [] }>();
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor, fullWidth, height, show } = props;
|
||||
const right = !show || !fullWidth ? undefined : 0;
|
||||
|
||||
return {
|
||||
// ...(props.isMixedNav ? { left: 0, position: `fixed` } : {}),
|
||||
backgroundColor,
|
||||
height: `${height}px`,
|
||||
marginTop: show ? 0 : `-${height}px`,
|
||||
right,
|
||||
};
|
||||
});
|
||||
|
||||
const logoStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
|
||||
};
|
||||
});
|
||||
|
||||
function handleToggleMenu() {
|
||||
if (props.isMobile) {
|
||||
emit('openMenu');
|
||||
} else {
|
||||
emit('toggleSidebar');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
:style="style"
|
||||
class="border-border top-0 flex w-full flex-[0_0_auto] items-center border-b transition-[margin-top] duration-200"
|
||||
>
|
||||
<div v-if="slots.logo" :style="logoStyle">
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
<VbenIconButton
|
||||
v-if="showToggleBtn || isMobile"
|
||||
class="my-0 ml-2 mr-1 rounded"
|
||||
@click="handleToggleMenu"
|
||||
>
|
||||
<IcRoundMenu class="size-5" />
|
||||
</VbenIconButton>
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
|
||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor: string;
|
||||
/**
|
||||
* 折叠区域高度
|
||||
* @default 32
|
||||
*/
|
||||
collapseHeight?: number;
|
||||
/**
|
||||
* 折叠宽度
|
||||
* @default 48
|
||||
*/
|
||||
collapseWidth?: number;
|
||||
/**
|
||||
* 隐藏的dom是否可见
|
||||
* @default true
|
||||
*/
|
||||
domVisible?: boolean;
|
||||
/**
|
||||
* 扩展区域背景颜色
|
||||
*/
|
||||
extraBackgroundColor: string;
|
||||
/**
|
||||
* 扩展区域宽度
|
||||
* @default 180
|
||||
*/
|
||||
extraWidth?: number;
|
||||
/**
|
||||
* 固定扩展区域
|
||||
* @default false
|
||||
*/
|
||||
fixedExtra?: boolean;
|
||||
/**
|
||||
* 头部高度
|
||||
*/
|
||||
headerHeight: number;
|
||||
/**
|
||||
* 是否侧边混合模式
|
||||
* @default false
|
||||
*/
|
||||
isSidebarMixed?: boolean;
|
||||
/**
|
||||
* 混合菜单宽度
|
||||
* @default 80
|
||||
*/
|
||||
mixedWidth?: number;
|
||||
/**
|
||||
* 顶部padding
|
||||
* @default 60
|
||||
*/
|
||||
paddingTop?: number;
|
||||
/**
|
||||
* 是否显示
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* 显示折叠按钮
|
||||
* @default false
|
||||
*/
|
||||
showCollapseButton?: boolean;
|
||||
/**
|
||||
* 主题
|
||||
*/
|
||||
theme?: string;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
* @default 180
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 0
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapseHeight: 42,
|
||||
collapseWidth: 48,
|
||||
domVisible: true,
|
||||
extraWidth: 180,
|
||||
fixedExtra: false,
|
||||
isSideMixed: false,
|
||||
mixedWidth: 80,
|
||||
paddingTop: 60,
|
||||
show: true,
|
||||
showCollapseButton: true,
|
||||
theme: 'dark',
|
||||
width: 180,
|
||||
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 slots = useSlots();
|
||||
|
||||
const asideRef = shallowRef<HTMLDivElement | null>();
|
||||
|
||||
const hiddenSideStyle = computed((): CSSProperties => {
|
||||
return calcMenuWidthStyle(true);
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { isSideMixed, paddingTop, zIndex } = props;
|
||||
|
||||
return {
|
||||
...calcMenuWidthStyle(false),
|
||||
paddingTop: `${paddingTop}px`,
|
||||
zIndex,
|
||||
...(isSideMixed && extraVisible.value ? { transition: 'none' } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
const extraStyle = computed((): CSSProperties => {
|
||||
const { extraBackgroundColor, extraWidth, show, width, zIndex } = props;
|
||||
return {
|
||||
backgroundColor: extraBackgroundColor,
|
||||
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, isSideMixed, mixedWidth } = props;
|
||||
if (isSideMixed && 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, isSideMixed } = props;
|
||||
|
||||
return {
|
||||
...(isSideMixed ? { display: 'flex', justifyContent: 'center' } : {}),
|
||||
height: `${headerHeight}px`,
|
||||
...contentWidthStyle.value,
|
||||
};
|
||||
});
|
||||
|
||||
const extraContentStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight, headerHeight } = props;
|
||||
return {
|
||||
color: 'red',
|
||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
const collapseStyle = computed((): CSSProperties => {
|
||||
const { collapseHeight } = props;
|
||||
|
||||
return {
|
||||
height: `${collapseHeight}px`,
|
||||
};
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
extraVisible.value = props.fixedExtra ? true : extraVisible.value;
|
||||
});
|
||||
|
||||
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
|
||||
const { backgroundColor, extraWidth, fixedExtra, isSideMixed, show, width } =
|
||||
props;
|
||||
|
||||
let widthValue = `${width + (isSideMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
||||
|
||||
const { collapseWidth } = props;
|
||||
|
||||
if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
|
||||
widthValue = `${collapseWidth}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
|
||||
backgroundColor,
|
||||
flex: `0 0 ${widthValue}`,
|
||||
marginLeft: show ? 0 : `-${widthValue}`,
|
||||
maxWidth: widthValue,
|
||||
minWidth: widthValue,
|
||||
width: widthValue,
|
||||
};
|
||||
}
|
||||
|
||||
function handleMouseenter() {
|
||||
// 未开启和未折叠状态不生效
|
||||
if (expandOnHover.value) {
|
||||
return;
|
||||
}
|
||||
if (!expandOnHovering.value) {
|
||||
collapse.value = false;
|
||||
}
|
||||
expandOnHovering.value = true;
|
||||
}
|
||||
|
||||
function handleMouseleave() {
|
||||
emit('leave');
|
||||
|
||||
if (expandOnHover.value) {
|
||||
return;
|
||||
}
|
||||
expandOnHovering.value = false;
|
||||
collapse.value = true;
|
||||
extraVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="domVisible"
|
||||
:style="hiddenSideStyle"
|
||||
class="h-full transition-all duration-200"
|
||||
></div>
|
||||
<aside
|
||||
:style="style"
|
||||
class="fixed left-0 top-0 h-full transition-all duration-200"
|
||||
@mouseenter="handleMouseenter"
|
||||
@mouseleave="handleMouseleave"
|
||||
>
|
||||
<SidebarFixedButton
|
||||
v-if="!collapse && !isSidebarMixed"
|
||||
v-model:expand-on-hover="expandOnHover"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div v-if="slots.logo" :style="headerStyle">
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
<VbenScrollbar :style="contentStyle">
|
||||
<slot></slot>
|
||||
</VbenScrollbar>
|
||||
|
||||
<div :style="collapseStyle"></div>
|
||||
<SidebarCollapseButton
|
||||
v-if="showCollapseButton && !isSidebarMixed"
|
||||
v-model:collapsed="collapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div
|
||||
v-if="isSidebarMixed"
|
||||
ref="asideRef"
|
||||
:style="extraStyle"
|
||||
class="fixed top-0 h-full overflow-hidden transition-all duration-200"
|
||||
>
|
||||
<SidebarCollapseButton
|
||||
v-if="isSidebarMixed && expandOnHover"
|
||||
v-model:collapsed="extraCollapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
|
||||
<SidebarFixedButton
|
||||
v-if="!extraCollapse"
|
||||
v-model:expand-on-hover="expandOnHover"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div v-if="!extraCollapse" :style="extraTitleStyle">
|
||||
<slot name="extra-title"></slot>
|
||||
</div>
|
||||
<VbenScrollbar :style="extraContentStyle" class="py-4">
|
||||
<slot name="extra"></slot>
|
||||
</VbenScrollbar>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* 高度
|
||||
* @default 30
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--background))',
|
||||
fixed: true,
|
||||
height: 30,
|
||||
});
|
||||
|
||||
const hiddenStyle = computed((): CSSProperties => {
|
||||
const { height } = props;
|
||||
return {
|
||||
height: `${height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor } = props;
|
||||
return {
|
||||
...hiddenStyle.value,
|
||||
backgroundColor,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :style="style" class="border-border flex w-full border-b">
|
||||
<slot></slot>
|
||||
<div class="flex items-center">
|
||||
<slot name="toolbar"></slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
@@ -0,0 +1,2 @@
|
||||
export { default as SidebarCollapseButton } from './sidebar-collapse-button.vue';
|
||||
export { default as SidebarFixedButton } from './sidebar-fixed-button.vue';
|
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiMenuClose, MdiMenuOpen } from '@vben-core/iconify';
|
||||
|
||||
interface Props {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const collapsed = defineModel<boolean>('collapsed');
|
||||
|
||||
function handleCollapsed() {
|
||||
collapsed.value = !collapsed.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:data-theme="theme"
|
||||
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 transition-all duration-300 data-[theme=dark]:bg-[hsl(var(--dark-accent))] data-[theme=dark]:text-[hsl(var(--dark-foreground)/60%)] data-[theme=dark]:hover:bg-[hsl(var(--dark-accent-hover))] data-[theme=dark]:hover:text-[hsl(var(--dark-foreground))]"
|
||||
@click.stop="handleCollapsed"
|
||||
>
|
||||
<MdiMenuClose v-if="collapsed" />
|
||||
<MdiMenuOpen v-else />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiPin, MdiPinOff } from '@vben-core/iconify';
|
||||
|
||||
interface Props {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||
|
||||
function toggleFixed() {
|
||||
expandOnHover.value = !expandOnHover.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:data-theme="theme"
|
||||
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-1 transition-all duration-300 data-[theme=dark]:bg-[hsl(var(--dark-accent))] data-[theme=dark]:text-[hsl(var(--dark-foreground)/60%)] data-[theme=dark]:hover:bg-[hsl(var(--dark-accent-hover))] data-[theme=dark]:hover:text-[hsl(var(--dark-foreground))]"
|
||||
@click="toggleFixed"
|
||||
>
|
||||
<MdiPinOff v-if="!expandOnHover" />
|
||||
<MdiPin v-else />
|
||||
</div>
|
||||
</template>
|
2
packages/@core/ui-kit/layout-ui/src/index.ts
Normal file
2
packages/@core/ui-kit/layout-ui/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type * from './vben-layout';
|
||||
export { default as VbenAdminLayout } from './vben-layout.vue';
|
176
packages/@core/ui-kit/layout-ui/src/vben-layout.ts
Normal file
176
packages/@core/ui-kit/layout-ui/src/vben-layout.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type {
|
||||
ContentCompactType,
|
||||
LayoutHeaderModeType,
|
||||
LayoutType,
|
||||
ThemeModeType,
|
||||
} from '@vben-core/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 #fff
|
||||
*/
|
||||
footerBackgroundColor?: string;
|
||||
/**
|
||||
* footer 是否可见
|
||||
* @default false
|
||||
*/
|
||||
footerEnable?: boolean;
|
||||
/**
|
||||
* footer 是否固定
|
||||
* @default true
|
||||
*/
|
||||
footerFixed?: boolean;
|
||||
/**
|
||||
* footer 高度
|
||||
* @default 32
|
||||
*/
|
||||
footerHeight?: number;
|
||||
/**
|
||||
* 背景颜色
|
||||
* @default #fff
|
||||
*/
|
||||
headerBackgroundColor?: string;
|
||||
/**
|
||||
* header高度
|
||||
* @default 48
|
||||
*/
|
||||
headerHeight?: number;
|
||||
/**
|
||||
* header高度增加高度
|
||||
* 在顶部存在导航时,额外加高header高度
|
||||
* @default 10
|
||||
*/
|
||||
headerHeightOffset?: number;
|
||||
/**
|
||||
* 顶栏是否隐藏
|
||||
* @default false
|
||||
*/
|
||||
headerHidden?: boolean;
|
||||
/**
|
||||
* header 显示模式
|
||||
* @default 'fixed'
|
||||
*/
|
||||
headerMode?: LayoutHeaderModeType;
|
||||
/**
|
||||
* 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 48
|
||||
*/
|
||||
sideCollapseWidth?: number;
|
||||
/**
|
||||
* 侧边菜单折叠状态
|
||||
* @default false
|
||||
*/
|
||||
sidebarCollapse?: boolean;
|
||||
/**
|
||||
* 侧边菜单是否折叠时,是否显示title
|
||||
* @default true
|
||||
*/
|
||||
sidebarCollapseShowTitle?: boolean;
|
||||
/**
|
||||
* 侧边栏是否可见
|
||||
* @default true
|
||||
*/
|
||||
sidebarEnable?: boolean;
|
||||
/**
|
||||
* 侧边栏是否隐藏
|
||||
* @default false
|
||||
*/
|
||||
sidebarHidden?: boolean;
|
||||
/**
|
||||
* 混合侧边栏宽度
|
||||
* @default 80
|
||||
*/
|
||||
sidebarMixedWidth?: number;
|
||||
/**
|
||||
* 侧边栏是否半深色
|
||||
* @default false
|
||||
*/
|
||||
sidebarSemiDark?: boolean;
|
||||
/**
|
||||
* 侧边栏
|
||||
* @default dark
|
||||
*/
|
||||
sidebarTheme?: ThemeModeType;
|
||||
/**
|
||||
* 侧边栏宽度
|
||||
* @default 210
|
||||
*/
|
||||
sidebarWidth?: number;
|
||||
/**
|
||||
* tab是否可见
|
||||
* @default true
|
||||
*/
|
||||
tabbarEnable?: boolean;
|
||||
/**
|
||||
* footer背景颜色
|
||||
* @default #fff
|
||||
*/
|
||||
tabsBackgroundColor?: string;
|
||||
/**
|
||||
* tab高度
|
||||
* @default 30
|
||||
*/
|
||||
tabsHeight?: number;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 100
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
export type { VbenLayoutProps };
|
575
packages/@core/ui-kit/layout-ui/src/vben-layout.vue
Normal file
575
packages/@core/ui-kit/layout-ui/src/vben-layout.vue
Normal file
@@ -0,0 +1,575 @@
|
||||
<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,
|
||||
// footerBackgroundColor: '#fff',
|
||||
footerFixed: true,
|
||||
footerHeight: 32,
|
||||
// headerBackgroundColor: 'hsl(var(--background))',
|
||||
headerHeight: 50,
|
||||
headerHeightOffset: 10,
|
||||
headerHidden: false,
|
||||
|
||||
headerMode: 'fixed',
|
||||
headerVisible: true,
|
||||
isMobile: false,
|
||||
layout: 'sidebar-nav',
|
||||
// sideCollapse: false,
|
||||
sideCollapseWidth: 60,
|
||||
sidebarCollapseShowTitle: false,
|
||||
sidebarHidden: false,
|
||||
sidebarMixedWidth: 80,
|
||||
sidebarSemiDark: true,
|
||||
sidebarTheme: 'dark',
|
||||
sidebarWidth: 180,
|
||||
tabbarEnable: true,
|
||||
// tabsBackgroundColor: 'hsl(var(--background))',
|
||||
tabsHeight: 36,
|
||||
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.tabsHeight;
|
||||
}
|
||||
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;
|
||||
|
||||
let backgroundColor = '';
|
||||
let extraBackgroundColor = '';
|
||||
|
||||
if (isDark) {
|
||||
backgroundColor = isSidebarMixedNav.value
|
||||
? 'hsl(var(--menu-dark-darken))'
|
||||
: 'hsl(var(--menu-dark))';
|
||||
} else {
|
||||
backgroundColor = isSidebarMixedNav.value
|
||||
? 'hsl(var(--menu-darken))'
|
||||
: 'hsl(var(--menu))';
|
||||
}
|
||||
|
||||
extraBackgroundColor = isDark ? 'hsl(var(--menu-dark))' : 'hsl(var(--menu))';
|
||||
|
||||
return {
|
||||
backgroundColor,
|
||||
extraBackgroundColor,
|
||||
theme: isDark ? 'dark' : 'light',
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 遮罩可见性
|
||||
*/
|
||||
const maskVisible = computed(() => !sidebarCollapse.value && props.isMobile);
|
||||
|
||||
/**
|
||||
* header fixed值
|
||||
*/
|
||||
const headerFixed = computed(() => {
|
||||
return (
|
||||
isMixedNav.value ||
|
||||
['auto', 'auto-scroll', 'fixed'].includes(props.headerMode)
|
||||
);
|
||||
});
|
||||
|
||||
const mainStyle = computed(() => {
|
||||
let width = '100%';
|
||||
let sidebarAndExtraWidth = 'unset';
|
||||
if (
|
||||
headerFixed.value &&
|
||||
!['header-nav', 'mixed-nav'].includes(realLayout.value) &&
|
||||
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;
|
||||
|
||||
return {
|
||||
marginTop:
|
||||
fixed &&
|
||||
!fullContent.value &&
|
||||
!headerIsHidden.value &&
|
||||
(!isHeaderAuto.value || scrollY.value < headerWrapperHeight.value)
|
||||
? `${headerWrapperHeight.value}px`
|
||||
: 0,
|
||||
paddingBottom: `${props.footerEnable ? props.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 (
|
||||
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"
|
||||
:width="getSidebarWidth"
|
||||
:z-index="sidebarZIndex"
|
||||
v-bind="sidebarFace"
|
||||
@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="tabsHeight"
|
||||
:style="tabbarStyle"
|
||||
>
|
||||
<slot name="tabbar"></slot>
|
||||
<template #toolbar>
|
||||
<slot name="tabbar-tools"></slot>
|
||||
</template>
|
||||
</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>
|
||||
<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>
|
1
packages/@core/ui-kit/layout-ui/tailwind.config.mjs
Normal file
1
packages/@core/ui-kit/layout-ui/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config';
|
6
packages/@core/ui-kit/layout-ui/tsconfig.json
Normal file
6
packages/@core/ui-kit/layout-ui/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
3
packages/@core/ui-kit/layout-ui/vite.config.mts
Normal file
3
packages/@core/ui-kit/layout-ui/vite.config.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig();
|
Reference in New Issue
Block a user