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 =
| 'full-content'
| 'header-mixed-nav'
| 'header-nav'
| 'mixed-nav'
| 'sidebar-mixed-nav'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@ const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
v-model="sidebarAutoActivateChild"
:disabled="
!sidebarEnable ||
!['sidebar-mixed-nav', 'mixed-nav', 'sidebar-nav'].includes(
!['sidebar-mixed-nav', 'mixed-nav', 'header-mixed-nav'].includes(
currentLayout as string,
) ||
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 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 SidebarMixedNav } from './sidebar-mixed-nav.vue';
export { default as SidebarNav } from './sidebar-nav.vue';

View File

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

View File

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