mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-11-04 15:53:43 +08:00 
			
		
		
		
	refactor(projects)!: refactor global menu & support reversed-horizontal-mix-menu. close #365
				
					
				
			This commit is contained in:
		@@ -1,5 +1,9 @@
 | 
			
		||||
import { transformRecordToOption } from '@/utils/common';
 | 
			
		||||
 | 
			
		||||
export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
 | 
			
		||||
 | 
			
		||||
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
 | 
			
		||||
 | 
			
		||||
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
 | 
			
		||||
  light: 'theme.themeSchema.light',
 | 
			
		||||
  dark: 'theme.themeSchema.dark',
 | 
			
		||||
 
 | 
			
		||||
@@ -30,17 +30,30 @@ export function useRouterPush(inSetup = true) {
 | 
			
		||||
      name: key
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (query) {
 | 
			
		||||
    if (Object.keys(query || {}).length) {
 | 
			
		||||
      routeLocation.query = query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (params) {
 | 
			
		||||
    if (Object.keys(params || {}).length) {
 | 
			
		||||
      routeLocation.params = params;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return routerPush(routeLocation);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function routerPushByKeyWithMetaQuery(key: RouteKey) {
 | 
			
		||||
    const allRoutes = router.getRoutes();
 | 
			
		||||
    const meta = allRoutes.find(item => item.name === key)?.meta || null;
 | 
			
		||||
 | 
			
		||||
    const query: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
    meta?.query?.forEach(item => {
 | 
			
		||||
      query[item.key] = item.value;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return routerPushByKey(key, { query });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function toHome() {
 | 
			
		||||
    return routerPushByKey('root');
 | 
			
		||||
  }
 | 
			
		||||
@@ -95,6 +108,7 @@ export function useRouterPush(inSetup = true) {
 | 
			
		||||
    routerPush,
 | 
			
		||||
    routerBack,
 | 
			
		||||
    routerPushByKey,
 | 
			
		||||
    routerPushByKeyWithMetaQuery,
 | 
			
		||||
    toLogin,
 | 
			
		||||
    toggleLoginModule,
 | 
			
		||||
    redirectFromLogin
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent } from 'vue';
 | 
			
		||||
import { AdminLayout, LAYOUT_SCROLL_EL_ID } from '@sa/materials';
 | 
			
		||||
import type { LayoutMode } from '@sa/materials';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
@@ -18,7 +18,9 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const { menus } = setupMixMenuContext();
 | 
			
		||||
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
 | 
			
		||||
 | 
			
		||||
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
 | 
			
		||||
 | 
			
		||||
const layoutMode = computed(() => {
 | 
			
		||||
  const vertical: LayoutMode = 'vertical';
 | 
			
		||||
@@ -26,30 +28,34 @@ const layoutMode = computed(() => {
 | 
			
		||||
  return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
 | 
			
		||||
  vertical: {
 | 
			
		||||
    showLogo: false,
 | 
			
		||||
    showMenu: false,
 | 
			
		||||
    showMenuToggler: true
 | 
			
		||||
  },
 | 
			
		||||
  'vertical-mix': {
 | 
			
		||||
    showLogo: false,
 | 
			
		||||
    showMenu: false,
 | 
			
		||||
    showMenuToggler: false
 | 
			
		||||
  },
 | 
			
		||||
  horizontal: {
 | 
			
		||||
    showLogo: true,
 | 
			
		||||
    showMenu: true,
 | 
			
		||||
    showMenuToggler: false
 | 
			
		||||
  },
 | 
			
		||||
  'horizontal-mix': {
 | 
			
		||||
    showLogo: true,
 | 
			
		||||
    showMenu: true,
 | 
			
		||||
    showMenuToggler: false
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
const headerProps = computed(() => {
 | 
			
		||||
  const { mode, reverseHorizontalMix } = themeStore.layout;
 | 
			
		||||
 | 
			
		||||
const headerProps = computed(() => headerPropsConfig[themeStore.layout.mode]);
 | 
			
		||||
  const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
 | 
			
		||||
    vertical: {
 | 
			
		||||
      showLogo: false,
 | 
			
		||||
      showMenu: false,
 | 
			
		||||
      showMenuToggler: true
 | 
			
		||||
    },
 | 
			
		||||
    'vertical-mix': {
 | 
			
		||||
      showLogo: false,
 | 
			
		||||
      showMenu: false,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    horizontal: {
 | 
			
		||||
      showLogo: true,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    'horizontal-mix': {
 | 
			
		||||
      showLogo: true,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return headerPropsConfig[mode];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
 | 
			
		||||
 | 
			
		||||
@@ -62,11 +68,16 @@ const siderWidth = computed(() => getSiderWidth());
 | 
			
		||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
 | 
			
		||||
 | 
			
		||||
function getSiderWidth() {
 | 
			
		||||
  const { reverseHorizontalMix } = themeStore.layout;
 | 
			
		||||
  const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
 | 
			
		||||
 | 
			
		||||
  if (isHorizontalMix.value && reverseHorizontalMix) {
 | 
			
		||||
    return isActiveFirstLevelMenuHasChildren.value ? width : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
 | 
			
		||||
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && menus.value.length) {
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
 | 
			
		||||
    w += mixChildMenuWidth;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -78,7 +89,7 @@ function getSiderCollapsedWidth() {
 | 
			
		||||
 | 
			
		||||
  let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
 | 
			
		||||
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && menus.value.length) {
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
 | 
			
		||||
    w += mixChildMenuWidth;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -116,6 +127,7 @@ function getSiderCollapsedWidth() {
 | 
			
		||||
    <template #sider>
 | 
			
		||||
      <GlobalSider />
 | 
			
		||||
    </template>
 | 
			
		||||
    <GlobalMenu />
 | 
			
		||||
    <GlobalContent />
 | 
			
		||||
    <ThemeDrawer />
 | 
			
		||||
    <template #footer>
 | 
			
		||||
 
 | 
			
		||||
@@ -26,10 +26,30 @@ function useMixMenu() {
 | 
			
		||||
    setActiveFirstLevelMenuKey(firstLevelRouteName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const menus = computed(
 | 
			
		||||
  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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
 | 
			
		||||
 | 
			
		||||
    return Boolean(findItem?.children?.length);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => route.name,
 | 
			
		||||
    () => {
 | 
			
		||||
@@ -39,9 +59,12 @@ function useMixMenu() {
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    allMenus,
 | 
			
		||||
    firstLevelMenus,
 | 
			
		||||
    childLevelMenus,
 | 
			
		||||
    isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
    activeFirstLevelMenuKey,
 | 
			
		||||
    setActiveFirstLevelMenuKey,
 | 
			
		||||
    getActiveFirstLevelMenuKey,
 | 
			
		||||
    menus
 | 
			
		||||
    getActiveFirstLevelMenuKey
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useFullscreen } from '@vueuse/core';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import HorizontalMenu from '../global-menu/base-menu.vue';
 | 
			
		||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import GlobalLogo from '../global-logo/index.vue';
 | 
			
		||||
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
 | 
			
		||||
import GlobalSearch from '../global-search/index.vue';
 | 
			
		||||
import { useMixMenuContext } from '../../context';
 | 
			
		||||
import ThemeButton from './components/theme-button.vue';
 | 
			
		||||
import UserAvatar from './components/user-avatar.vue';
 | 
			
		||||
 | 
			
		||||
@@ -29,29 +26,15 @@ defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { isFullscreen, toggle } = useFullscreen();
 | 
			
		||||
const { menus } = useMixMenuContext();
 | 
			
		||||
 | 
			
		||||
const headerMenus = computed(() => {
 | 
			
		||||
  if (themeStore.layout.mode === 'horizontal') {
 | 
			
		||||
    return routeStore.menus;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (themeStore.layout.mode === 'horizontal-mix') {
 | 
			
		||||
    return menus.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [];
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <DarkModeContainer class="h-full flex-y-center px-12px shadow-header">
 | 
			
		||||
    <GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
 | 
			
		||||
    <HorizontalMenu v-if="showMenu" mode="horizontal" :menus="headerMenus" class="px-12px" />
 | 
			
		||||
    <MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
 | 
			
		||||
    <div v-if="showMenu" :id="GLOBAL_HEADER_MENU_ID" class="h-full flex-y-center flex-1-hidden"></div>
 | 
			
		||||
    <div v-else class="h-full flex-y-center flex-1-hidden">
 | 
			
		||||
      <MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
 | 
			
		||||
      <GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="h-full flex-y-center justify-end">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,96 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import type { MentionOption, MenuProps } from 'naive-ui';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'BaseMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  darkTheme?: boolean;
 | 
			
		||||
  mode?: MenuProps['mode'];
 | 
			
		||||
  menus: App.Global.Menu[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  mode: 'vertical'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKey } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const naiveMenus = computed(() => props.menus as unknown as MentionOption[]);
 | 
			
		||||
 | 
			
		||||
const isHorizontal = computed(() => props.mode === 'horizontal');
 | 
			
		||||
 | 
			
		||||
const siderCollapse = computed(() => themeStore.layout.mode === 'vertical' && appStore.siderCollapse);
 | 
			
		||||
 | 
			
		||||
const headerHeight = computed(() => `${themeStore.header.height}px`);
 | 
			
		||||
 | 
			
		||||
const selectedKey = computed(() => {
 | 
			
		||||
  const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
  const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
  const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
  return routeName;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const expandedKeys = ref<string[]>([]);
 | 
			
		||||
 | 
			
		||||
function updateExpandedKeys() {
 | 
			
		||||
  if (isHorizontal.value || siderCollapse.value || !selectedKey.value) {
 | 
			
		||||
    expandedKeys.value = [];
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleClickMenu(key: RouteKey) {
 | 
			
		||||
  const query = routeStore.getRouteQueryOfMetaByKey(key);
 | 
			
		||||
 | 
			
		||||
  routerPushByKey(key, { query });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => route.name,
 | 
			
		||||
  () => {
 | 
			
		||||
    updateExpandedKeys();
 | 
			
		||||
  },
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <SimpleScrollbar>
 | 
			
		||||
    <NMenu
 | 
			
		||||
      v-model:expanded-keys="expandedKeys"
 | 
			
		||||
      :mode="mode"
 | 
			
		||||
      :value="selectedKey"
 | 
			
		||||
      :collapsed="siderCollapse"
 | 
			
		||||
      :collapsed-width="themeStore.sider.collapsedWidth"
 | 
			
		||||
      :collapsed-icon-size="22"
 | 
			
		||||
      :options="naiveMenus"
 | 
			
		||||
      :inverted="darkTheme"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="handleClickMenu"
 | 
			
		||||
    />
 | 
			
		||||
  </SimpleScrollbar>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
:deep(.n-menu--horizontal) {
 | 
			
		||||
  --n-item-height: v-bind(headerHeight) !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -3,31 +3,29 @@ import { computed } from 'vue';
 | 
			
		||||
import { createReusableTemplate } from '@vueuse/core';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { transformColorWithOpacity } from '@sa/color';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'FirstLevelMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  menus: App.Global.Menu[];
 | 
			
		||||
  activeMenuKey?: string;
 | 
			
		||||
  inverted?: boolean;
 | 
			
		||||
  siderCollapse?: boolean;
 | 
			
		||||
  darkMode?: boolean;
 | 
			
		||||
  themeColor: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineProps<Props>();
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
interface Emits {
 | 
			
		||||
  (e: 'select', menu: App.Global.Menu): boolean;
 | 
			
		||||
  (e: 'toggleSiderCollapse'): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<Emits>();
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
 | 
			
		||||
interface MixMenuItemProps {
 | 
			
		||||
  /** Menu item label */
 | 
			
		||||
  label: App.Global.Menu['label'];
 | 
			
		||||
@@ -36,12 +34,12 @@ interface MixMenuItemProps {
 | 
			
		||||
  /** Active menu item */
 | 
			
		||||
  active: boolean;
 | 
			
		||||
  /** Mini size */
 | 
			
		||||
  isMini: boolean;
 | 
			
		||||
  isMini?: boolean;
 | 
			
		||||
}
 | 
			
		||||
const [DefineMixMenuItem, MixMenuItem] = createReusableTemplate<MixMenuItemProps>();
 | 
			
		||||
 | 
			
		||||
const selectedBgColor = computed(() => {
 | 
			
		||||
  const { darkMode, themeColor } = themeStore;
 | 
			
		||||
  const { darkMode, themeColor } = props;
 | 
			
		||||
 | 
			
		||||
  const light = transformColorWithOpacity(themeColor, 0.1, '#ffffff');
 | 
			
		||||
  const dark = transformColorWithOpacity(themeColor, 0.3, '#000000');
 | 
			
		||||
@@ -52,6 +50,10 @@ const selectedBgColor = computed(() => {
 | 
			
		||||
function handleClickMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  emit('select', menu);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleSiderCollapse() {
 | 
			
		||||
  emit('toggleSiderCollapse');
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -80,21 +82,21 @@ function handleClickMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
    <slot></slot>
 | 
			
		||||
    <SimpleScrollbar>
 | 
			
		||||
      <MixMenuItem
 | 
			
		||||
        v-for="menu in routeStore.menus"
 | 
			
		||||
        v-for="menu in menus"
 | 
			
		||||
        :key="menu.key"
 | 
			
		||||
        :label="menu.label"
 | 
			
		||||
        :icon="menu.icon"
 | 
			
		||||
        :active="menu.key === activeMenuKey"
 | 
			
		||||
        :is-mini="appStore.siderCollapse"
 | 
			
		||||
        :is-mini="siderCollapse"
 | 
			
		||||
        @click="handleClickMixMenu(menu)"
 | 
			
		||||
      />
 | 
			
		||||
    </SimpleScrollbar>
 | 
			
		||||
    <MenuToggler
 | 
			
		||||
      arrow-icon
 | 
			
		||||
      :collapsed="appStore.siderCollapse"
 | 
			
		||||
      :collapsed="siderCollapse"
 | 
			
		||||
      :z-index="99"
 | 
			
		||||
      :class="{ 'text-white:88 !hover:text-white': inverted }"
 | 
			
		||||
      @click="appStore.toggleSiderCollapse"
 | 
			
		||||
      @click="toggleSiderCollapse"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useMixMenuContext } from '../../context';
 | 
			
		||||
import FirstLevelMenu from './first-level-menu.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HorizontalMixMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
 | 
			
		||||
const { routerPushByKey } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
 | 
			
		||||
  if (!menu.children?.length) {
 | 
			
		||||
    routerPushByKey(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <FirstLevelMenu :active-menu-key="activeFirstLevelMenuKey" @select="handleSelectMixMenu">
 | 
			
		||||
    <slot></slot>
 | 
			
		||||
  </FirstLevelMenu>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										33
									
								
								src/layouts/modules/global-menu/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/layouts/modules/global-menu/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import type { Component } from 'vue';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import VerticalMenu from './modules/vertical-menu.vue';
 | 
			
		||||
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
 | 
			
		||||
import HorizontalMenu from './modules/horizontal-menu.vue';
 | 
			
		||||
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
 | 
			
		||||
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'GlobalMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const activeMenu = computed(() => {
 | 
			
		||||
  const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
 | 
			
		||||
    vertical: VerticalMenu,
 | 
			
		||||
    'vertical-mix': VerticalMixMenu,
 | 
			
		||||
    horizontal: HorizontalMenu,
 | 
			
		||||
    'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return menuMap[themeStore.layout.mode];
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <component :is="activeMenu" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										39
									
								
								src/layouts/modules/global-menu/modules/horizontal-menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/layouts/modules/global-menu/modules/horizontal-menu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HorizontalMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const selectedKey = computed(() => {
 | 
			
		||||
  const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
  const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
  const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
  return routeName;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
 | 
			
		||||
    <NMenu
 | 
			
		||||
      mode="horizontal"
 | 
			
		||||
      :value="selectedKey"
 | 
			
		||||
      :options="routeStore.menus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
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 { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import FirstLevelMenu from '../components/first-level-menu.vue';
 | 
			
		||||
import { useMixMenuContext } from '../../../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HorizontalMixMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
 | 
			
		||||
 | 
			
		||||
const selectedKey = computed(() => {
 | 
			
		||||
  const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
  const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
  const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
  return routeName;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
 | 
			
		||||
  if (!menu.children?.length) {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
 | 
			
		||||
    <NMenu
 | 
			
		||||
      mode="horizontal"
 | 
			
		||||
      :value="selectedKey"
 | 
			
		||||
      :options="childLevelMenus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <FirstLevelMenu
 | 
			
		||||
      :menus="allMenus"
 | 
			
		||||
      :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
      :inverted="inverted"
 | 
			
		||||
      :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
      :dark-mode="themeStore.darkMode"
 | 
			
		||||
      :theme-color="themeStore.themeColor"
 | 
			
		||||
      @select="handleSelectMixMenu"
 | 
			
		||||
      @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
    >
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
    </FirstLevelMenu>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -0,0 +1,97 @@
 | 
			
		||||
<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 { 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 { useMixMenuContext } from '../../../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ReversedHorizontalMixMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const {
 | 
			
		||||
  firstLevelMenus,
 | 
			
		||||
  childLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  setActiveFirstLevelMenuKey,
 | 
			
		||||
  isActiveFirstLevelMenuHasChildren
 | 
			
		||||
} = useMixMenuContext();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
 | 
			
		||||
 | 
			
		||||
const selectedKey = computed(() => {
 | 
			
		||||
  const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
  const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
  const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
  return routeName;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(key: RouteKey) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(key);
 | 
			
		||||
 | 
			
		||||
  if (!isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(key);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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="handleSelectMixMenu"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <SimpleScrollbar>
 | 
			
		||||
      <NMenu
 | 
			
		||||
        v-model:expanded-keys="expandedKeys"
 | 
			
		||||
        mode="vertical"
 | 
			
		||||
        :value="selectedKey"
 | 
			
		||||
        :collapsed="appStore.siderCollapse"
 | 
			
		||||
        :collapsed-width="themeStore.sider.collapsedWidth"
 | 
			
		||||
        :collapsed-icon-size="22"
 | 
			
		||||
        :options="childLevelMenus"
 | 
			
		||||
        :inverted="inverted"
 | 
			
		||||
        :indent="18"
 | 
			
		||||
        @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
      />
 | 
			
		||||
    </SimpleScrollbar>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										70
									
								
								src/layouts/modules/global-menu/modules/vertical-menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/layouts/modules/global-menu/modules/vertical-menu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
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 { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'VerticalMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
 | 
			
		||||
 | 
			
		||||
const selectedKey = computed(() => {
 | 
			
		||||
  const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
  const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
  const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
  return routeName;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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_SIDER_MENU_ID}`">
 | 
			
		||||
    <SimpleScrollbar>
 | 
			
		||||
      <NMenu
 | 
			
		||||
        v-model:expanded-keys="expandedKeys"
 | 
			
		||||
        mode="vertical"
 | 
			
		||||
        :value="selectedKey"
 | 
			
		||||
        :collapsed="appStore.siderCollapse"
 | 
			
		||||
        :collapsed-width="themeStore.sider.collapsedWidth"
 | 
			
		||||
        :collapsed-icon-size="22"
 | 
			
		||||
        :options="routeStore.menus"
 | 
			
		||||
        :inverted="inverted"
 | 
			
		||||
        :indent="18"
 | 
			
		||||
        @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
      />
 | 
			
		||||
    </SimpleScrollbar>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										135
									
								
								src/layouts/modules/global-menu/modules/vertical-mix-menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/layouts/modules/global-menu/modules/vertical-mix-menu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { useBoolean } from '@sa/hooks';
 | 
			
		||||
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 { $t } from '@/locales';
 | 
			
		||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useMixMenuContext } from '../../../context';
 | 
			
		||||
import FirstLevelMenu from '../components/first-level-menu.vue';
 | 
			
		||||
import GlobalLogo from '../../global-logo/index.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'VerticalMenuMix'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
 | 
			
		||||
const {
 | 
			
		||||
  allMenus,
 | 
			
		||||
  childLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  setActiveFirstLevelMenuKey,
 | 
			
		||||
  getActiveFirstLevelMenuKey
 | 
			
		||||
  //
 | 
			
		||||
} = useMixMenuContext();
 | 
			
		||||
 | 
			
		||||
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(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
 | 
			
		||||
  if (menu.children?.length) {
 | 
			
		||||
    setDrawerVisible(true);
 | 
			
		||||
  } else {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleResetActiveMenu() {
 | 
			
		||||
  getActiveFirstLevelMenuKey();
 | 
			
		||||
  setDrawerVisible(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const selectedKey = computed(() => {
 | 
			
		||||
  const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
  const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
  const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
  return routeName;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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_SIDER_MENU_ID}`">
 | 
			
		||||
    <div class="h-full flex" @mouseleave="handleResetActiveMenu">
 | 
			
		||||
      <FirstLevelMenu
 | 
			
		||||
        :menus="allMenus"
 | 
			
		||||
        :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
        :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"
 | 
			
		||||
              :options="childLevelMenus"
 | 
			
		||||
              :collapsed="appStore.siderCollapse"
 | 
			
		||||
              :collapsed-width="themeStore.sider.collapsedWidth"
 | 
			
		||||
              :collapsed-icon-size="22"
 | 
			
		||||
              :inverted="inverted"
 | 
			
		||||
              :indent="18"
 | 
			
		||||
              @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
            />
 | 
			
		||||
          </SimpleScrollbar>
 | 
			
		||||
        </DarkModeContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useBoolean } from '@sa/hooks';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import { useMixMenuContext } from '../../context';
 | 
			
		||||
import FirstLevelMenu from './first-level-menu.vue';
 | 
			
		||||
import BaseMenu from './base-menu.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'VerticalMixMenu'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const { routerPushByKey } = useRouterPush();
 | 
			
		||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
 | 
			
		||||
const { menus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey, getActiveFirstLevelMenuKey } = useMixMenuContext();
 | 
			
		||||
 | 
			
		||||
const siderInverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
 | 
			
		||||
 | 
			
		||||
const hasMenus = computed(() => menus.value.length > 0);
 | 
			
		||||
 | 
			
		||||
const showDrawer = computed(() => hasMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
 | 
			
		||||
  if (menu.children?.length) {
 | 
			
		||||
    setDrawerVisible(true);
 | 
			
		||||
  } else {
 | 
			
		||||
    routerPushByKey(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleResetActiveMenu() {
 | 
			
		||||
  getActiveFirstLevelMenuKey();
 | 
			
		||||
  setDrawerVisible(false);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="h-full flex" @mouseleave="handleResetActiveMenu">
 | 
			
		||||
    <FirstLevelMenu :active-menu-key="activeFirstLevelMenuKey" :inverted="siderInverted" @select="handleSelectMixMenu">
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
    </FirstLevelMenu>
 | 
			
		||||
    <div
 | 
			
		||||
      class="relative h-full transition-width-300"
 | 
			
		||||
      :style="{ width: appStore.mixSiderFixed && hasMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
 | 
			
		||||
    >
 | 
			
		||||
      <DarkModeContainer
 | 
			
		||||
        class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
 | 
			
		||||
        :inverted="siderInverted"
 | 
			
		||||
        :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': siderInverted }"
 | 
			
		||||
            @click="appStore.toggleMixSiderFixed"
 | 
			
		||||
          />
 | 
			
		||||
        </header>
 | 
			
		||||
        <BaseMenu :dark-theme="siderInverted" :menus="menus" />
 | 
			
		||||
      </DarkModeContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -2,11 +2,8 @@
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import GlobalLogo from '../global-logo/index.vue';
 | 
			
		||||
import VerticalMenu from '../global-menu/base-menu.vue';
 | 
			
		||||
import VerticalMixMenu from '../global-menu/vertical-mix-menu.vue';
 | 
			
		||||
import HorizontalMixMenu from '../global-menu/horizontal-mix-menu.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'GlobalSider'
 | 
			
		||||
@@ -14,12 +11,12 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
 | 
			
		||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
 | 
			
		||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
 | 
			
		||||
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
 | 
			
		||||
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
 | 
			
		||||
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -29,11 +26,7 @@ const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
 | 
			
		||||
      :show-title="!appStore.siderCollapse"
 | 
			
		||||
      :style="{ height: themeStore.header.height + 'px' }"
 | 
			
		||||
    />
 | 
			
		||||
    <VerticalMixMenu v-if="isVerticalMix">
 | 
			
		||||
      <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
 | 
			
		||||
    </VerticalMixMenu>
 | 
			
		||||
    <HorizontalMixMenu v-else-if="isHorizontalMix" />
 | 
			
		||||
    <VerticalMenu v-else :dark-theme="darkMenu" :menus="routeStore.menus" />
 | 
			
		||||
    <div :id="GLOBAL_SIDER_MENU_ID" :class="menuWrapperClass"></div>
 | 
			
		||||
  </DarkModeContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import LayoutModeCard from '../components/layout-mode-card.vue';
 | 
			
		||||
import SettingItem from '../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'LayoutMode'
 | 
			
		||||
@@ -10,6 +11,10 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
function handleReverseHorizontalMixChange(value: boolean) {
 | 
			
		||||
  themeStore.setLayoutReverseHorizontalMix(value);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -44,6 +49,13 @@ const themeStore = useThemeStore();
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </LayoutModeCard>
 | 
			
		||||
  <SettingItem
 | 
			
		||||
    v-if="themeStore.layout.mode === 'horizontal-mix'"
 | 
			
		||||
    :label="$t('theme.layoutMode.reverseHorizontalMix')"
 | 
			
		||||
    class="mt-16px"
 | 
			
		||||
  >
 | 
			
		||||
    <NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
 | 
			
		||||
  </SettingItem>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,8 @@ const local: App.I18n.Schema = {
 | 
			
		||||
      vertical: 'Vertical Menu Mode',
 | 
			
		||||
      horizontal: 'Horizontal Menu Mode',
 | 
			
		||||
      'vertical-mix': 'Vertical Mix Menu Mode',
 | 
			
		||||
      'horizontal-mix': 'Horizontal Mix menu Mode'
 | 
			
		||||
      'horizontal-mix': 'Horizontal Mix menu Mode',
 | 
			
		||||
      reverseHorizontalMix: 'Reverse first level menus and child level menus position'
 | 
			
		||||
    },
 | 
			
		||||
    recommendColor: 'Apply Recommended Color Algorithm',
 | 
			
		||||
    recommendColorDesc: 'The recommended color algorithm refers to',
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,8 @@ const local: App.I18n.Schema = {
 | 
			
		||||
      vertical: '左侧菜单模式',
 | 
			
		||||
      'vertical-mix': '左侧菜单混合模式',
 | 
			
		||||
      horizontal: '顶部菜单模式',
 | 
			
		||||
      'horizontal-mix': '顶部菜单混合模式'
 | 
			
		||||
      'horizontal-mix': '顶部菜单混合模式',
 | 
			
		||||
      reverseHorizontalMix: '一级菜单与子级菜单位置反转'
 | 
			
		||||
    },
 | 
			
		||||
    recommendColor: '应用推荐算法的颜色',
 | 
			
		||||
    recommendColorDesc: '推荐颜色的算法参照',
 | 
			
		||||
 
 | 
			
		||||
@@ -354,34 +354,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
 | 
			
		||||
    return getSelectedMenuKeyPathByKey(selectedKey, menus.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get route meta by key
 | 
			
		||||
   *
 | 
			
		||||
   * @param key Route key
 | 
			
		||||
   */
 | 
			
		||||
  function getRouteMetaByKey(key: string) {
 | 
			
		||||
    const allRoutes = router.getRoutes();
 | 
			
		||||
 | 
			
		||||
    return allRoutes.find(route => route.name === key)?.meta || null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get route query of meta by key
 | 
			
		||||
   *
 | 
			
		||||
   * @param key
 | 
			
		||||
   */
 | 
			
		||||
  function getRouteQueryOfMetaByKey(key: string) {
 | 
			
		||||
    const meta = getRouteMetaByKey(key);
 | 
			
		||||
 | 
			
		||||
    const query: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
    meta?.query?.forEach(item => {
 | 
			
		||||
      query[item.key] = item.value;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return query;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    resetStore,
 | 
			
		||||
    routeHome,
 | 
			
		||||
@@ -398,7 +370,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
 | 
			
		||||
    isInitAuthRoute,
 | 
			
		||||
    setIsInitAuthRoute,
 | 
			
		||||
    getIsAuthRouteExist,
 | 
			
		||||
    getSelectedMenuKeyPath,
 | 
			
		||||
    getRouteQueryOfMetaByKey
 | 
			
		||||
    getSelectedMenuKeyPath
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,14 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
    );
 | 
			
		||||
    addThemeVarsToGlobal(themeTokens, darkThemeTokens);
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Set layout reverse horizontal mix
 | 
			
		||||
   *
 | 
			
		||||
   * @param reverse Reverse horizontal mix
 | 
			
		||||
   */
 | 
			
		||||
  function setLayoutReverseHorizontalMix(reverse: boolean) {
 | 
			
		||||
    settings.value.layout.reverseHorizontalMix = reverse;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Cache theme settings */
 | 
			
		||||
  function cacheThemeSettings() {
 | 
			
		||||
@@ -193,6 +201,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
    setThemeScheme,
 | 
			
		||||
    toggleThemeScheme,
 | 
			
		||||
    updateThemeColors,
 | 
			
		||||
    setThemeLayout
 | 
			
		||||
    setThemeLayout,
 | 
			
		||||
    setLayoutReverseHorizontalMix
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
 | 
			
		||||
  isInfoFollowPrimary: true,
 | 
			
		||||
  layout: {
 | 
			
		||||
    mode: 'vertical',
 | 
			
		||||
    scrollMode: 'content'
 | 
			
		||||
    scrollMode: 'content',
 | 
			
		||||
    reverseHorizontalMix: false
 | 
			
		||||
  },
 | 
			
		||||
  page: {
 | 
			
		||||
    animate: true,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								src/typings/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								src/typings/app.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -24,6 +24,12 @@ declare namespace App {
 | 
			
		||||
        mode: UnionKey.ThemeLayoutMode;
 | 
			
		||||
        /** Scroll mode */
 | 
			
		||||
        scrollMode: UnionKey.ThemeScrollMode;
 | 
			
		||||
        /**
 | 
			
		||||
         * Whether to reverse the horizontal mix
 | 
			
		||||
         *
 | 
			
		||||
         * if true, the vertical child level menus in left and horizontal first level menus in top
 | 
			
		||||
         */
 | 
			
		||||
        reverseHorizontalMix?: boolean;
 | 
			
		||||
      };
 | 
			
		||||
      /** Page */
 | 
			
		||||
      page: {
 | 
			
		||||
@@ -164,7 +170,7 @@ declare namespace App {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** The global menu */
 | 
			
		||||
    interface Menu {
 | 
			
		||||
    type Menu = {
 | 
			
		||||
      /**
 | 
			
		||||
       * The menu key
 | 
			
		||||
       *
 | 
			
		||||
@@ -183,7 +189,7 @@ declare namespace App {
 | 
			
		||||
      icon?: () => VNode;
 | 
			
		||||
      /** The menu children */
 | 
			
		||||
      children?: Menu[];
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    type Breadcrumb = Omit<Menu, 'children'> & {
 | 
			
		||||
      options?: Breadcrumb[];
 | 
			
		||||
@@ -326,7 +332,7 @@ declare namespace App {
 | 
			
		||||
      theme: {
 | 
			
		||||
        themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
 | 
			
		||||
        grayscale: string;
 | 
			
		||||
        layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string>;
 | 
			
		||||
        layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
 | 
			
		||||
        recommendColor: string;
 | 
			
		||||
        recommendColorDesc: string;
 | 
			
		||||
        themeColor: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								src/typings/union-key.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/typings/union-key.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -20,7 +20,7 @@ declare namespace UnionKey {
 | 
			
		||||
   * - vertical: the vertical menu in left
 | 
			
		||||
   * - horizontal: the horizontal menu in top
 | 
			
		||||
   * - vertical-mix: two vertical mixed menus in left
 | 
			
		||||
   * - horizontal-mix: the vertical menu in left and horizontal menu in top
 | 
			
		||||
   * - horizontal-mix: the vertical first level menus in left and horizontal child level menus in top
 | 
			
		||||
   */
 | 
			
		||||
  type ThemeLayoutMode = 'vertical' | 'horizontal' | 'vertical-mix' | 'horizontal-mix';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user