mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-11-04 07:43:42 +08:00 
			
		
		
		
	feat(projects): add 'vertical-hybrid-header-first' layout mode
This commit is contained in:
		@@ -23,6 +23,7 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
 | 
			
		||||
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
 | 
			
		||||
  vertical: 'theme.layout.layoutMode.vertical',
 | 
			
		||||
  'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
 | 
			
		||||
  'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
 | 
			
		||||
  horizontal: 'theme.layout.layoutMode.horizontal',
 | 
			
		||||
  'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
 | 
			
		||||
  'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,11 @@ const headerProps = computed(() => {
 | 
			
		||||
      showMenu: false,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    'vertical-hybrid-header-first': {
 | 
			
		||||
      showLogo: !isActiveFirstLevelMenuHasChildren.value,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    horizontal: {
 | 
			
		||||
      showLogo: true,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
@@ -66,6 +71,8 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
 | 
			
		||||
 | 
			
		||||
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 isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
 | 
			
		||||
@@ -74,36 +81,46 @@ const siderWidth = computed(() => getSiderWidth());
 | 
			
		||||
 | 
			
		||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
 | 
			
		||||
 | 
			
		||||
function getSiderWidth() {
 | 
			
		||||
  const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
 | 
			
		||||
function getSiderAndCollapsedWidth(isCollapsed: boolean) {
 | 
			
		||||
  const {
 | 
			
		||||
    mixChildMenuWidth,
 | 
			
		||||
    collapsedWidth,
 | 
			
		||||
    width: themeWidth,
 | 
			
		||||
    mixCollapsedWidth,
 | 
			
		||||
    mixWidth: themeMixWidth
 | 
			
		||||
  } = themeStore.sider;
 | 
			
		||||
 | 
			
		||||
  const width = isCollapsed ? collapsedWidth : themeWidth;
 | 
			
		||||
  const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
 | 
			
		||||
 | 
			
		||||
  if (isTopHybridHeaderFirst.value) {
 | 
			
		||||
    return isActiveFirstLevelMenuHasChildren.value ? width : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let w = isVerticalMix.value || isTopHybridSidebarFirst.value ? mixWidth : width;
 | 
			
		||||
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
 | 
			
		||||
    w += mixChildMenuWidth;
 | 
			
		||||
  if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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() {
 | 
			
		||||
  const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
  return getSiderAndCollapsedWidth(true);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { useContext } from '@sa/hooks';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
 | 
			
		||||
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
 | 
			
		||||
 | 
			
		||||
@@ -9,6 +11,17 @@ function useMixMenu() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
  const routeStore = useRouteStore();
 | 
			
		||||
  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('');
 | 
			
		||||
 | 
			
		||||
@@ -22,20 +35,6 @@ function useMixMenu() {
 | 
			
		||||
    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(() => {
 | 
			
		||||
    if (!activeFirstLevelMenuKey.value) {
 | 
			
		||||
      return false;
 | 
			
		||||
@@ -46,6 +45,54 @@ function useMixMenu() {
 | 
			
		||||
    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(
 | 
			
		||||
    () => route.name,
 | 
			
		||||
    () => {
 | 
			
		||||
@@ -55,13 +102,19 @@ function useMixMenu() {
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    allMenus,
 | 
			
		||||
    firstLevelMenus,
 | 
			
		||||
    childLevelMenus,
 | 
			
		||||
    isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
    activeFirstLevelMenuKey,
 | 
			
		||||
    setActiveFirstLevelMenuKey,
 | 
			
		||||
    getActiveFirstLevelMenuKey
 | 
			
		||||
    isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
    handleSelectFirstLevelMenu,
 | 
			
		||||
    getActiveFirstLevelMenuKey,
 | 
			
		||||
    secondLevelMenus,
 | 
			
		||||
    activeSecondLevelMenuKey,
 | 
			
		||||
    setActiveSecondLevelMenuKey,
 | 
			
		||||
    isActiveSecondLevelMenuHasChildren,
 | 
			
		||||
    handleSelectSecondLevelMenu,
 | 
			
		||||
    getActiveSecondLevelMenuKey,
 | 
			
		||||
    childLevelMenus
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { computed } from 'vue';
 | 
			
		||||
import { createReusableTemplate } from '@vueuse/core';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { transformColorWithOpacity } from '@sa/color';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'FirstLevelMenu'
 | 
			
		||||
@@ -20,7 +21,7 @@ interface Props {
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
interface Emits {
 | 
			
		||||
  (e: 'select', menu: App.Global.Menu): boolean;
 | 
			
		||||
  (e: 'select', menuKey: RouteKey): boolean;
 | 
			
		||||
  (e: 'toggleSiderCollapse'): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -47,8 +48,8 @@ const selectedBgColor = computed(() => {
 | 
			
		||||
  return darkMode ? dark : light;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function handleClickMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  emit('select', menu);
 | 
			
		||||
function handleClickMixMenu(menuKey: RouteKey) {
 | 
			
		||||
  emit('select', menuKey);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleSiderCollapse() {
 | 
			
		||||
@@ -88,7 +89,7 @@ function toggleSiderCollapse() {
 | 
			
		||||
        :icon="menu.icon"
 | 
			
		||||
        :active="menu.key === activeMenuKey"
 | 
			
		||||
        :is-mini="siderCollapse"
 | 
			
		||||
        @click="handleClickMixMenu(menu)"
 | 
			
		||||
        @click="handleClickMixMenu(menu.routeKey)"
 | 
			
		||||
      />
 | 
			
		||||
    </SimpleScrollbar>
 | 
			
		||||
    <MenuToggler
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import VerticalMenu from './modules/vertical-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 TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
 | 
			
		||||
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
 | 
			
		||||
@@ -20,6 +21,7 @@ const activeMenu = computed(() => {
 | 
			
		||||
  const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
 | 
			
		||||
    vertical: VerticalMenu,
 | 
			
		||||
    'vertical-mix': VerticalMixMenu,
 | 
			
		||||
    'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
 | 
			
		||||
    horizontal: HorizontalMenu,
 | 
			
		||||
    'top-hybrid-sidebar-first': TopHybridSidebarFirst,
 | 
			
		||||
    'top-hybrid-header-first': TopHybridHeaderFirst
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
@@ -11,7 +10,7 @@ import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../../../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ReversedHorizontalMixMenu'
 | 
			
		||||
  name: 'TopHybridHeaderFirst'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
@@ -19,23 +18,9 @@ const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const {
 | 
			
		||||
  firstLevelMenus,
 | 
			
		||||
  childLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  setActiveFirstLevelMenuKey,
 | 
			
		||||
  isActiveFirstLevelMenuHasChildren
 | 
			
		||||
} = useMixMenuContext();
 | 
			
		||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(key: RouteKey) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(key);
 | 
			
		||||
 | 
			
		||||
  if (!isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(key);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const expandedKeys = ref<string[]>([]);
 | 
			
		||||
 | 
			
		||||
function updateExpandedKeys() {
 | 
			
		||||
@@ -63,7 +48,7 @@ watch(
 | 
			
		||||
      :options="firstLevelMenus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="handleSelectMixMenu"
 | 
			
		||||
      @update:value="handleSelectFirstLevelMenu"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
@@ -75,7 +60,7 @@ watch(
 | 
			
		||||
        :collapsed="appStore.siderCollapse"
 | 
			
		||||
        :collapsed-width="themeStore.sider.collapsedWidth"
 | 
			
		||||
        :collapsed-icon-size="22"
 | 
			
		||||
        :options="childLevelMenus"
 | 
			
		||||
        :options="secondLevelMenus"
 | 
			
		||||
        :indent="18"
 | 
			
		||||
        @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
      />
 | 
			
		||||
 
 | 
			
		||||
@@ -7,22 +7,14 @@ import FirstLevelMenu from '../components/first-level-menu.vue';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../../../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HorizontalMixMenu'
 | 
			
		||||
  name: 'TopHybridSidebarFirst'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
 | 
			
		||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
 | 
			
		||||
  if (!menu.children?.length) {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -30,7 +22,7 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
    <NMenu
 | 
			
		||||
      mode="horizontal"
 | 
			
		||||
      :value="selectedKey"
 | 
			
		||||
      :options="childLevelMenus"
 | 
			
		||||
      :options="secondLevelMenus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
@@ -38,12 +30,12 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <FirstLevelMenu
 | 
			
		||||
      :menus="allMenus"
 | 
			
		||||
      :menus="firstLevelMenus"
 | 
			
		||||
      :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
      :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
      :dark-mode="themeStore.darkMode"
 | 
			
		||||
      :theme-color="themeStore.themeColor"
 | 
			
		||||
      @select="handleSelectMixMenu"
 | 
			
		||||
      @select="handleSelectFirstLevelMenu"
 | 
			
		||||
      @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { useBoolean } from '@sa/hooks';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
@@ -24,28 +25,26 @@ const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
 | 
			
		||||
const {
 | 
			
		||||
  allMenus,
 | 
			
		||||
  childLevelMenus,
 | 
			
		||||
  firstLevelMenus,
 | 
			
		||||
  secondLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  setActiveFirstLevelMenuKey,
 | 
			
		||||
  getActiveFirstLevelMenuKey
 | 
			
		||||
  //
 | 
			
		||||
  isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
  getActiveFirstLevelMenuKey,
 | 
			
		||||
  handleSelectFirstLevelMenu
 | 
			
		||||
} = useMixMenuContext();
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
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));
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
function handleSelectMenu(key: RouteKey) {
 | 
			
		||||
  handleSelectFirstLevelMenu(key);
 | 
			
		||||
 | 
			
		||||
  if (menu.children?.length) {
 | 
			
		||||
  if (isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    setDrawerVisible(true);
 | 
			
		||||
  } else {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -80,13 +79,13 @@ watch(
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <div class="h-full flex" @mouseleave="handleResetActiveMenu">
 | 
			
		||||
      <FirstLevelMenu
 | 
			
		||||
        :menus="allMenus"
 | 
			
		||||
        :menus="firstLevelMenus"
 | 
			
		||||
        :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
        :inverted="inverted"
 | 
			
		||||
        :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
        :dark-mode="themeStore.darkMode"
 | 
			
		||||
        :theme-color="themeStore.themeColor"
 | 
			
		||||
        @select="handleSelectMixMenu"
 | 
			
		||||
        @select="handleSelectMenu"
 | 
			
		||||
        @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
      >
 | 
			
		||||
        <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
 | 
			
		||||
@@ -113,7 +112,7 @@ watch(
 | 
			
		||||
              v-model:expanded-keys="expandedKeys"
 | 
			
		||||
              mode="vertical"
 | 
			
		||||
              :value="selectedKey"
 | 
			
		||||
              :options="childLevelMenus"
 | 
			
		||||
              :options="secondLevelMenus"
 | 
			
		||||
              :inverted="inverted"
 | 
			
		||||
              :indent="18"
 | 
			
		||||
              @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,16 +12,13 @@ defineOptions({
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
 | 
			
		||||
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
 | 
			
		||||
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
 | 
			
		||||
const darkMenu = computed(
 | 
			
		||||
  () =>
 | 
			
		||||
    !themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
 | 
			
		||||
);
 | 
			
		||||
const showLogo = computed(
 | 
			
		||||
  () => !isVerticalMix.value && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value
 | 
			
		||||
);
 | 
			
		||||
const showLogo = computed(() => themeStore.layout.mode === 'vertical');
 | 
			
		||||
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,11 @@ const layoutConfig: LayoutConfig = {
 | 
			
		||||
    menuClass: 'w-1/4 h-full',
 | 
			
		||||
    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: {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    menuClass: 'w-full h-1/4',
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,14 @@ const themeStore = useThemeStore();
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </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>
 | 
			
		||||
      <div class="layout-header !bg-primary"></div>
 | 
			
		||||
      <div class="horizontal-wrapper">
 | 
			
		||||
 
 | 
			
		||||
@@ -91,11 +91,14 @@ const local: App.I18n.Schema = {
 | 
			
		||||
        vertical: 'Vertical Mode',
 | 
			
		||||
        horizontal: 'Horizontal Mode',
 | 
			
		||||
        'vertical-mix': 'Vertical Mix Mode',
 | 
			
		||||
        'vertical-hybrid-header-first': 'Left Hybrid Header-First',
 | 
			
		||||
        'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-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-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.',
 | 
			
		||||
        'top-hybrid-sidebar-first_detail':
 | 
			
		||||
          'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',
 | 
			
		||||
 
 | 
			
		||||
@@ -90,11 +90,14 @@ const local: App.I18n.Schema = {
 | 
			
		||||
        title: '布局模式',
 | 
			
		||||
        vertical: '左侧菜单模式',
 | 
			
		||||
        'vertical-mix': '左侧菜单混合模式',
 | 
			
		||||
        'vertical-hybrid-header-first': '左侧混合-顶部优先',
 | 
			
		||||
        horizontal: '顶部菜单模式',
 | 
			
		||||
        'top-hybrid-sidebar-first': '顶部混合-侧边优先',
 | 
			
		||||
        'top-hybrid-header-first': '顶部混合-顶部优先',
 | 
			
		||||
        vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
 | 
			
		||||
        'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在右侧浅色区域。',
 | 
			
		||||
        'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在左侧浅色区域。',
 | 
			
		||||
        'vertical-hybrid-header-first_detail':
 | 
			
		||||
          '左侧混合布局,一级菜单在顶部,二级菜单在左侧浅色区域,三级菜单在左侧深色区域。',
 | 
			
		||||
        horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
 | 
			
		||||
        'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
 | 
			
		||||
        'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/typings/union-key.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/typings/union-key.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -35,6 +35,7 @@ declare namespace UnionKey {
 | 
			
		||||
    | 'vertical'
 | 
			
		||||
    | 'horizontal'
 | 
			
		||||
    | 'vertical-mix'
 | 
			
		||||
    | 'vertical-hybrid-header-first'
 | 
			
		||||
    | 'top-hybrid-sidebar-first'
 | 
			
		||||
    | 'top-hybrid-header-first';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user