feat: header mixed layout (#5263)

* feat: new layout header-mixed

* fix: header-mixed layout update

* feat: layout preference update

* fix: extra menus follow layout setting
This commit is contained in:
Netfan 2024-12-30 14:01:17 +08:00 committed by GitHub
parent 07c4ad05f4
commit ff8d5ca351
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 321 additions and 40 deletions

View File

@ -1,5 +1,6 @@
type LayoutType = type LayoutType =
| 'full-content' | 'full-content'
| 'header-mixed-nav'
| 'header-nav' | 'header-nav'
| 'mixed-nav' | 'mixed-nav'
| 'sidebar-mixed-nav' | 'sidebar-mixed-nav'

View File

@ -82,6 +82,10 @@ function usePreferences() {
() => appPreferences.value.layout === 'header-nav', () => appPreferences.value.layout === 'header-nav',
); );
const isHeaderMixedNav = computed(
() => appPreferences.value.layout === 'header-mixed-nav',
);
/** /**
* @zh_CN * @zh_CN
*/ */
@ -93,7 +97,12 @@ function usePreferences() {
* @zh_CN * @zh_CN
*/ */
const isSideMode = computed(() => { const isSideMode = computed(() => {
return isMixedNav.value || isSideMixedNav.value || isSideNav.value; return (
isMixedNav.value ||
isSideMixedNav.value ||
isSideNav.value ||
isHeaderMixedNav.value
);
}); });
const sidebarCollapsed = computed(() => { const sidebarCollapsed = computed(() => {
@ -214,6 +223,7 @@ function usePreferences() {
globalSearchShortcutKey, globalSearchShortcutKey,
isDark, isDark,
isFullContent, isFullContent,
isHeaderMixedNav,
isHeaderNav, isHeaderNav,
isMixedNav, isMixedNav,
isMobile, isMobile,

View File

@ -31,9 +31,17 @@ export function useLayout(props: VbenLayoutProps) {
*/ */
const isMixedNav = computed(() => currentLayout.value === 'mixed-nav'); const isMixedNav = computed(() => currentLayout.value === 'mixed-nav');
/**
*
*/
const isHeaderMixedNav = computed(
() => currentLayout.value === 'header-mixed-nav',
);
return { return {
currentLayout, currentLayout,
isFullContent, isFullContent,
isHeaderMixedNav,
isHeaderNav, isHeaderNav,
isMixedNav, isMixedNav,
isSidebarMixedNav, isSidebarMixedNav,

View File

@ -87,6 +87,7 @@ const { y: mouseY } = useMouse({ target: contentRef, type: 'client' });
const { const {
currentLayout, currentLayout,
isFullContent, isFullContent,
isHeaderMixedNav,
isHeaderNav, isHeaderNav,
isMixedNav, isMixedNav,
isSidebarMixedNav, isSidebarMixedNav,
@ -112,7 +113,9 @@ const getSideCollapseWidth = computed(() => {
const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } = const { sidebarCollapseShowTitle, sidebarMixedWidth, sideCollapseWidth } =
props; props;
return sidebarCollapseShowTitle || isSidebarMixedNav.value return sidebarCollapseShowTitle ||
isSidebarMixedNav.value ||
isHeaderMixedNav.value
? sidebarMixedWidth ? sidebarMixedWidth
: sideCollapseWidth; : sideCollapseWidth;
}); });
@ -145,12 +148,15 @@ const getSidebarWidth = computed(() => {
if ( if (
!sidebarEnableState.value || !sidebarEnableState.value ||
(sidebarHidden && !isSidebarMixedNav.value && !isMixedNav.value) (sidebarHidden &&
!isSidebarMixedNav.value &&
!isMixedNav.value &&
!isHeaderMixedNav.value)
) { ) {
return width; return width;
} }
if (isSidebarMixedNav.value && !isMobile) { if ((isHeaderMixedNav.value || isSidebarMixedNav.value) && !isMobile) {
width = sidebarMixedWidth; width = sidebarMixedWidth;
} else if (sidebarCollapse.value) { } else if (sidebarCollapse.value) {
width = isMobile ? 0 : getSideCollapseWidth.value; width = isMobile ? 0 : getSideCollapseWidth.value;
@ -176,7 +182,8 @@ const isSideMode = computed(
() => () =>
currentLayout.value === 'mixed-nav' || currentLayout.value === 'mixed-nav' ||
currentLayout.value === 'sidebar-mixed-nav' || currentLayout.value === 'sidebar-mixed-nav' ||
currentLayout.value === 'sidebar-nav', currentLayout.value === 'sidebar-nav' ||
currentLayout.value === 'header-mixed-nav',
); );
/** /**
@ -213,7 +220,7 @@ const mainStyle = computed(() => {
) { ) {
// fixed // fixed
const isSideNavEffective = const isSideNavEffective =
isSidebarMixedNav.value && (isSidebarMixedNav.value || isHeaderMixedNav.value) &&
sidebarExpandOnHover.value && sidebarExpandOnHover.value &&
sidebarExtraVisible.value; sidebarExtraVisible.value;
@ -476,7 +483,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
:extra-width="sidebarExtraWidth" :extra-width="sidebarExtraWidth"
:fixed-extra="sidebarExpandOnHover" :fixed-extra="sidebarExpandOnHover"
:header-height="isMixedNav ? 0 : headerHeight" :header-height="isMixedNav ? 0 : headerHeight"
:is-sidebar-mixed="isSidebarMixedNav" :is-sidebar-mixed="isSidebarMixedNav || isHeaderMixedNav"
:margin-top="sidebarMarginTop" :margin-top="sidebarMarginTop"
:mixed-width="sidebarMixedWidth" :mixed-width="sidebarMixedWidth"
:show="showSidebar" :show="showSidebar"
@ -489,7 +496,7 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
<slot name="logo"></slot> <slot name="logo"></slot>
</template> </template>
<template v-if="isSidebarMixedNav"> <template v-if="isSidebarMixedNav || isHeaderMixedNav">
<slot name="mixed-menu"></slot> <slot name="mixed-menu"></slot>
</template> </template>
<template v-else> <template v-else>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MenuRecordRaw } from '@vben/types'; import type { MenuRecordRaw } from '@vben/types';
import { computed, useSlots, watch } from 'vue'; import { computed, type SetupContext, useSlots, watch } from 'vue';
import { useRefresh } from '@vben/hooks'; import { useRefresh } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
@ -39,6 +39,7 @@ const {
isMixedNav, isMixedNav,
isMobile, isMobile,
isSideMixedNav, isSideMixedNav,
isHeaderMixedNav,
layout, layout,
preferencesButtonPosition, preferencesButtonPosition,
sidebarCollapsed, sidebarCollapsed,
@ -83,11 +84,16 @@ const logoCollapsed = computed(() => {
if (isHeaderNav.value || isMixedNav.value) { if (isHeaderNav.value || isMixedNav.value) {
return false; return false;
} }
return sidebarCollapsed.value || isSideMixedNav.value; return (
sidebarCollapsed.value || isSideMixedNav.value || isHeaderMixedNav.value
);
}); });
const showHeaderNav = computed(() => { const showHeaderNav = computed(() => {
return !isMobile.value && (isHeaderNav.value || isMixedNav.value); return (
!isMobile.value &&
(isHeaderNav.value || isMixedNav.value || isHeaderMixedNav.value)
);
}); });
// //
@ -108,6 +114,8 @@ const {
headerMenus, headerMenus,
sidebarActive, sidebarActive,
sidebarMenus, sidebarMenus,
mixedSidebarActive,
mixHeaderMenus,
sidebarVisible, sidebarVisible,
} = useMixedMenu(); } = useMixedMenu();
@ -154,7 +162,7 @@ watch(
// //
watch(() => preferences.app.locale, refresh, { flush: 'post' }); watch(() => preferences.app.locale, refresh, { flush: 'post' });
const slots = useSlots(); const slots: SetupContext['slots'] = useSlots();
const headerSlots = computed(() => { const headerSlots = computed(() => {
return Object.keys(slots).filter((key) => key.startsWith('header-')); return Object.keys(slots).filter((key) => key.startsWith('header-'));
}); });
@ -267,8 +275,8 @@ const headerSlots = computed(() => {
</template> </template>
<template #mixed-menu> <template #mixed-menu>
<LayoutMixedMenu <LayoutMixedMenu
:active-path="extraActiveMenu" :active-path="isHeaderMixedNav ? mixedSidebarActive : extraActiveMenu"
:menus="wrapperMenus(headerMenus, false)" :menus="wrapperMenus(mixHeaderMenus, false)"
:rounded="isMenuRounded" :rounded="isMenuRounded"
:theme="sidebarTheme" :theme="sidebarTheme"
@default-select="handleDefaultSelect" @default-select="handleDefaultSelect"

View File

@ -1,6 +1,6 @@
import type { MenuRecordRaw } from '@vben/types'; import type { MenuRecordRaw } from '@vben/types';
import { computed, ref, watch } from 'vue'; import { computed, nextTick, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
@ -17,7 +17,7 @@ function useExtraMenu() {
/** 记录当前顶级菜单下哪个子菜单最后激活 */ /** 记录当前顶级菜单下哪个子菜单最后激活 */
const defaultSubMap = new Map<string, string>(); const defaultSubMap = new Map<string, string>();
const extraRootMenus = ref<MenuRecordRaw[]>([]);
const route = useRoute(); const route = useRoute();
const extraMenus = ref<MenuRecordRaw[]>([]); const extraMenus = ref<MenuRecordRaw[]>([]);
const sidebarExtraVisible = ref<boolean>(false); const sidebarExtraVisible = ref<boolean>(false);
@ -49,11 +49,13 @@ function useExtraMenu() {
* @param menu * @param menu
* @param rootMenu * @param rootMenu
*/ */
const handleDefaultSelect = ( const handleDefaultSelect = async (
menu: MenuRecordRaw, menu: MenuRecordRaw,
rootMenu?: MenuRecordRaw, rootMenu?: MenuRecordRaw,
) => { ) => {
extraMenus.value = rootMenu?.children ?? []; await nextTick();
extraMenus.value = rootMenu?.children ?? extraRootMenus.value ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path; extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
if (preferences.sidebar.expandOnHover) { if (preferences.sidebar.expandOnHover) {
@ -65,17 +67,16 @@ function useExtraMenu() {
* *
*/ */
const handleSideMouseLeave = () => { const handleSideMouseLeave = () => {
// const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
// menus.value,
// route.path,
// );
calcExtraMenus(route.path);
if (preferences.sidebar.expandOnHover) { if (preferences.sidebar.expandOnHover) {
sidebarExtraVisible.value = extraMenus.value.length > 0;
return; return;
} }
sidebarExtraVisible.value = false; sidebarExtraVisible.value = false;
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus.value,
route.path,
);
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
extraMenus.value = rootMenu?.children ?? [];
}; };
const handleMenuMouseEnter = (menu: MenuRecordRaw) => { const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
@ -87,20 +88,36 @@ function useExtraMenu() {
} }
}; };
watch( function calcExtraMenus(path: string) {
() => route.path, const currentPath = route.meta?.activePath || path;
(path) => { const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
const currentPath = route.meta?.activePath || path; menus.value,
// if (preferences.sidebar.expandOnHover) { currentPath,
// return; );
// } if (preferences.app.layout === 'header-mixed-nav') {
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath( const subExtra = findRootMenuByPath(
menus.value, rootMenu?.children ?? [],
currentPath, currentPath,
1,
); );
extraRootMenus.value = subExtra.rootMenu?.children ?? [];
extraActiveMenu.value = subExtra.rootMenuPath ?? '';
extraMenus.value = subExtra.rootMenu?.children ?? [];
} else {
extraRootMenus.value = rootMenu?.children ?? [];
if (rootMenuPath) defaultSubMap.set(rootMenuPath, currentPath); if (rootMenuPath) defaultSubMap.set(rootMenuPath, currentPath);
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? ''; extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
extraMenus.value = rootMenu?.children ?? []; extraMenus.value = rootMenu?.children ?? [];
}
if (preferences.sidebar.expandOnHover) {
sidebarExtraVisible.value = extraMenus.value.length > 0;
}
}
watch(
() => [route.path, preferences.app.layout],
([path]) => {
calcExtraMenus(path || '');
}, },
{ immediate: true }, { immediate: true },
); );

View File

@ -15,12 +15,16 @@ function useMixedMenu() {
const route = useRoute(); const route = useRoute();
const splitSideMenus = ref<MenuRecordRaw[]>([]); const splitSideMenus = ref<MenuRecordRaw[]>([]);
const rootMenuPath = ref<string>(''); const rootMenuPath = ref<string>('');
const mixedRootMenuPath = ref<string>('');
const mixExtraMenus = ref<MenuRecordRaw[]>([]);
/** 记录当前顶级菜单下哪个子菜单最后激活 */ /** 记录当前顶级菜单下哪个子菜单最后激活 */
const defaultSubMap = new Map<string, string>(); const defaultSubMap = new Map<string, string>();
const { isMixedNav } = usePreferences(); const { isMixedNav, isHeaderMixedNav } = usePreferences();
const needSplit = computed( const needSplit = computed(
() => preferences.navigation.split && isMixedNav.value, () =>
(preferences.navigation.split && isMixedNav.value) ||
isHeaderMixedNav.value,
); );
const sidebarVisible = computed(() => { const sidebarVisible = computed(() => {
@ -54,6 +58,10 @@ function useMixedMenu() {
return needSplit.value ? splitSideMenus.value : menus.value; return needSplit.value ? splitSideMenus.value : menus.value;
}); });
const mixHeaderMenus = computed(() => {
return isHeaderMixedNav.value ? sidebarMenus.value : headerMenus.value;
});
/** /**
* *
*/ */
@ -61,6 +69,10 @@ function useMixedMenu() {
return (route?.meta?.activePath as string) ?? route.path; return (route?.meta?.activePath as string) ?? route.path;
}); });
const mixedSidebarActive = computed(() => {
return mixedRootMenuPath.value || sidebarActive.value;
});
/** /**
* *
*/ */
@ -118,6 +130,9 @@ function useMixedMenu() {
if (!rootMenu) { if (!rootMenu) {
rootMenu = menus.value.find((item) => item.path === path); rootMenu = menus.value.find((item) => item.path === path);
} }
const result = findRootMenuByPath(rootMenu?.children || [], path, 1);
mixedRootMenuPath.value = result.rootMenuPath ?? '';
mixExtraMenus.value = result.rootMenu?.children ?? [];
rootMenuPath.value = rootMenu?.path ?? ''; rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = rootMenu?.children ?? []; splitSideMenus.value = rootMenu?.children ?? [];
} }
@ -145,6 +160,9 @@ function useMixedMenu() {
headerMenus, headerMenus,
sidebarActive, sidebarActive,
sidebarMenus, sidebarMenus,
mixedSidebarActive,
mixHeaderMenus,
mixExtraMenus,
sidebarVisible, sidebarVisible,
}; };
} }

View File

@ -9,6 +9,7 @@ import { VbenTooltip } from '@vben-core/shadcn-ui';
import { import {
FullContent, FullContent,
HeaderMixedNav,
HeaderNav, HeaderNav,
MixedNav, MixedNav,
SidebarMixedNav, SidebarMixedNav,
@ -33,6 +34,7 @@ const components: Record<LayoutType, Component> = {
'mixed-nav': MixedNav, 'mixed-nav': MixedNav,
'sidebar-mixed-nav': SidebarMixedNav, 'sidebar-mixed-nav': SidebarMixedNav,
'sidebar-nav': SidebarNav, 'sidebar-nav': SidebarNav,
'header-mixed-nav': HeaderMixedNav,
}; };
const PRESET = computed((): PresetItem[] => [ const PRESET = computed((): PresetItem[] => [
@ -56,6 +58,11 @@ const PRESET = computed((): PresetItem[] => [
tip: $t('preferences.mixedMenuTip'), tip: $t('preferences.mixedMenuTip'),
type: 'mixed-nav', type: 'mixed-nav',
}, },
{
name: $t('preferences.headerTwoColumn'),
tip: $t('preferences.headerTwoColumnTip'),
type: 'header-mixed-nav',
},
{ {
name: $t('preferences.fullContent'), name: $t('preferences.fullContent'),
tip: $t('preferences.fullContentTip'), tip: $t('preferences.fullContentTip'),

View File

@ -44,7 +44,7 @@ const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
v-model="sidebarAutoActivateChild" v-model="sidebarAutoActivateChild"
:disabled=" :disabled="
!sidebarEnable || !sidebarEnable ||
!['sidebar-mixed-nav', 'mixed-nav', 'sidebar-nav'].includes( !['sidebar-mixed-nav', 'mixed-nav', 'header-mixed-nav'].includes(
currentLayout as string, currentLayout as string,
) || ) ||
disabled disabled

View File

@ -0,0 +1,202 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<path
id="svg_2"
d="m-3.37838,3.7543a1.93401,4.02457 0 0 1 1.93401,-4.02457l11.3488,0l0,66.40541l-11.3488,0a1.93401,4.02457 0 0 1 -1.93401,-4.02457l0,-58.35627z"
fill="hsl(var(--primary))"
stroke="null"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="15.46086"
/>
<rect
id="svg_4"
fill="#ffffff"
height="7.67897"
rx="2"
stroke="null"
width="8.18938"
x="0.58676"
y="1.42154"
/>
<rect
id="svg_8"
fill="hsl(var(--primary))"
height="9.07027"
rx="2"
stroke="null"
width="75.91967"
x="25.38277"
y="1.42876"
/>
<rect
id="svg_9"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="27.91529"
y="3.69284"
/>
<rect
id="svg_10"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="80.75054"
y="3.62876"
/>
<rect
id="svg_11"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="87.78868"
y="3.69981"
/>
<rect
id="svg_12"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="94.6847"
y="3.62876"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="42.9287"
x="58.75427"
y="14.613"
/>
<rect
id="svg_14"
fill="currentColor"
fill-opacity="0.08"
height="20.97838"
rx="2"
stroke="null"
width="28.36894"
x="26.14342"
y="14.613"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="75.09493"
x="26.34264"
y="39.68822"
/>
<rect
id="svg_5"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.79832"
y="28.39462"
/>
<rect
id="svg_6"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="41.80156"
/>
<rect
id="svg_7"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="55.36623"
/>
<rect
id="svg_16"
fill="currentColor"
fill-opacity="0.08"
height="65.72065"
stroke="null"
width="12.49265"
x="9.85477"
y="-0.02618"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="35.14924"
y="4.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="47.25735"
y="4.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="59.23033"
y="4.07319"
/>
</g>
</svg>
</template>

View File

@ -2,6 +2,7 @@ import HeaderNav from './header-nav.vue';
export { default as ContentCompact } from './content-compact.vue'; export { default as ContentCompact } from './content-compact.vue';
export { default as FullContent } from './full-content.vue'; export { default as FullContent } from './full-content.vue';
export { default as HeaderMixedNav } from './header-mixed-nav.vue';
export { default as MixedNav } from './mixed-nav.vue'; export { default as MixedNav } from './mixed-nav.vue';
export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue'; export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue';
export { default as SidebarNav } from './sidebar-nav.vue'; export { default as SidebarNav } from './sidebar-nav.vue';

View File

@ -17,7 +17,9 @@
"horizontalTip": "水平菜单模式,菜单全部显示在顶部", "horizontalTip": "水平菜单模式,菜单全部显示在顶部",
"twoColumn": "双列菜单", "twoColumn": "双列菜单",
"twoColumnTip": "垂直双列菜单模式", "twoColumnTip": "垂直双列菜单模式",
"mixedMenu": "混合菜单", "headerTwoColumn": "混合双列",
"headerTwoColumnTip": "双列、水平菜单共存模式",
"mixedMenu": "混合垂直",
"mixedMenuTip": "垂直水平菜单共存", "mixedMenuTip": "垂直水平菜单共存",
"fullContent": "内容全屏", "fullContent": "内容全屏",
"fullContentTip": "不显示任何菜单,只显示内容主体", "fullContentTip": "不显示任何菜单,只显示内容主体",

View File

@ -21,9 +21,9 @@ function findMenuByPath(
* @param menus * @param menus
* @param path * @param path
*/ */
function findRootMenuByPath(menus: MenuRecordRaw[], path?: string) { function findRootMenuByPath(menus: MenuRecordRaw[], path?: string, level = 0) {
const findMenu = findMenuByPath(menus, path); const findMenu = findMenuByPath(menus, path);
const rootMenuPath = findMenu?.parents?.[0]; const rootMenuPath = findMenu?.parents?.[level];
const rootMenu = rootMenuPath const rootMenu = rootMenuPath
? menus.find((item) => item.path === rootMenuPath) ? menus.find((item) => item.path === rootMenuPath)
: undefined; : undefined;