feat(projects): add 'vertical-hybrid-header-first' layout mode

This commit is contained in:
wenyuan 2025-07-12 23:35:45 +08:00 committed by Soybean
parent b6ac3106ce
commit b4e5c6d990
15 changed files with 310 additions and 94 deletions

View File

@ -23,6 +23,7 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = { export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
vertical: 'theme.layout.layoutMode.vertical', vertical: 'theme.layout.layoutMode.vertical',
'vertical-mix': 'theme.layout.layoutMode.vertical-mix', 'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
horizontal: 'theme.layout.layoutMode.horizontal', horizontal: 'theme.layout.layoutMode.horizontal',
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first', 'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first' 'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'

View File

@ -42,6 +42,11 @@ const headerProps = computed(() => {
showMenu: false, showMenu: false,
showMenuToggler: false showMenuToggler: false
}, },
'vertical-hybrid-header-first': {
showLogo: !isActiveFirstLevelMenuHasChildren.value,
showMenu: true,
showMenuToggler: false
},
horizontal: { horizontal: {
showLogo: true, showLogo: true,
showMenu: true, showMenu: true,
@ -66,6 +71,8 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix'); const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first');
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first'); const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first'); const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
@ -74,36 +81,46 @@ const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth()); const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderWidth() { function getSiderAndCollapsedWidth(isCollapsed: boolean) {
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider; const {
mixChildMenuWidth,
collapsedWidth,
width: themeWidth,
mixCollapsedWidth,
mixWidth: themeMixWidth
} = themeStore.sider;
const width = isCollapsed ? collapsedWidth : themeWidth;
const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
if (isTopHybridHeaderFirst.value) { if (isTopHybridHeaderFirst.value) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0; return isActiveFirstLevelMenuHasChildren.value ? width : 0;
} }
let w = isVerticalMix.value || isTopHybridSidebarFirst.value ? mixWidth : width; if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
return 0;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
} }
return w; const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
let finalWidth = isMixMode ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth;
}
if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth;
}
return finalWidth;
}
function getSiderWidth() {
return getSiderAndCollapsedWidth(false);
} }
function getSiderCollapsedWidth() { function getSiderCollapsedWidth() {
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider; return getSiderAndCollapsedWidth(true);
if (isTopHybridHeaderFirst.value) {
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
}
let w = isVerticalMix.value || isTopHybridSidebarFirst.value ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
return w;
} }
</script> </script>

View File

@ -1,7 +1,9 @@
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useContext } from '@sa/hooks'; import { useContext } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu); export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
@ -9,6 +11,17 @@ function useMixMenu() {
const route = useRoute(); const route = useRoute();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const activeFirstLevelMenuKey = ref(''); const activeFirstLevelMenuKey = ref('');
@ -22,20 +35,6 @@ function useMixMenu() {
setActiveFirstLevelMenuKey(firstLevelRouteName); setActiveFirstLevelMenuKey(firstLevelRouteName);
} }
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const childLevelMenus = computed<App.Global.Menu[]>(
() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const isActiveFirstLevelMenuHasChildren = computed(() => { const isActiveFirstLevelMenuHasChildren = computed(() => {
if (!activeFirstLevelMenuKey.value) { if (!activeFirstLevelMenuKey.value) {
return false; return false;
@ -46,6 +45,54 @@ function useMixMenu() {
return Boolean(findItem?.children?.length); return Boolean(findItem?.children?.length);
}); });
function handleSelectFirstLevelMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const secondLevelMenus = computed<App.Global.Menu[]>(
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const activeSecondLevelMenuKey = ref('');
function setActiveSecondLevelMenuKey(key: string) {
activeSecondLevelMenuKey.value = key;
}
function getActiveSecondLevelMenuKey() {
const [firstLevelRouteName, level2SuffixName] = selectedKey.value.split('_');
const secondLevelRouteName = `${firstLevelRouteName}_${level2SuffixName}`;
setActiveSecondLevelMenuKey(secondLevelRouteName);
}
const isActiveSecondLevelMenuHasChildren = computed(() => {
if (!activeSecondLevelMenuKey.value) {
return false;
}
const findItem = secondLevelMenus.value.find(item => item.key === activeSecondLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
function handleSelectSecondLevelMenu(key: RouteKey) {
setActiveSecondLevelMenuKey(key);
if (!isActiveSecondLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const childLevelMenus = computed<App.Global.Menu[]>(
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
);
watch( watch(
() => route.name, () => route.name,
() => { () => {
@ -55,13 +102,19 @@ function useMixMenu() {
); );
return { return {
allMenus,
firstLevelMenus, firstLevelMenus,
childLevelMenus,
isActiveFirstLevelMenuHasChildren,
activeFirstLevelMenuKey, activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey, setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey isActiveFirstLevelMenuHasChildren,
handleSelectFirstLevelMenu,
getActiveFirstLevelMenuKey,
secondLevelMenus,
activeSecondLevelMenuKey,
setActiveSecondLevelMenuKey,
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
}; };
} }

View File

@ -3,6 +3,7 @@ import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core'; import { createReusableTemplate } from '@vueuse/core';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { transformColorWithOpacity } from '@sa/color'; import { transformColorWithOpacity } from '@sa/color';
import type { RouteKey } from '@elegant-router/types';
defineOptions({ defineOptions({
name: 'FirstLevelMenu' name: 'FirstLevelMenu'
@ -20,7 +21,7 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
interface Emits { interface Emits {
(e: 'select', menu: App.Global.Menu): boolean; (e: 'select', menuKey: RouteKey): boolean;
(e: 'toggleSiderCollapse'): void; (e: 'toggleSiderCollapse'): void;
} }
@ -47,8 +48,8 @@ const selectedBgColor = computed(() => {
return darkMode ? dark : light; return darkMode ? dark : light;
}); });
function handleClickMixMenu(menu: App.Global.Menu) { function handleClickMixMenu(menuKey: RouteKey) {
emit('select', menu); emit('select', menuKey);
} }
function toggleSiderCollapse() { function toggleSiderCollapse() {
@ -88,7 +89,7 @@ function toggleSiderCollapse() {
:icon="menu.icon" :icon="menu.icon"
:active="menu.key === activeMenuKey" :active="menu.key === activeMenuKey"
:is-mini="siderCollapse" :is-mini="siderCollapse"
@click="handleClickMixMenu(menu)" @click="handleClickMixMenu(menu.routeKey)"
/> />
</SimpleScrollbar> </SimpleScrollbar>
<MenuToggler <MenuToggler

View File

@ -5,6 +5,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import VerticalMenu from './modules/vertical-menu.vue'; import VerticalMenu from './modules/vertical-menu.vue';
import VerticalMixMenu from './modules/vertical-mix-menu.vue'; import VerticalMixMenu from './modules/vertical-mix-menu.vue';
import VerticalHybridHeaderFirst from './modules/vertical-hybrid-header-first.vue';
import HorizontalMenu from './modules/horizontal-menu.vue'; import HorizontalMenu from './modules/horizontal-menu.vue';
import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue'; import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue'; import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
@ -20,6 +21,7 @@ const activeMenu = computed(() => {
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = { const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
vertical: VerticalMenu, vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu, 'vertical-mix': VerticalMixMenu,
'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
horizontal: HorizontalMenu, horizontal: HorizontalMenu,
'top-hybrid-sidebar-first': TopHybridSidebarFirst, 'top-hybrid-sidebar-first': TopHybridSidebarFirst,
'top-hybrid-header-first': TopHybridHeaderFirst 'top-hybrid-header-first': TopHybridHeaderFirst

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
@ -11,7 +10,7 @@ import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../../../context'; import { useMenu, useMixMenuContext } from '../../../context';
defineOptions({ defineOptions({
name: 'ReversedHorizontalMixMenu' name: 'TopHybridHeaderFirst'
}); });
const route = useRoute(); const route = useRoute();
@ -19,23 +18,9 @@ const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
firstLevelMenus,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren
} = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
function handleSelectMixMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const expandedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() { function updateExpandedKeys() {
@ -63,7 +48,7 @@ watch(
:options="firstLevelMenus" :options="firstLevelMenus"
:indent="18" :indent="18"
responsive responsive
@update:value="handleSelectMixMenu" @update:value="handleSelectFirstLevelMenu"
/> />
</Teleport> </Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
@ -75,7 +60,7 @@ watch(
:collapsed="appStore.siderCollapse" :collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth" :collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:options="childLevelMenus" :options="secondLevelMenus"
:indent="18" :indent="18"
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"
/> />

View File

@ -7,22 +7,14 @@ import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMenu, useMixMenuContext } from '../../../context'; import { useMenu, useMixMenuContext } from '../../../context';
defineOptions({ defineOptions({
name: 'HorizontalMixMenu' name: 'TopHybridSidebarFirst'
}); });
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext(); const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
</script> </script>
<template> <template>
@ -30,7 +22,7 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
<NMenu <NMenu
mode="horizontal" mode="horizontal"
:value="selectedKey" :value="selectedKey"
:options="childLevelMenus" :options="secondLevelMenus"
:indent="18" :indent="18"
responsive responsive
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"
@ -38,12 +30,12 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
</Teleport> </Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<FirstLevelMenu <FirstLevelMenu
:menus="allMenus" :menus="firstLevelMenus"
:active-menu-key="activeFirstLevelMenuKey" :active-menu-key="activeFirstLevelMenuKey"
:sider-collapse="appStore.siderCollapse" :sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode" :dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor" :theme-color="themeStore.themeColor"
@select="handleSelectMixMenu" @select="handleSelectFirstLevelMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse" @toggle-sider-collapse="appStore.toggleSiderCollapse"
/> />
</Teleport> </Teleport>

View File

@ -0,0 +1,149 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../../../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
defineOptions({
name: 'VerticalHybridHeaderFirst'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const {
firstLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
getActiveFirstLevelMenuKey,
secondLevelMenus,
activeSecondLevelMenuKey,
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
} = useMixMenuContext();
const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(key: RouteKey) {
handleSelectSecondLevelMenu(key);
if (isActiveSecondLevelMenuHasChildren.value) {
setDrawerVisible(true);
}
}
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);
if (secondLevelMenus.value.length > 0) {
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
}
}
function handleResetActiveMenu() {
setDrawerVisible(false);
if (!appStore.mixSiderFixed) {
getActiveFirstLevelMenuKey();
getActiveSecondLevelMenuKey();
}
}
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="activeFirstLevelMenuKey"
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu
:menus="secondLevelMenus"
:active-menu-key="activeSecondLevelMenuKey"
:inverted="inverted"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
>
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="inverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:options="childLevelMenus"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</DarkModeContainer>
</div>
</div>
</Teleport>
</template>
<style scoped></style>

View File

@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks'; import { useBoolean } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
@ -24,28 +25,26 @@ const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean(); const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const { const {
allMenus, firstLevelMenus,
childLevelMenus, secondLevelMenus,
activeFirstLevelMenuKey, activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey, isActiveFirstLevelMenuHasChildren,
getActiveFirstLevelMenuKey getActiveFirstLevelMenuKey,
// handleSelectFirstLevelMenu
} = useMixMenuContext(); } = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted); const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0); const hasChildMenus = computed(() => secondLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed)); const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(menu: App.Global.Menu) { function handleSelectMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(menu.key); handleSelectFirstLevelMenu(key);
if (menu.children?.length) { if (isActiveFirstLevelMenuHasChildren.value) {
setDrawerVisible(true); setDrawerVisible(true);
} else {
routerPushByKeyWithMetaQuery(menu.routeKey);
} }
} }
@ -80,13 +79,13 @@ watch(
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu"> <div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu <FirstLevelMenu
:menus="allMenus" :menus="firstLevelMenus"
:active-menu-key="activeFirstLevelMenuKey" :active-menu-key="activeFirstLevelMenuKey"
:inverted="inverted" :inverted="inverted"
:sider-collapse="appStore.siderCollapse" :sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode" :dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor" :theme-color="themeStore.themeColor"
@select="handleSelectMixMenu" @select="handleSelectMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse" @toggle-sider-collapse="appStore.toggleSiderCollapse"
> >
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" /> <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
@ -113,7 +112,7 @@ watch(
v-model:expanded-keys="expandedKeys" v-model:expanded-keys="expandedKeys"
mode="vertical" mode="vertical"
:value="selectedKey" :value="selectedKey"
:options="childLevelMenus" :options="secondLevelMenus"
:inverted="inverted" :inverted="inverted"
:indent="18" :indent="18"
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"

View File

@ -12,16 +12,13 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first'); const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first'); const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
const darkMenu = computed( const darkMenu = computed(
() => () =>
!themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted !themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
); );
const showLogo = computed( const showLogo = computed(() => themeStore.layout.mode === 'vertical');
() => !isVerticalMix.value && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value
);
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full')); const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
</script> </script>

View File

@ -43,6 +43,11 @@ const layoutConfig: LayoutConfig = {
menuClass: 'w-1/4 h-full', menuClass: 'w-1/4 h-full',
mainClass: 'w-2/3 h-3/4' mainClass: 'w-2/3 h-3/4'
}, },
'vertical-hybrid-header-first': {
placement: 'bottom',
menuClass: 'w-1/4 h-full',
mainClass: 'w-2/3 h-3/4'
},
horizontal: { horizontal: {
placement: 'bottom', placement: 'bottom',
menuClass: 'w-full h-1/4', menuClass: 'w-full h-1/4',

View File

@ -30,6 +30,14 @@ const themeStore = useThemeStore();
<div class="layout-main"></div> <div class="layout-main"></div>
</div> </div>
</template> </template>
<template #vertical-hybrid-header-first>
<div class="layout-sider h-full w-8px !bg-primary"></div>
<div class="layout-sider h-full w-16px !bg-primary-300"></div>
<div class="vertical-wrapper">
<div class="layout-header bg-primary"></div>
<div class="layout-main"></div>
</div>
</template>
<template #horizontal> <template #horizontal>
<div class="layout-header !bg-primary"></div> <div class="layout-header !bg-primary"></div>
<div class="horizontal-wrapper"> <div class="horizontal-wrapper">

View File

@ -91,11 +91,14 @@ const local: App.I18n.Schema = {
vertical: 'Vertical Mode', vertical: 'Vertical Mode',
horizontal: 'Horizontal Mode', horizontal: 'Horizontal Mode',
'vertical-mix': 'Vertical Mix Mode', 'vertical-mix': 'Vertical Mix Mode',
'vertical-hybrid-header-first': 'Left Hybrid Header-First',
'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First', 'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First',
'top-hybrid-header-first': 'Top-Hybrid Header-First', 'top-hybrid-header-first': 'Top-Hybrid Header-First',
vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.', vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.',
'vertical-mix_detail': 'vertical-mix_detail':
'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter right side.', 'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter left side.',
'vertical-hybrid-header-first_detail':
'Vertical mix-menu layout, with the primary menu at the top, the secondary menu on the dark left side, and the secondary menu on the lighter left side.',
horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.', horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.',
'top-hybrid-sidebar-first_detail': 'top-hybrid-sidebar-first_detail':
'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.', 'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',

View File

@ -90,11 +90,14 @@ const local: App.I18n.Schema = {
title: '布局模式', title: '布局模式',
vertical: '左侧菜单模式', vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式', 'vertical-mix': '左侧菜单混合模式',
'vertical-hybrid-header-first': '左侧混合-顶部优先',
horizontal: '顶部菜单模式', horizontal: '顶部菜单模式',
'top-hybrid-sidebar-first': '顶部混合-侧边优先', 'top-hybrid-sidebar-first': '顶部混合-侧边优先',
'top-hybrid-header-first': '顶部混合-顶部优先', 'top-hybrid-header-first': '顶部混合-顶部优先',
vertical_detail: '左侧菜单布局,菜单在左,内容在右。', vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在右侧浅色区域。', 'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在左侧浅色区域。',
'vertical-hybrid-header-first_detail':
'左侧混合布局,一级菜单在顶部,二级菜单在左侧浅色区域,三级菜单在左侧深色区域。',
horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。', horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。', 'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。' 'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'

View File

@ -35,6 +35,7 @@ declare namespace UnionKey {
| 'vertical' | 'vertical'
| 'horizontal' | 'horizontal'
| 'vertical-mix' | 'vertical-mix'
| 'vertical-hybrid-header-first'
| 'top-hybrid-sidebar-first' | 'top-hybrid-sidebar-first'
| 'top-hybrid-header-first'; | 'top-hybrid-header-first';