fix: layout error

This commit is contained in:
vben
2024-06-09 15:39:11 +08:00
parent 35c3dd78ec
commit 640ad6d9e7
58 changed files with 678 additions and 679 deletions

View 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();
});
});

View File

@@ -8,11 +8,9 @@ function findMenuByPath(
if (menu.path === path) {
return menu;
}
if (menu?.children?.length) {
const findMenu = findMenuByPath(menu.children, path);
if (findMenu) {
return findMenu;
}
const findMenu = menu.children && findMenuByPath(menu.children, path);
if (findMenu) {
return findMenu;
}
}
return null;

View File

@@ -1,3 +1,4 @@
export * from './find-menu-by-path';
export * from './flatten-object';
export * from './generator-menus';
export * from './generator-routes';

View File

@@ -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"
}
}

View File

@@ -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,

View File

@@ -94,9 +94,9 @@ interface NavigationPreferences {
interface SidebarPreferences {
/** 侧边栏是否折叠 */
collapse: boolean;
collapsed: boolean;
/** 侧边栏折叠时是否显示title */
collapseShowTitle: boolean;
collapsedShowTitle: boolean;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */

View File

@@ -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',
);
/**

View File

@@ -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';

View File

@@ -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"
}
}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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> -->

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -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';

View File

@@ -0,0 +1 @@
export { default as VbenScrollbar } from './scrollbar.vue';

View File

@@ -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>

View File

@@ -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

View File

@@ -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';

View File

@@ -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';

View File

@@ -0,0 +1,2 @@
export { default as TabsToolMore } from './tool-more.vue';
export { default as TabsToolScreen } from './tool-screen.vue';

View File

@@ -50,8 +50,8 @@
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben/locales": "workspace:*",
"@vueuse/core": "^10.10.0",
"@vueuse/integrations": "^10.10.0",
"@vueuse/core": "^10.10.1",
"@vueuse/integrations": "^10.10.1",
"qrcode": "^1.5.3",
"vue": "3.4.27",
"vue-router": "^4.3.3"

View File

@@ -6,7 +6,7 @@ import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IcRoundClose, IcRoundSearchOff } from '@vben-core/iconify';
import { ScrollArea, VbenIcon } from '@vben-core/shadcn-ui';
import { VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { mapTree, traverseTreeValues } from '@vben-core/toolkit';
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';
@@ -214,7 +214,7 @@ onMounted(() => {
</script>
<template>
<ScrollArea>
<VbenScrollbar>
<div class="!flex h-full justify-center px-4 sm:max-h-[450px]">
<!-- 无搜索结果 -->
<div
@@ -275,5 +275,5 @@ onMounted(() => {
</li>
</ul>
</div>
</ScrollArea>
</VbenScrollbar>
</template>

View File

@@ -7,10 +7,10 @@ import {
IcRoundNotificationsNone,
} from '@vben-core/iconify';
import {
ScrollArea,
VbenButton,
VbenIconButton,
VbenPopover,
VbenScrollbar,
} from '@vben-core/shadcn-ui';
import { useToggle } from '@vueuse/core';
@@ -81,7 +81,7 @@ function handleClick(item: NotificationItem) {
</template>
<div class="relative">
<div class="flex items-center justify-between border-b p-4 py-3">
<div class="flex items-center justify-between p-4 py-3">
<div class="text-foreground">{{ $t('widgets.notifications') }}</div>
<VbenIconButton
:tooltip="$t('widgets.make-all-as-read')"
@@ -90,11 +90,11 @@ function handleClick(item: NotificationItem) {
<IcRoundMarkEmailRead />
</VbenIconButton>
</div>
<ScrollArea v-if="notifications.length > 0">
<VbenScrollbar v-if="notifications.length > 0">
<ul class="!flex max-h-[360px] w-full flex-col">
<template v-for="item in notifications" :key="item.title">
<li
class="hover:bg-accent relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
class="hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
@click="handleClick(item)"
>
<span
@@ -123,7 +123,7 @@ function handleClick(item: NotificationItem) {
</li>
</template>
</ul>
</ScrollArea>
</VbenScrollbar>
<template v-else>
<div class="flex-center text-muted-foreground min-h-[150px] w-full">
@@ -131,7 +131,9 @@ function handleClick(item: NotificationItem) {
</div>
</template>
<div class="flex items-center justify-between border-t px-4 py-3">
<div
class="border-border flex items-center justify-between border-t px-4 py-3"
>
<VbenButton size="sm" variant="ghost" @click="handleClear">
{{ $t('widgets.clear-notifications') }}
</VbenButton>

View File

@@ -11,8 +11,8 @@ import {
FullContent,
HeaderNav,
MixedNav,
SideMixedNav,
SideNav,
SidebarMixedNav,
SidebarNav,
} from '../../icons';
interface PresetItem {
@@ -25,26 +25,26 @@ defineOptions({
name: 'PreferenceLayout',
});
const modelValue = defineModel<LayoutType>({ default: 'side-nav' });
const modelValue = defineModel<LayoutType>({ default: 'sidebar-nav' });
const components: Record<LayoutType, Component> = {
'full-content': FullContent,
'header-nav': HeaderNav,
'mixed-nav': MixedNav,
'side-mixed-nav': SideMixedNav,
'side-nav': SideNav,
'sidebar-mixed-nav': SidebarMixedNav,
'sidebar-nav': SidebarNav,
};
const PRESET = computed((): PresetItem[] => [
{
name: $t('preference.vertical'),
tip: $t('preference.vertical-tip'),
type: 'side-nav',
type: 'sidebar-nav',
},
{
name: $t('preference.two-column'),
tip: $t('preference.two-column-tip'),
type: 'side-mixed-nav',
type: 'sidebar-mixed-nav',
},
{
name: $t('preference.horizontal'),

View File

@@ -10,21 +10,21 @@ defineOptions({
defineProps<{ disabled: boolean }>();
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarCollapseShowTitle = defineModel<boolean>(
'sidebarCollapseShowTitle',
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
const sidebarCollapse = defineModel<boolean>('sidebarCollapse');
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
</script>
<template>
<SwitchItem v-model="sidebarEnable" :disabled="disabled">
{{ $t('preference.side-visible') }}
</SwitchItem>
<SwitchItem v-model="sidebarCollapse" :disabled="!sidebarEnable || disabled">
<SwitchItem v-model="sidebarCollapsed" :disabled="!sidebarEnable || disabled">
{{ $t('preference.collapse') }}
</SwitchItem>
<SwitchItem
v-model="sidebarCollapseShowTitle"
v-model="sidebarCollapsedShowTitle"
:disabled="!sidebarEnable || disabled"
>
{{ $t('preference.collapse-show-title') }}

View File

@@ -3,8 +3,8 @@ import HeaderNav from './header-nav.vue';
export { default as ContentCompact } from './content-compact.vue';
export { default as FullContent } from './full-content.vue';
export { default as MixedNav } from './mixed-nav.vue';
export { default as SideMixedNav } from './side-mixed-nav.vue';
export { default as SideNav } from './side-nav.vue';
export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue';
export { default as SidebarNav } from './sidebar-nav.vue';
const ContentWide = HeaderNav;
export { ContentWide, HeaderNav };

View File

@@ -22,7 +22,7 @@ import Preferences from './preferences.vue';
:breadcrumb-hide-only-one="preferences.breadcrumb.hideOnlyOne"
:breadcrumb-home="preferences.breadcrumb.showHome"
:breadcrumb-icon="preferences.breadcrumb.showIcon"
:breadcrumb-style="preferences.breadcrumb.styleType"
:breadcrumb-style-type="preferences.breadcrumb.styleType"
:color-primary-presets="COLOR_PRIMARY_RESETS"
:footer-enable="preferences.footer.enable"
:footer-fixed="preferences.footer.fixed"
@@ -32,8 +32,8 @@ import Preferences from './preferences.vue';
:navigation-split="preferences.navigation.split"
:navigation-style-type="preferences.navigation.styleType"
:shortcut-keys-enable="preferences.shortcutKeys.enable"
:sidebar-collapse="preferences.sidebar.collapse"
:sidebar-collapse-show-title="preferences.sidebar.collapseShowTitle"
:sidebar-collapsed="preferences.sidebar.collapsed"
:sidebar-collapsed-show-title="preferences.sidebar.collapsedShowTitle"
:sidebar-enable="preferences.sidebar.enable"
:tabbar-enable="preferences.tabbar.enable"
:tabbar-show-icon="preferences.tabbar.showIcon"
@@ -103,11 +103,11 @@ import Preferences from './preferences.vue';
@update:shortcut-keys-enable="
(val) => updatePreferences({ shortcutKeys: { enable: val } })
"
@update:sidebar-collapse="
(val) => updatePreferences({ sidebar: { collapse: val } })
@update:sidebar-collapsed="
(val) => updatePreferences({ sidebar: { collapsed: val } })
"
@update:sidebar-collapse-show-title="
(val) => updatePreferences({ sidebar: { collapseShowTitle: val } })
@update:sidebar-collapsed-show-title="
(val) => updatePreferences({ sidebar: { collapsedShowTitle: val } })
"
@update:sidebar-enable="
(val) => updatePreferences({ sidebar: { enable: val } })

View File

@@ -70,9 +70,9 @@ const transitionEnable = defineModel<boolean>('transitionEnable');
const themeColorPrimary = defineModel<string>('themeColorPrimary');
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarCollapse = defineModel<boolean>('sidebarCollapse');
const sidebarCollapseShowTitle = defineModel<boolean>(
'sidebarCollapseShowTitle',
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
const headerEnable = defineModel<boolean>('headerEnable');
@@ -216,8 +216,8 @@ function handleReset() {
<Block :title="$t('preference.sidebar')">
<Sidebar
v-model:side-collapse-show-title="sidebarCollapseShowTitle"
v-model:sidebar-collapse="sidebarCollapse"
v-model:sidebar-collapsed="sidebarCollapsed"
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
v-model:sidebar-enable="sidebarEnable"
:disabled="!isSideMode"
/>

View File

@@ -41,6 +41,7 @@
}
},
"dependencies": {
"@vben-core/helpers": "workspace:*",
"@vben-core/iconify": "workspace:*",
"@vben-core/layout-ui": "workspace:*",
"@vben-core/menu-ui": "workspace:*",

View File

@@ -9,11 +9,7 @@ import {
updatePreferences,
usePreferences,
} from '@vben-core/preferences';
import {
VbenBackTop,
// VbenFloatingButtonGroup,
VbenLogo,
} from '@vben-core/shadcn-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { mapTree } from '@vben-core/toolkit';
import { MenuRecordRaw } from '@vben-core/typings';
@@ -27,7 +23,7 @@ import {
useExtraMenu,
useMixedMenu,
} from './menu';
import { LayoutTabs, LayoutTabsToolbar } from './tabs';
import { LayoutTabbar, LayoutTabbarTools } from './tabbar';
import { Breadcrumb } from './widgets';
defineOptions({ name: 'BasicLayout' });
@@ -45,8 +41,8 @@ const theme = computed(() => {
});
const logoClass = computed(() => {
const { collapse, collapseShowTitle } = preferences.sidebar;
return collapseShowTitle && collapse && !isMixedNav.value ? 'mx-auto' : '';
const { collapsed, collapsedShowTitle } = preferences.sidebar;
return collapsedShowTitle && collapsed && !isMixedNav.value ? 'mx-auto' : '';
});
const isMenuRounded = computed(() => {
@@ -59,12 +55,12 @@ const logoCollapse = computed(() => {
}
const { isMobile } = preferences.app;
const { collapse } = preferences.sidebar;
const { collapsed } = preferences.sidebar;
if (!collapse && isMobile) {
if (!collapsed && isMobile) {
return false;
}
return collapse || isSideMixedNav.value;
return collapsed || isSideMixedNav.value;
});
const showHeaderNav = computed(() => {
@@ -74,11 +70,11 @@ const showHeaderNav = computed(() => {
const {
extraActiveMenu,
extraMenus,
extraVisible,
handleDefaultSelect,
handleMenuMouseEnter,
handleMixedMenuSelect,
handleSideMouseLeave,
sidebarExtraVisible,
} = useExtraMenu();
const {
@@ -92,17 +88,22 @@ const {
function wrapperMenus(menus: MenuRecordRaw[]) {
return mapTree(menus, (item) => {
return {
...item,
name: $t(item.name),
};
return { ...item, name: $t(item.name) };
});
}
function toggleSidebar() {
updatePreferences({
sidebar: {
hidden: !preferences.sidebar.hidden,
},
});
}
</script>
<template>
<VbenAdminLayout
v-model:side-extra-visible="extraVisible"
v-model:sidebar-extra-visible="sidebarExtraVisible"
:content-compact="preferences.app.contentCompact"
:footer-enable="preferences.footer.enable"
:footer-fixed="preferences.footer.fixed"
@@ -111,37 +112,38 @@ function wrapperMenus(menus: MenuRecordRaw[]) {
:header-visible="preferences.header.enable"
:is-mobile="preferences.app.isMobile"
:layout="layout"
:side-collapse="preferences.sidebar.collapse"
:side-collapse-show-title="preferences.sidebar.collapseShowTitle"
:side-expand-on-hover="preferences.sidebar.expandOnHover"
:side-extra-collapse="preferences.sidebar.extraCollapse"
:side-hidden="preferences.sidebar.hidden"
:side-semi-dark="preferences.app.semiDarkMenu"
:side-theme="theme"
:side-visible="sideVisible"
:side-width="preferences.sidebar.width"
:tabs-visible="preferences.tabbar.enable"
:sidebar-collapse="preferences.sidebar.collapsed"
:sidebar-collapse-show-title="preferences.sidebar.collapsedShowTitle"
:sidebar-enable="sideVisible"
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
:sidebar-hidden="preferences.sidebar.hidden"
:sidebar-semi-dark="preferences.app.semiDarkMenu"
:sidebar-theme="theme"
:sidebar-width="preferences.sidebar.width"
:tabbar-enable="preferences.tabbar.enable"
@side-mouse-leave="handleSideMouseLeave"
@update:side-collapse="
(value: boolean) => updatePreferences({ sidebar: { collapse: value } })
@toggle-sidebar="toggleSidebar"
@update:sidebar-collapse="
(value: boolean) => updatePreferences({ sidebar: { collapsed: value } })
"
@update:side-expand-on-hover="
@update:sidebar-enable="
(value: boolean) => updatePreferences({ sidebar: { enable: value } })
"
@update:sidebar-expand-on-hover="
(value: boolean) =>
updatePreferences({ sidebar: { expandOnHover: value } })
"
@update:side-extra-collapse="
@update:sidebar-extra-collapse="
(value: boolean) =>
updatePreferences({ sidebar: { extraCollapse: value } })
"
@update:side-visible="
(value: boolean) => updatePreferences({ sidebar: { enable: value } })
"
>
<template v-if="preferences.app.showPreference" #preferences>
<PreferencesWidget />
</template>
<template #floating-button-group>
<template #floating-groups>
<VbenBackTop />
<!-- <VbenFloatingButtonGroup /> -->
</template>
@@ -194,8 +196,8 @@ function wrapperMenus(menus: MenuRecordRaw[]) {
<template #menu>
<LayoutMenu
:accordion="preferences.navigation.accordion"
:collapse="preferences.sidebar.collapse"
:collapse-show-title="preferences.sidebar.collapseShowTitle"
:collapse="preferences.sidebar.collapsed"
:collapse-show-title="preferences.sidebar.collapsedShowTitle"
:default-active="sideActive"
:menus="wrapperMenus(sideMenus)"
:rounded="isMenuRounded"
@@ -207,7 +209,8 @@ function wrapperMenus(menus: MenuRecordRaw[]) {
<template #mixed-menu>
<LayoutMixedMenu
:active-path="extraActiveMenu"
:collapse="!preferences.sidebar.collapseShowTitle"
:collapse="!preferences.sidebar.collapsedShowTitle"
:menus="wrapperMenus(headerMenus)"
:rounded="isMenuRounded"
:theme="theme"
@default-select="handleDefaultSelect"
@@ -234,14 +237,14 @@ function wrapperMenus(menus: MenuRecordRaw[]) {
/>
</template>
<template #tabs>
<LayoutTabs
<template #tabbar>
<LayoutTabbar
v-if="preferences.tabbar.enable"
:show-icon="preferences.tabbar.showIcon"
/>
</template>
<template #tabs-toolbar>
<LayoutTabsToolbar v-if="preferences.tabbar.enable" />
<template #tabbar-tools>
<LayoutTabbarTools v-if="preferences.tabbar.enable" />
</template>
<!-- 主体内容 -->

View File

@@ -2,17 +2,15 @@
import type { NormalMenuProps } from '@vben-core/menu-ui';
import type { MenuRecordRaw } from '@vben-core/typings';
import { computed, onBeforeMount } from 'vue';
import { onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { findMenuByPath } from '@vben-core/helpers';
import { NormalMenu } from '@vben-core/menu-ui';
import { useAccessStore } from '@vben-core/stores';
import { findMenuByPath } from './helper';
interface Props extends NormalMenuProps {}
defineProps<Props>();
const props = defineProps<Props>();
const emit = defineEmits<{
defaultSelect: [MenuRecordRaw, MenuRecordRaw?];
@@ -20,19 +18,16 @@ const emit = defineEmits<{
select: [MenuRecordRaw];
}>();
const accessStore = useAccessStore();
const route = useRoute();
const menus = computed(() => accessStore.getAccessMenus);
function handleSelect(menu: MenuRecordRaw) {
emit('select', menu);
}
onBeforeMount(() => {
const menu = findMenuByPath(menus.value, route.path);
const menu = findMenuByPath(props.menus || [], route.path);
if (menu) {
const rootMenu = menus.value.find(
const rootMenu = (props.menus || []).find(
(item) => item.path === menu.parents?.[0],
);
emit('defaultSelect', menu, rootMenu);

View File

@@ -3,10 +3,10 @@ import type { MenuRecordRaw } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { findRootMenuByPath } from '@vben-core/helpers';
import { preferences } from '@vben-core/preferences';
import { useAccessStore } from '@vben-core/stores';
import { findRootMenuByPath } from './helper';
import { useNavigation } from './use-navigation';
function useExtraMenu() {
@@ -17,7 +17,7 @@ function useExtraMenu() {
const route = useRoute();
const extraMenus = ref<MenuRecordRaw[]>([]);
const extraVisible = ref<boolean>(false);
const sidebarExtraVisible = ref<boolean>(false);
const extraActiveMenu = ref('');
/**
@@ -29,7 +29,7 @@ function useExtraMenu() {
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
const hasChildren = extraMenus.value.length > 0;
extraVisible.value = hasChildren;
sidebarExtraVisible.value = hasChildren;
if (!hasChildren) {
await navigation(menu.path);
}
@@ -48,7 +48,7 @@ function useExtraMenu() {
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
if (preferences.sidebar.expandOnHover) {
extraVisible.value = extraMenus.value.length > 0;
sidebarExtraVisible.value = extraMenus.value.length > 0;
}
};
@@ -59,7 +59,7 @@ function useExtraMenu() {
if (preferences.sidebar.expandOnHover) {
return;
}
extraVisible.value = false;
sidebarExtraVisible.value = false;
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus.value,
@@ -74,18 +74,18 @@ function useExtraMenu() {
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
extraMenus.value = findMenu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
extraVisible.value = extraMenus.value.length > 0;
sidebarExtraVisible.value = extraMenus.value.length > 0;
}
};
return {
extraActiveMenu,
extraMenus,
extraVisible,
handleDefaultSelect,
handleMenuMouseEnter,
handleMixedMenuSelect,
handleSideMouseLeave,
sidebarExtraVisible,
};
}

View File

@@ -3,10 +3,10 @@ import type { MenuRecordRaw } from '@vben-core/typings';
import { computed, onBeforeMount, ref } from 'vue';
import { useRoute } from 'vue-router';
import { findRootMenuByPath } from '@vben-core/helpers';
import { preferences, usePreferences } from '@vben-core/preferences';
import { useAccessStore } from '@vben-core/stores';
import { findRootMenuByPath } from './helper';
import { useNavigation } from './use-navigation';
function useMixedMenu() {

View File

@@ -0,0 +1,3 @@
export { default as LayoutTabbar } from './tabbar.vue';
export { default as LayoutTabbarTools } from './tabbar-tools.vue';
export * from './use-tabs';

View File

@@ -3,7 +3,7 @@ import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { preferences, updatePreferences } from '@vben-core/preferences';
import { TabsMore, TabsScreen } from '@vben-core/tabs-ui';
import { TabsToolMore, TabsToolScreen } from '@vben-core/tabs-ui';
import { useTabs } from './use-tabs';
@@ -29,8 +29,8 @@ function handleScreenChange(screen: boolean) {
<template>
<div class="flex-center h-full">
<TabsMore :menus="menus" />
<TabsScreen
<TabsToolMore :menus="menus" />
<TabsToolScreen
:screen="preferences.sidebar.hidden"
@change="handleScreenChange"
@update:screen="handleScreenChange"

View File

@@ -4,7 +4,7 @@ import { TabsView } from '@vben-core/tabs-ui';
import { useTabs } from './use-tabs';
defineOptions({
name: 'LayoutTabs',
name: 'LayoutTabbar',
});
defineProps<{ showIcon?: boolean }>();

View File

@@ -1,3 +0,0 @@
export { default as LayoutTabs } from './tabs.vue';
export { default as LayoutTabsToolbar } from './tabs-toolbar.vue';
export * from './use-tabs';