mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 08:36:19 +08:00
fix: layout error
This commit is contained in:
88
packages/@core/forward/helpers/src/find-menu-by-path.test.ts
Normal file
88
packages/@core/forward/helpers/src/find-menu-by-path.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { findMenuByPath, findRootMenuByPath } from './find-menu-by-path';
|
||||
|
||||
// 示例菜单数据
|
||||
const menus: any[] = [
|
||||
{ path: '/', children: [] },
|
||||
{ path: '/about', children: [] },
|
||||
{
|
||||
path: '/contact',
|
||||
children: [
|
||||
{ path: '/contact/email', children: [] },
|
||||
{ path: '/contact/phone', children: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/services',
|
||||
children: [
|
||||
{ path: '/services/design', children: [] },
|
||||
{
|
||||
path: '/services/development',
|
||||
children: [{ path: '/services/development/web', children: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('menu Finder Tests', () => {
|
||||
it('finds a top-level menu', () => {
|
||||
const menu = findMenuByPath(menus, '/about');
|
||||
expect(menu).toBeDefined();
|
||||
expect(menu?.path).toBe('/about');
|
||||
});
|
||||
|
||||
it('finds a nested menu', () => {
|
||||
const menu = findMenuByPath(menus, '/services/development/web');
|
||||
expect(menu).toBeDefined();
|
||||
expect(menu?.path).toBe('/services/development/web');
|
||||
});
|
||||
|
||||
it('returns null for a non-existent path', () => {
|
||||
const menu = findMenuByPath(menus, '/non-existent');
|
||||
expect(menu).toBeNull();
|
||||
});
|
||||
|
||||
it('handles empty menus list', () => {
|
||||
const menu = findMenuByPath([], '/about');
|
||||
expect(menu).toBeNull();
|
||||
});
|
||||
|
||||
it('handles menu items without children', () => {
|
||||
const menu = findMenuByPath(
|
||||
[{ path: '/only', children: undefined }] as any[],
|
||||
'/only',
|
||||
);
|
||||
expect(menu).toBeDefined();
|
||||
expect(menu?.path).toBe('/only');
|
||||
});
|
||||
|
||||
it('finds root menu by path', () => {
|
||||
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
||||
menus,
|
||||
'/services/development/web',
|
||||
);
|
||||
|
||||
expect(findMenu).toBeDefined();
|
||||
expect(rootMenu).toBeUndefined();
|
||||
expect(rootMenuPath).toBeUndefined();
|
||||
expect(findMenu?.path).toBe('/services/development/web');
|
||||
});
|
||||
|
||||
it('returns null for undefined or empty path', () => {
|
||||
const menuUndefinedPath = findMenuByPath(menus);
|
||||
const menuEmptyPath = findMenuByPath(menus, '');
|
||||
expect(menuUndefinedPath).toBeNull();
|
||||
expect(menuEmptyPath).toBeNull();
|
||||
});
|
||||
|
||||
it('checks for root menu when path does not exist', () => {
|
||||
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
||||
menus,
|
||||
'/non-existent',
|
||||
);
|
||||
expect(findMenu).toBeNull();
|
||||
expect(rootMenu).toBeUndefined();
|
||||
expect(rootMenuPath).toBeUndefined();
|
||||
});
|
||||
});
|
35
packages/@core/forward/helpers/src/find-menu-by-path.ts
Normal file
35
packages/@core/forward/helpers/src/find-menu-by-path.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
function findMenuByPath(
|
||||
list: MenuRecordRaw[],
|
||||
path?: string,
|
||||
): MenuRecordRaw | null {
|
||||
for (const menu of list) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
const findMenu = menu.children && findMenuByPath(menu.children, path);
|
||||
if (findMenu) {
|
||||
return findMenu;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根菜单
|
||||
* @param menus
|
||||
* @param path
|
||||
*/
|
||||
function findRootMenuByPath(menus: MenuRecordRaw[], path?: string) {
|
||||
const findMenu = findMenuByPath(menus, path);
|
||||
const rootMenuPath = findMenu?.parents?.[0];
|
||||
const rootMenu = menus.find((item) => item.path === rootMenuPath);
|
||||
return {
|
||||
findMenu,
|
||||
rootMenu,
|
||||
rootMenuPath,
|
||||
};
|
||||
}
|
||||
|
||||
export { findMenuByPath, findRootMenuByPath };
|
@@ -1,3 +1,4 @@
|
||||
export * from './find-menu-by-path';
|
||||
export * from './flatten-object';
|
||||
export * from './generator-menus';
|
||||
export * from './generator-routes';
|
||||
|
@@ -36,7 +36,7 @@
|
||||
"@vben-core/helpers": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"@vueuse/core": "^10.10.1",
|
||||
"vue": "3.4.27"
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ const defaultPreferences: Preferences = {
|
||||
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.0/source/avatar-v1.webp',
|
||||
dynamicTitle: true,
|
||||
isMobile: false,
|
||||
layout: 'side-nav',
|
||||
layout: 'sidebar-nav',
|
||||
locale: 'zh-CN',
|
||||
name: 'Vben Admin Pro',
|
||||
semiDarkMenu: true,
|
||||
@@ -47,8 +47,8 @@ const defaultPreferences: Preferences = {
|
||||
},
|
||||
shortcutKeys: { enable: true },
|
||||
sidebar: {
|
||||
collapse: false,
|
||||
collapseShowTitle: true,
|
||||
collapsed: false,
|
||||
collapsedShowTitle: true,
|
||||
enable: true,
|
||||
expandOnHover: true,
|
||||
extraCollapse: true,
|
||||
|
@@ -94,9 +94,9 @@ interface NavigationPreferences {
|
||||
|
||||
interface SidebarPreferences {
|
||||
/** 侧边栏是否折叠 */
|
||||
collapse: boolean;
|
||||
collapsed: boolean;
|
||||
/** 侧边栏折叠时,是否显示title */
|
||||
collapseShowTitle: boolean;
|
||||
collapsedShowTitle: boolean;
|
||||
/** 侧边栏是否可见 */
|
||||
enable: boolean;
|
||||
/** 菜单自动展开状态 */
|
||||
|
@@ -33,7 +33,7 @@ function usePreferences() {
|
||||
* @zh_CN 布局方式
|
||||
*/
|
||||
const layout = computed(() =>
|
||||
appPreferences.value.isMobile ? 'side-nav' : appPreferences.value.layout,
|
||||
appPreferences.value.isMobile ? 'sidebar-nav' : appPreferences.value.layout,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -46,13 +46,15 @@ function usePreferences() {
|
||||
/**
|
||||
* @zh_CN 是否侧边导航模式
|
||||
*/
|
||||
const isSideNav = computed(() => appPreferences.value.layout === 'side-nav');
|
||||
const isSideNav = computed(
|
||||
() => appPreferences.value.layout === 'sidebar-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否侧边混合模式
|
||||
*/
|
||||
const isSideMixedNav = computed(
|
||||
() => appPreferences.value.layout === 'side-mixed-nav',
|
||||
() => appPreferences.value.layout === 'sidebar-mixed-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
|
4
packages/@core/shared/typings/src/app.d.ts
vendored
4
packages/@core/shared/typings/src/app.d.ts
vendored
@@ -4,8 +4,8 @@ type LayoutType =
|
||||
| 'full-content'
|
||||
| 'header-nav'
|
||||
| 'mixed-nav'
|
||||
| 'side-mixed-nav'
|
||||
| 'side-nav';
|
||||
| 'sidebar-mixed-nav'
|
||||
| 'sidebar-nav';
|
||||
|
||||
type ThemeModeType = 'auto' | 'dark' | 'light';
|
||||
|
||||
|
@@ -41,12 +41,10 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/design": "workspace:*",
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"@vueuse/core": "^10.10.1",
|
||||
"vue": "3.4.27"
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +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 LayoutSide } from './layout-side.vue';
|
||||
export { default as LayoutTabs } from './layout-tabs.vue';
|
||||
export { default as LayoutSidebar } from './layout-sidebar.vue';
|
||||
export { default as LayoutTabbar } from './layout-tabbar.vue';
|
||||
|
@@ -42,8 +42,6 @@ interface Props {
|
||||
paddingTop?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
@@ -65,7 +63,9 @@ const style = computed((): CSSProperties => {
|
||||
} = props;
|
||||
|
||||
const compactStyle: CSSProperties =
|
||||
contentCompact === 'compact' ? { margin: '0 auto', width: `1200px` } : {};
|
||||
contentCompact === 'compact'
|
||||
? { margin: '0 auto', width: `${props.contentCompactWidth}px` }
|
||||
: {};
|
||||
return {
|
||||
...compactStyle,
|
||||
flex: 1,
|
||||
|
@@ -2,8 +2,6 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 背景颜色
|
||||
@@ -36,8 +34,6 @@ interface Props {
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutFooter' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--color-background))',
|
||||
fixed: true,
|
||||
@@ -47,8 +43,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const { b } = useNamespace('footer');
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const { backgroundColor, fixed, height, show, width, zIndex } = props;
|
||||
return {
|
||||
@@ -63,11 +57,7 @@ const style = computed((): CSSProperties => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer
|
||||
:class="b()"
|
||||
:style="style"
|
||||
class="bottom-0 w-full transition-all duration-200"
|
||||
>
|
||||
<footer :style="style" class="bottom-0 w-full transition-all duration-200">
|
||||
<slot></slot>
|
||||
</footer>
|
||||
</template>
|
||||
|
@@ -41,15 +41,12 @@ interface Props {
|
||||
* @default true
|
||||
*/
|
||||
showToggleBtn?: boolean;
|
||||
/**
|
||||
* 侧边是否显示
|
||||
*/
|
||||
sideHidden?: boolean;
|
||||
|
||||
/**
|
||||
* 侧边菜单宽度
|
||||
* @default 0
|
||||
*/
|
||||
sideWidth?: number;
|
||||
sidebarWidth?: number;
|
||||
/**
|
||||
* 宽度
|
||||
* @default 100%
|
||||
@@ -62,8 +59,6 @@ interface Props {
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutHeader' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--color-background))',
|
||||
// fixed: true,
|
||||
@@ -71,12 +66,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isMixedNav: false,
|
||||
show: true,
|
||||
showToggleBtn: false,
|
||||
sideWidth: 0,
|
||||
sidebarWidth: 0,
|
||||
width: '100%',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ openMenu: []; toggleMenu: [] }>();
|
||||
const emit = defineEmits<{ openMenu: []; toggleSidebar: [] }>();
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
@@ -95,16 +90,16 @@ const style = computed((): CSSProperties => {
|
||||
|
||||
const logoStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
minWidth: `${props.isMobile ? 40 : props.sideWidth}px`,
|
||||
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
|
||||
};
|
||||
});
|
||||
|
||||
function handleToggleMenu() {
|
||||
emit('toggleMenu');
|
||||
}
|
||||
|
||||
function handleOpenMenu() {
|
||||
emit('openMenu');
|
||||
if (props.isMobile) {
|
||||
emit('openMenu');
|
||||
} else {
|
||||
emit('toggleSidebar');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -117,21 +112,12 @@ function handleOpenMenu() {
|
||||
<slot name="logo"></slot>
|
||||
</div>
|
||||
<VbenIconButton
|
||||
v-if="showToggleBtn"
|
||||
v-if="showToggleBtn || isMobile"
|
||||
class="my-0 ml-2 mr-1 rounded"
|
||||
@click="handleToggleMenu"
|
||||
>
|
||||
<IcRoundMenu class="size-5" />
|
||||
</VbenIconButton>
|
||||
|
||||
<VbenIconButton
|
||||
v-if="isMobile"
|
||||
class="my-0 ml-2 mr-1 rounded"
|
||||
@click="handleOpenMenu"
|
||||
>
|
||||
<IcRoundMenu class="size-5" />
|
||||
</VbenIconButton>
|
||||
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
|
@@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
// import { onClickOutside } from '@vueuse/core';
|
||||
import { computed, ref, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
import { computed, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
|
||||
import { ScrollArea } from '@vben-core/shadcn-ui';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { SideCollapseButton, SidePinButton } from './widgets';
|
||||
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -50,7 +48,7 @@ interface Props {
|
||||
* 是否侧边混合模式
|
||||
* @default false
|
||||
*/
|
||||
isSideMixed?: boolean;
|
||||
isSidebarMixed?: boolean;
|
||||
/**
|
||||
* 混合菜单宽度
|
||||
* @default 80
|
||||
@@ -88,8 +86,6 @@ interface Props {
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutSide' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapseHeight: 42,
|
||||
collapseWidth: 48,
|
||||
@@ -113,11 +109,9 @@ const expandOnHovering = defineModel<boolean>('expandOnHovering');
|
||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||
const extraVisible = defineModel<boolean>('extraVisible');
|
||||
|
||||
const { b, e, is } = useNamespace('side');
|
||||
const slots = useSlots();
|
||||
|
||||
const asideRef = shallowRef<HTMLDivElement | null>();
|
||||
const scrolled = ref(false);
|
||||
|
||||
const hiddenSideStyle = computed((): CSSProperties => {
|
||||
return calcMenuWidthStyle(true);
|
||||
@@ -244,54 +238,51 @@ function handleMouseleave() {
|
||||
collapse.value = true;
|
||||
extraVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
scrolled.value = (target?.scrollTop ?? 0) > 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="domVisible" :class="e('hide')" :style="hiddenSideStyle"></div>
|
||||
<div
|
||||
v-if="domVisible"
|
||||
:style="hiddenSideStyle"
|
||||
class="h-full transition-all duration-200"
|
||||
></div>
|
||||
<aside
|
||||
:class="[b(), is(theme, true)]"
|
||||
:style="style"
|
||||
class="fixed left-0 top-0 h-full transition-all duration-200"
|
||||
@mouseenter="handleMouseenter"
|
||||
@mouseleave="handleMouseleave"
|
||||
>
|
||||
<SidePinButton
|
||||
v-if="!collapse && !isSideMixed"
|
||||
<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>
|
||||
<ScrollArea :on-scroll="handleScroll" :style="contentStyle">
|
||||
<div :class="[e('shadow'), { scrolled }]"></div>
|
||||
<VbenScrollbar :style="contentStyle">
|
||||
<slot></slot>
|
||||
</ScrollArea>
|
||||
</VbenScrollbar>
|
||||
|
||||
<div :style="collapseStyle"></div>
|
||||
<SideCollapseButton
|
||||
v-if="showCollapseButton && !isSideMixed"
|
||||
v-model:collapse="collapse"
|
||||
<SidebarCollapseButton
|
||||
v-if="showCollapseButton && !isSidebarMixed"
|
||||
v-model:collapsed="collapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
<div
|
||||
v-if="isSideMixed"
|
||||
v-if="isSidebarMixed"
|
||||
ref="asideRef"
|
||||
:class="e('extra')"
|
||||
:style="extraStyle"
|
||||
class="transition-[width] duration-200"
|
||||
class="fixed top-0 h-full overflow-hidden transition-all duration-200"
|
||||
>
|
||||
<SideCollapseButton
|
||||
v-if="isSideMixed && expandOnHover"
|
||||
v-model:collapse="extraCollapse"
|
||||
<SidebarCollapseButton
|
||||
v-if="isSidebarMixed && expandOnHover"
|
||||
v-model:collapsed="extraCollapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
|
||||
<SidePinButton
|
||||
<SidebarFixedButton
|
||||
v-if="!extraCollapse"
|
||||
v-model:expand-on-hover="expandOnHover"
|
||||
:theme="theme"
|
||||
@@ -299,79 +290,49 @@ function handleScroll(event: Event) {
|
||||
<div v-if="!extraCollapse" :style="extraTitleStyle">
|
||||
<slot name="extra-title"></slot>
|
||||
</div>
|
||||
<ScrollArea
|
||||
:class="e('extra-content')"
|
||||
:on-scroll="handleScroll"
|
||||
:style="extraContentStyle"
|
||||
>
|
||||
<div :class="[e('shadow'), { scrolled }]"></div>
|
||||
<VbenScrollbar :style="extraContentStyle" class="py-4">
|
||||
<slot name="extra"></slot>
|
||||
</ScrollArea>
|
||||
</VbenScrollbar>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
// @include b('sidebar') {
|
||||
// --color-surface: var(--color-menu);
|
||||
|
||||
@include b('side') {
|
||||
--color-surface: var(--color-menu);
|
||||
// @include is('dark') {
|
||||
// --color-surface: var(--color-menu-dark);
|
||||
// }
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
transition: all 0.2s ease 0s;
|
||||
// @include e('shadow') {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// z-index: 1;
|
||||
// inline-size: 100%;
|
||||
// block-size: 40px;
|
||||
// height: 50px;
|
||||
// pointer-events: none;
|
||||
// background: linear-gradient(
|
||||
// to bottom,
|
||||
// hsl(var(--color-surface)),
|
||||
// transparent
|
||||
// );
|
||||
// opacity: 0;
|
||||
// transition: opacity 0.15s ease-in-out;
|
||||
// will-change: opacity;
|
||||
|
||||
@include is('dark') {
|
||||
--color-surface: var(--color-menu-dark);
|
||||
}
|
||||
// &.scrolled {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
|
||||
@include e('shadow') {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
inline-size: 100%;
|
||||
block-size: 40px;
|
||||
height: 50px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--color-surface)),
|
||||
transparent
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
will-change: opacity;
|
||||
|
||||
&.scrolled {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include is('dark') {
|
||||
.#{$namespace}-side__extra {
|
||||
&-content {
|
||||
border-color: hsl(var(--color-dark-border)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include e('hide') {
|
||||
height: 100%;
|
||||
transition: all 0.2s ease 0s;
|
||||
}
|
||||
|
||||
@include e('extra') {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease 0s;
|
||||
|
||||
&-content {
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// @include is('dark') {
|
||||
// .#{$namespace}-side__extra {
|
||||
// &-content {
|
||||
// border-color: hsl(var(--color-dark-border)) !important;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
</style>
|
@@ -14,8 +14,6 @@ interface Props {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'LayoutTabs' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
backgroundColor: 'hsl(var(--color-background))',
|
||||
fixed: true,
|
||||
@@ -34,7 +32,6 @@ const style = computed((): CSSProperties => {
|
||||
return {
|
||||
...hiddenStyle.value,
|
||||
backgroundColor,
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
</script>
|
@@ -1,2 +1,2 @@
|
||||
export { default as SideCollapseButton } from './side-collapse-button.vue';
|
||||
export { default as SidePinButton } from './side-pin-button.vue';
|
||||
export { default as SidebarCollapseButton } from './sidebar-collapse-button.vue';
|
||||
export { default as SidebarFixedButton } from './sidebar-fixed-button.vue';
|
||||
|
@@ -1,63 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiMenuClose, MdiMenuOpen } from '@vben-core/iconify';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
interface Props {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'SideCollapseButton' });
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const collapse = defineModel<boolean>('collapse');
|
||||
|
||||
const { b, is } = useNamespace('side-collapse');
|
||||
|
||||
function handleCollapse() {
|
||||
collapse.value = !collapse.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[b(), is(theme, true)]" @click.stop="handleCollapse">
|
||||
<MdiMenuClose v-if="collapse" />
|
||||
<MdiMenuOpen v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('side-collapse') {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
color: hsl(var(--color-foreground) / 60%);
|
||||
cursor: pointer;
|
||||
background: hsl(var(--color-accent)) !important;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@include is('dark') {
|
||||
color: hsl(var(--color-dark-foreground) / 60%) !important;
|
||||
background: hsl(var(--color-dark-accent)) !important;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-dark-foreground)) !important;
|
||||
background: hsl(var(--color-dark-accent-hover)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-accent-hover));
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,64 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiPin, MdiPinOff } from '@vben-core/iconify';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
interface Props {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'SidePinButton' });
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||
|
||||
const { b, is } = useNamespace('side-pin');
|
||||
|
||||
function togglePined() {
|
||||
expandOnHover.value = !expandOnHover.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[b(), is(theme, true)]" @click="togglePined">
|
||||
<MdiPinOff v-if="!expandOnHover" />
|
||||
<MdiPin v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('side-pin') {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 6px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
color: hsl(var(--color-foreground) / 60%);
|
||||
cursor: pointer;
|
||||
background: hsl(var(--color-accent)) !important;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@include is('dark') {
|
||||
color: hsl(var(--color-dark-foreground) / 60%) !important;
|
||||
background: unset;
|
||||
background: hsl(var(--color-dark-accent)) !important;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-dark-foreground)) !important;
|
||||
background: hsl(var(--color-dark-accent-hover)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-accent-hover));
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -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(--color-dark-accent))] data-[theme=dark]:text-[hsl(var(--color-dark-foreground)/60%)] data-[theme=dark]:hover:bg-[hsl(var(--color-dark-accent-hover))] data-[theme=dark]:hover:text-[hsl(var(--color-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(--color-dark-accent))] data-[theme=dark]:text-[hsl(var(--color-dark-foreground)/60%)] data-[theme=dark]:hover:bg-[hsl(var(--color-dark-accent-hover))] data-[theme=dark]:hover:text-[hsl(var(--color-dark-foreground))]"
|
||||
@click="toggleFixed"
|
||||
>
|
||||
<MdiPinOff v-if="!expandOnHover" />
|
||||
<MdiPin v-else />
|
||||
</div>
|
||||
</template>
|
@@ -99,64 +99,64 @@ interface VbenLayoutProps {
|
||||
isMobile?: boolean;
|
||||
/**
|
||||
* 布局方式
|
||||
* side-nav 侧边菜单布局
|
||||
* sidebar-nav 侧边菜单布局
|
||||
* header-nav 顶部菜单布局
|
||||
* mixed-nav 侧边&顶部菜单布局
|
||||
* side-mixed-nav 侧边混合菜单布局
|
||||
* sidebar-mixed-nav 侧边混合菜单布局
|
||||
* full-content 全屏内容布局
|
||||
* @default side-nav
|
||||
* @default sidebar-nav
|
||||
*/
|
||||
layout?: LayoutType;
|
||||
/**
|
||||
* 侧边菜单折叠状态
|
||||
* @default false
|
||||
*/
|
||||
sideCollapse?: boolean;
|
||||
/**
|
||||
* 侧边菜单是否折叠时,是否显示title
|
||||
* @default true
|
||||
*/
|
||||
sideCollapseShowTitle?: boolean;
|
||||
/**
|
||||
* 侧边菜单折叠宽度
|
||||
* @default 48
|
||||
*/
|
||||
sideCollapseWidth?: number;
|
||||
/**
|
||||
* 侧边栏是否隐藏
|
||||
* 侧边菜单折叠状态
|
||||
* @default false
|
||||
*/
|
||||
sideHidden?: boolean;
|
||||
sidebarCollapse?: boolean;
|
||||
/**
|
||||
* 混合侧边扩展区域是否可见
|
||||
* @default false
|
||||
* 侧边菜单是否折叠时,是否显示title
|
||||
* @default true
|
||||
*/
|
||||
sideMixedExtraVisible?: boolean;
|
||||
/**
|
||||
* 混合侧边栏宽度
|
||||
* @default 80
|
||||
*/
|
||||
sideMixedWidth?: number;
|
||||
/**
|
||||
* 侧边栏是否半深色
|
||||
* @default false
|
||||
*/
|
||||
sideSemiDark?: boolean;
|
||||
/**
|
||||
* 侧边栏
|
||||
* @default dark
|
||||
*/
|
||||
sideTheme?: ThemeModeType;
|
||||
sidebarCollapseShowTitle?: boolean;
|
||||
/**
|
||||
* 侧边栏是否可见
|
||||
* @default true
|
||||
*/
|
||||
sideVisible?: boolean;
|
||||
sidebarEnable?: boolean;
|
||||
/**
|
||||
* 侧边栏是否隐藏
|
||||
* @default false
|
||||
*/
|
||||
sidebarHidden?: boolean;
|
||||
/**
|
||||
* 混合侧边栏宽度
|
||||
* @default 80
|
||||
*/
|
||||
sidebarMixedWidth?: number;
|
||||
/**
|
||||
* 侧边栏是否半深色
|
||||
* @default false
|
||||
*/
|
||||
sidebarSemiDark?: boolean;
|
||||
/**
|
||||
* 侧边栏
|
||||
* @default dark
|
||||
*/
|
||||
sidebarTheme?: ThemeModeType;
|
||||
/**
|
||||
* 侧边栏宽度
|
||||
* @default 210
|
||||
*/
|
||||
sideWidth?: number;
|
||||
sidebarWidth?: number;
|
||||
/**
|
||||
* tab是否可见
|
||||
* @default true
|
||||
*/
|
||||
tabbarEnable?: boolean;
|
||||
/**
|
||||
* footer背景颜色
|
||||
* @default #fff
|
||||
@@ -167,11 +167,6 @@ interface VbenLayoutProps {
|
||||
* @default 30
|
||||
*/
|
||||
tabsHeight?: number;
|
||||
/**
|
||||
* tab是否可见
|
||||
* @default true
|
||||
*/
|
||||
tabsVisible?: boolean;
|
||||
/**
|
||||
* zIndex
|
||||
* @default 100
|
||||
|
@@ -8,8 +8,8 @@ import {
|
||||
LayoutContent,
|
||||
LayoutFooter,
|
||||
LayoutHeader,
|
||||
LayoutSide,
|
||||
LayoutTabs,
|
||||
LayoutSidebar,
|
||||
LayoutTabbar,
|
||||
} from './components';
|
||||
import { VbenLayoutProps } from './vben-layout';
|
||||
|
||||
@@ -38,27 +38,27 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
headerMode: 'fixed',
|
||||
headerVisible: true,
|
||||
isMobile: false,
|
||||
layout: 'side-nav',
|
||||
sideCollapseShowTitle: false,
|
||||
layout: 'sidebar-nav',
|
||||
// sideCollapse: false,
|
||||
sideCollapseWidth: 60,
|
||||
sideHidden: false,
|
||||
sideMixedWidth: 80,
|
||||
sideSemiDark: true,
|
||||
sideTheme: 'dark',
|
||||
sideWidth: 180,
|
||||
sidebarCollapseShowTitle: false,
|
||||
sidebarHidden: false,
|
||||
sidebarMixedWidth: 80,
|
||||
sidebarSemiDark: true,
|
||||
sidebarTheme: 'dark',
|
||||
sidebarWidth: 180,
|
||||
tabbarEnable: true,
|
||||
// tabsBackgroundColor: 'hsl(var(--color-background))',
|
||||
tabsHeight: 36,
|
||||
tabsVisible: true,
|
||||
zIndex: 200,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ sideMouseLeave: [] }>();
|
||||
const sideCollapse = defineModel<boolean>('sideCollapse');
|
||||
const sideExtraVisible = defineModel<boolean>('sideExtraVisible');
|
||||
const sideExtraCollapse = defineModel<boolean>('sideExtraCollapse');
|
||||
const sideExpandOnHover = defineModel<boolean>('sideExpandOnHover');
|
||||
const sideVisible = defineModel<boolean>('sideVisible', { default: true });
|
||||
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,
|
||||
@@ -69,12 +69,12 @@ const {
|
||||
const { y: mouseY } = useMouse({ type: 'client' });
|
||||
|
||||
// side是否处于hover状态展开菜单中
|
||||
const sideExpandOnHovering = ref(false);
|
||||
const sidebarExpandOnHovering = ref(false);
|
||||
// const sideHidden = ref(false);
|
||||
const headerIsHidden = ref(false);
|
||||
|
||||
const realLayout = computed(() => {
|
||||
return props.isMobile ? 'side-nav' : props.layout;
|
||||
return props.isMobile ? 'sidebar-nav' : props.layout;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -85,7 +85,9 @@ const fullContent = computed(() => realLayout.value === 'full-content');
|
||||
/**
|
||||
* 是否侧边混合模式
|
||||
*/
|
||||
const isSideMixedNav = computed(() => realLayout.value === 'side-mixed-nav');
|
||||
const isSidebarMixedNav = computed(
|
||||
() => realLayout.value === 'sidebar-mixed-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* 是否为头部导航模式
|
||||
@@ -123,25 +125,25 @@ const headerWrapperHeight = computed(() => {
|
||||
if (props.headerVisible && !props.headerHidden) {
|
||||
height += getHeaderHeight.value;
|
||||
}
|
||||
if (props.tabsVisible) {
|
||||
if (props.tabbarEnable) {
|
||||
height += props.tabsHeight;
|
||||
}
|
||||
|
||||
return height;
|
||||
});
|
||||
|
||||
const getSideCollapseWidth = computed(() => {
|
||||
const { sideCollapseShowTitle, sideCollapseWidth, sideMixedWidth } = props;
|
||||
return sideCollapseShowTitle || isSideMixedNav
|
||||
? sideMixedWidth
|
||||
const { sideCollapseWidth, sidebarCollapseShowTitle, sidebarMixedWidth } =
|
||||
props;
|
||||
return sidebarCollapseShowTitle || isSidebarMixedNav.value
|
||||
? sidebarMixedWidth
|
||||
: sideCollapseWidth;
|
||||
});
|
||||
|
||||
/**
|
||||
* 动态获取侧边区域是否可见
|
||||
*/
|
||||
const sideVisibleState = computed(() => {
|
||||
return !isHeaderNav.value && sideVisible.value;
|
||||
const sidebarEnableState = computed(() => {
|
||||
return !isHeaderNav.value && sidebarEnable.value;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -155,27 +157,27 @@ const sidePaddingTop = computed(() => {
|
||||
/**
|
||||
* 动态获取侧边宽度
|
||||
*/
|
||||
const getSideWidth = computed(() => {
|
||||
const { isMobile, sideHidden, sideMixedWidth, sideWidth } = props;
|
||||
const getSidebarWidth = computed(() => {
|
||||
const { isMobile, sidebarHidden, sidebarMixedWidth, sidebarWidth } = props;
|
||||
let width = 0;
|
||||
|
||||
if (sideHidden) {
|
||||
if (sidebarHidden) {
|
||||
return width;
|
||||
}
|
||||
|
||||
if (
|
||||
!sideVisibleState.value ||
|
||||
(sideHidden && !isSideMixedNav.value && !isMixedNav.value)
|
||||
!sidebarEnableState.value ||
|
||||
(sidebarHidden && !isSidebarMixedNav.value && !isMixedNav.value)
|
||||
) {
|
||||
return width;
|
||||
}
|
||||
|
||||
if (isSideMixedNav.value && !isMobile) {
|
||||
width = sideMixedWidth;
|
||||
} else if (sideCollapse.value) {
|
||||
if (isSidebarMixedNav.value && !isMobile) {
|
||||
width = sidebarMixedWidth;
|
||||
} else if (sidebarCollapse.value) {
|
||||
width = isMobile ? 0 : getSideCollapseWidth.value;
|
||||
} else {
|
||||
width = sideWidth;
|
||||
width = sidebarWidth;
|
||||
}
|
||||
return width;
|
||||
});
|
||||
@@ -184,37 +186,37 @@ const getSideWidth = computed(() => {
|
||||
* 获取扩展区域宽度
|
||||
*/
|
||||
const getExtraWidth = computed(() => {
|
||||
const { sideWidth } = props;
|
||||
return sideExtraCollapse.value ? getSideCollapseWidth.value : sideWidth;
|
||||
const { sidebarWidth } = props;
|
||||
return sidebarExtraCollapse.value ? getSideCollapseWidth.value : sidebarWidth;
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否侧边栏模式,包含混合侧边
|
||||
*/
|
||||
const isSideMode = computed(() =>
|
||||
['mixed-nav', 'side-mixed-nav', 'side-nav'].includes(realLayout.value),
|
||||
['mixed-nav', 'sidebar-mixed-nav', 'sidebar-nav'].includes(realLayout.value),
|
||||
);
|
||||
|
||||
const showSide = computed(() => {
|
||||
const showSidebar = computed(() => {
|
||||
// if (isMixedNav.value && !props.sideHidden) {
|
||||
// return false;
|
||||
// }
|
||||
return isSideMode.value && sideVisible.value;
|
||||
return isSideMode.value && sidebarEnable.value;
|
||||
});
|
||||
|
||||
const sideFace = computed(() => {
|
||||
const { sideSemiDark, sideTheme } = props;
|
||||
const isDark = sideTheme === 'dark' || sideSemiDark;
|
||||
const sidebarFace = computed(() => {
|
||||
const { sidebarSemiDark, sidebarTheme } = props;
|
||||
const isDark = sidebarTheme === 'dark' || sidebarSemiDark;
|
||||
|
||||
let backgroundColor = '';
|
||||
let extraBackgroundColor = '';
|
||||
|
||||
if (isDark) {
|
||||
backgroundColor = isSideMixedNav.value
|
||||
backgroundColor = isSidebarMixedNav.value
|
||||
? 'hsl(var(--color-menu-dark-darken))'
|
||||
: 'hsl(var(--color-menu-dark))';
|
||||
} else {
|
||||
backgroundColor = isSideMixedNav.value
|
||||
backgroundColor = isSidebarMixedNav.value
|
||||
? 'hsl(var(--color-menu-darken))'
|
||||
: 'hsl(var(--color-menu))';
|
||||
}
|
||||
@@ -233,7 +235,7 @@ const sideFace = computed(() => {
|
||||
/**
|
||||
* 遮罩可见性
|
||||
*/
|
||||
const maskVisible = computed(() => !sideCollapse.value && props.isMobile);
|
||||
const maskVisible = computed(() => !sidebarCollapse.value && props.isMobile);
|
||||
|
||||
/**
|
||||
* header fixed值
|
||||
@@ -247,53 +249,55 @@ const headerFixed = computed(() => {
|
||||
|
||||
const mainStyle = computed(() => {
|
||||
let width = '100%';
|
||||
let sidebarWidth = 'unset';
|
||||
let sidebarAndExtraWidth = 'unset';
|
||||
if (
|
||||
headerFixed.value &&
|
||||
!['header-nav', 'mixed-nav'].includes(realLayout.value) &&
|
||||
showSide.value &&
|
||||
showSidebar.value &&
|
||||
!props.isMobile
|
||||
) {
|
||||
// pin模式下生效
|
||||
// fixed模式下生效
|
||||
const isSideNavEffective =
|
||||
isSideMixedNav.value && sideExpandOnHover.value && sideExtraVisible.value;
|
||||
isSidebarMixedNav.value &&
|
||||
sidebarExpandOnHover.value &&
|
||||
sidebarExtraVisible.value;
|
||||
|
||||
if (isSideNavEffective) {
|
||||
const sideCollapseWidth = sideCollapse.value
|
||||
const sideCollapseWidth = sidebarCollapse.value
|
||||
? getSideCollapseWidth.value
|
||||
: props.sideMixedWidth;
|
||||
const sideWidth = sideExtraCollapse.value
|
||||
: props.sidebarMixedWidth;
|
||||
const sideWidth = sidebarExtraCollapse.value
|
||||
? getSideCollapseWidth.value
|
||||
: props.sideWidth;
|
||||
: props.sidebarWidth;
|
||||
|
||||
// 100% - 侧边菜单混合宽度 - 菜单宽度
|
||||
sidebarWidth = `${sideCollapseWidth + sideWidth}px`;
|
||||
width = `calc(100% - ${sidebarWidth})`;
|
||||
sidebarAndExtraWidth = `${sideCollapseWidth + sideWidth}px`;
|
||||
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
||||
} else {
|
||||
sidebarWidth =
|
||||
sideExpandOnHovering.value && !sideExpandOnHover.value
|
||||
sidebarAndExtraWidth =
|
||||
sidebarExpandOnHovering.value && !sidebarExpandOnHover.value
|
||||
? `${getSideCollapseWidth.value}px`
|
||||
: `${getSideWidth.value}px`;
|
||||
width = `calc(100% - ${sidebarWidth})`;
|
||||
: `${getSidebarWidth.value}px`;
|
||||
width = `calc(100% - ${sidebarAndExtraWidth})`;
|
||||
}
|
||||
}
|
||||
return {
|
||||
sidebarWidth,
|
||||
sidebarAndExtraWidth,
|
||||
width,
|
||||
};
|
||||
});
|
||||
|
||||
const tabsStyle = computed((): CSSProperties => {
|
||||
const tabbarStyle = computed((): CSSProperties => {
|
||||
let width = '';
|
||||
let marginLeft = 0;
|
||||
|
||||
if (!isMixedNav.value) {
|
||||
width = '100%';
|
||||
} else if (sideVisible.value) {
|
||||
marginLeft = sideCollapse.value
|
||||
} else if (sidebarEnable.value) {
|
||||
marginLeft = sidebarCollapse.value
|
||||
? getSideCollapseWidth.value
|
||||
: props.sideWidth;
|
||||
width = `calc(100% - ${getSideWidth.value}px)`;
|
||||
: props.sidebarWidth;
|
||||
width = `calc(100% - ${getSidebarWidth.value}px)`;
|
||||
} else {
|
||||
width = '100%';
|
||||
}
|
||||
@@ -304,14 +308,6 @@ const tabsStyle = computed((): CSSProperties => {
|
||||
};
|
||||
});
|
||||
|
||||
const footerWidth = computed(() => {
|
||||
if (!props.footerFixed) {
|
||||
return '100%';
|
||||
}
|
||||
|
||||
return mainStyle.value.width;
|
||||
});
|
||||
|
||||
const contentStyle = computed((): CSSProperties => {
|
||||
const fixed = headerFixed.value;
|
||||
|
||||
@@ -337,7 +333,7 @@ const headerWrapperStyle = computed((): CSSProperties => {
|
||||
const fixed = headerFixed.value;
|
||||
return {
|
||||
height: fullContent.value ? '0' : `${headerWrapperHeight.value}px`,
|
||||
left: isMixedNav.value ? 0 : mainStyle.value.sidebarWidth,
|
||||
left: isMixedNav.value ? 0 : mainStyle.value.sidebarAndExtraWidth,
|
||||
position: fixed ? 'fixed' : 'static',
|
||||
top:
|
||||
headerIsHidden.value || fullContent.value
|
||||
@@ -351,26 +347,31 @@ const headerWrapperStyle = computed((): CSSProperties => {
|
||||
/**
|
||||
* 侧边栏z-index
|
||||
*/
|
||||
const sideZIndex = computed(() => {
|
||||
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,
|
||||
};
|
||||
return { zIndex: props.zIndex };
|
||||
});
|
||||
|
||||
const showHeaderToggleButton = computed(() => {
|
||||
return (
|
||||
isSideMode.value &&
|
||||
!isSideMixedNav.value &&
|
||||
!isSidebarMixedNav.value &&
|
||||
!isMixedNav.value &&
|
||||
!props.isMobile
|
||||
);
|
||||
// return false;
|
||||
});
|
||||
|
||||
const showHeaderLogo = computed(() => {
|
||||
@@ -380,7 +381,7 @@ const showHeaderLogo = computed(() => {
|
||||
watch(
|
||||
() => props.isMobile,
|
||||
(val) => {
|
||||
sideCollapse.value = val;
|
||||
sidebarCollapse.value = val;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -445,49 +446,48 @@ watch(
|
||||
}
|
||||
|
||||
function handleClickMask() {
|
||||
sideCollapse.value = true;
|
||||
sidebarCollapse.value = true;
|
||||
}
|
||||
|
||||
function handleToggleMenu() {
|
||||
// sideVisible.value = !sideVisible.value;
|
||||
// sideHidden.value = !sideHidden.value;
|
||||
function handleToggleSidebar() {
|
||||
emit('toggleSidebar');
|
||||
}
|
||||
|
||||
function handleOpenMenu() {
|
||||
sideCollapse.value = false;
|
||||
sidebarCollapse.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex min-h-full w-full">
|
||||
<slot name="preferences"></slot>
|
||||
<slot name="floating-button-group"></slot>
|
||||
<LayoutSide
|
||||
v-if="sideVisibleState"
|
||||
v-model:collapse="sideCollapse"
|
||||
v-model:expand-on-hover="sideExpandOnHover"
|
||||
v-model:expand-on-hovering="sideExpandOnHovering"
|
||||
v-model:extra-collapse="sideExtraCollapse"
|
||||
v-model:extra-visible="sideExtraVisible"
|
||||
<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="sideExpandOnHover"
|
||||
:fixed-extra="sidebarExpandOnHover"
|
||||
:header-height="isMixedNav ? 0 : getHeaderHeight"
|
||||
:is-side-mixed="isSideMixedNav"
|
||||
:mixed-width="sideMixedWidth"
|
||||
:is-sidebar-mixed="isSidebarMixedNav"
|
||||
:mixed-width="sidebarMixedWidth"
|
||||
:padding-top="sidePaddingTop"
|
||||
:show="showSide"
|
||||
:width="getSideWidth"
|
||||
:z-index="sideZIndex"
|
||||
v-bind="sideFace"
|
||||
: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="isSideMixedNav">
|
||||
<template v-if="isSidebarMixedNav">
|
||||
<slot name="mixed-menu"></slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -500,7 +500,7 @@ function handleOpenMenu() {
|
||||
<template #extra-title>
|
||||
<slot name="side-extra-title"></slot>
|
||||
</template>
|
||||
</LayoutSide>
|
||||
</LayoutSidebar>
|
||||
|
||||
<div
|
||||
class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
|
||||
@@ -517,12 +517,11 @@ function handleOpenMenu() {
|
||||
:is-mobile="isMobile"
|
||||
:show="!fullContent && !headerHidden"
|
||||
:show-toggle-btn="showHeaderToggleButton"
|
||||
:side-hidden="sideHidden"
|
||||
:side-width="sideWidth"
|
||||
:sidebar-width="sidebarWidth"
|
||||
:width="mainStyle.width"
|
||||
:z-index="headerZIndex"
|
||||
@open-menu="handleOpenMenu"
|
||||
@toggle-menu="handleToggleMenu"
|
||||
@toggle-sidebar="handleToggleSidebar"
|
||||
>
|
||||
<template v-if="showHeaderLogo" #logo>
|
||||
<slot name="logo"></slot>
|
||||
@@ -530,12 +529,16 @@ function handleOpenMenu() {
|
||||
<slot name="header"></slot>
|
||||
</LayoutHeader>
|
||||
|
||||
<LayoutTabs v-if="tabsVisible" :height="tabsHeight" :style="tabsStyle">
|
||||
<slot name="tabs"></slot>
|
||||
<LayoutTabbar
|
||||
v-if="tabbarEnable"
|
||||
:height="tabsHeight"
|
||||
:style="tabbarStyle"
|
||||
>
|
||||
<slot name="tabbar"></slot>
|
||||
<template #toolbar>
|
||||
<slot name="tabs-toolbar"></slot>
|
||||
<slot name="tabbar-tools"></slot>
|
||||
</template>
|
||||
</LayoutTabs>
|
||||
</LayoutTabbar>
|
||||
</div>
|
||||
|
||||
<!-- </div> -->
|
||||
|
@@ -46,7 +46,7 @@
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"@vueuse/core": "^10.10.1",
|
||||
"vue": "3.4.27"
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"@vueuse/core": "^10.10.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "2.1.1",
|
||||
"radix-vue": "^1.8.3",
|
||||
|
@@ -17,6 +17,7 @@ export * from './logo';
|
||||
export * from './menu-badge';
|
||||
export * from './pin-input';
|
||||
export * from './popover';
|
||||
export * from './scrollbar';
|
||||
export * from './segmented';
|
||||
export * from './sheet';
|
||||
export * from './spinner';
|
||||
|
@@ -0,0 +1 @@
|
||||
export { default as VbenScrollbar } from './scrollbar.vue';
|
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ScrollArea } from '#/components/ui/scroll-area';
|
||||
import { cn } from '#/lib/utils';
|
||||
|
||||
interface Props {
|
||||
class?: HTMLAttributes['class'];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
class: '',
|
||||
});
|
||||
|
||||
const isAtTop = ref(true);
|
||||
const isAtBottom = ref(false);
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const scrollTop = target?.scrollTop ?? 0;
|
||||
const offsetHeight = target?.offsetHeight ?? 0;
|
||||
const scrollHeight = target?.scrollHeight ?? 0;
|
||||
isAtTop.value = scrollTop <= 0;
|
||||
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollArea
|
||||
:class="[
|
||||
cn(props.class),
|
||||
{
|
||||
// 'shadow-none': isAtTop && isAtBottom,
|
||||
// shadow: !isAtTop || !isAtBottom,
|
||||
// 'dark:shadow-white/20': !isAtTop || !isAtBottom,
|
||||
// 'shadow-inner': !isAtBottom,
|
||||
// 'dark:shadow-inner-white/20': !isAtBottom,
|
||||
},
|
||||
]"
|
||||
:on-scroll="handleScroll"
|
||||
>
|
||||
<slot></slot>
|
||||
</ScrollArea>
|
||||
</template>
|
@@ -4,7 +4,7 @@ import { computed, useSlots } from 'vue';
|
||||
import { Cross2Icon } from '@radix-icons/vue';
|
||||
|
||||
import { VbenButton, VbenIconButton } from '#/components/button';
|
||||
import { ScrollArea } from '#/components/ui/scroll-area';
|
||||
import { VbenScrollbar } from '#/components/scrollbar';
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
@@ -89,9 +89,9 @@ function handlerSubmit() {
|
||||
</SheetClose>
|
||||
</SheetHeader>
|
||||
<div class="h-full pb-16">
|
||||
<ScrollArea class="h-full">
|
||||
<VbenScrollbar class="h-full">
|
||||
<slot></slot>
|
||||
</ScrollArea>
|
||||
</VbenScrollbar>
|
||||
</div>
|
||||
<SheetFooter v-if="showFooter || slots.footer" as-child>
|
||||
<div
|
||||
|
@@ -1,3 +1 @@
|
||||
export { default as ChromeTabs } from './chrome-tabs/tabs.vue';
|
||||
export { default as TabsMore } from './tabs-more.vue';
|
||||
export { default as TabsScreen } from './tabs-screen.vue';
|
||||
|
@@ -1,3 +1,3 @@
|
||||
export { TabsMore, TabsScreen } from './components';
|
||||
export { default as TabsView } from './tabs-view.vue';
|
||||
export * from './widgets';
|
||||
export type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||
|
2
packages/@core/uikit/tabs-ui/src/widgets/index.ts
Normal file
2
packages/@core/uikit/tabs-ui/src/widgets/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as TabsToolMore } from './tool-more.vue';
|
||||
export { default as TabsToolScreen } from './tool-screen.vue';
|
Reference in New Issue
Block a user