mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-11-04 15:53:43 +08:00 
			
		
		
		
	feat(projects): vertical-mix的导航模式的二级菜单显示
This commit is contained in:
		@@ -3,11 +3,7 @@
 | 
			
		||||
    class="relative flex-center h-30px pl-14px border-1px rounded-2px cursor-pointer"
 | 
			
		||||
    :class="[
 | 
			
		||||
      closable ? 'pr-6px' : 'pr-14px',
 | 
			
		||||
      active || isHover
 | 
			
		||||
        ? 'text-primary border-primary border-opacity-30'
 | 
			
		||||
        : darkMode
 | 
			
		||||
        ? 'border-[#ffffff3d]'
 | 
			
		||||
        : 'border-[#e5e7eb]',
 | 
			
		||||
      active || isHover ? 'text-primary border-primary border-opacity-30 ' : 'border-[#e5e7eb] dark:border-[#ffffff3d]',
 | 
			
		||||
      { 'bg-primary bg-opacity-10': active }
 | 
			
		||||
    ]"
 | 
			
		||||
    @mouseenter="setTrue"
 | 
			
		||||
@@ -34,10 +30,6 @@ defineProps({
 | 
			
		||||
  closable: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: true
 | 
			
		||||
  },
 | 
			
		||||
  darkMode: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
const emit = defineEmits(['close']);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex-1 flex-col-stretch p-10px"
 | 
			
		||||
    :class="{ 'bg-[#f5f7f9]': !theme.darkMode, 'overflow-hidden': routeProps.fullPage }"
 | 
			
		||||
    class="flex-1 flex-col-stretch p-10px bg-[#f5f7f9] dark:bg-black"
 | 
			
		||||
    :class="{ 'overflow-hidden': routeProps.fullPage }"
 | 
			
		||||
  >
 | 
			
		||||
    <router-view v-slot="{ Component, route }">
 | 
			
		||||
      <transition :name="theme.pageStyle.animateType" mode="out-in" appear>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-layout-footer>
 | 
			
		||||
    <div class="flex-center h-48px text-[#00000073]">Copyright ©2021 Soybean Admin</div>
 | 
			
		||||
    <div class="flex-center h-48px text-[#333639] dark:text-[#ffffffd1]">Copyright ©2021 Soybean Admin</div>
 | 
			
		||||
  </n-layout-footer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-layout-sider
 | 
			
		||||
    :style="{ zIndex }"
 | 
			
		||||
    :native-scrollbar="false"
 | 
			
		||||
    :inverted="inverted"
 | 
			
		||||
    collapse-mode="width"
 | 
			
		||||
    :collapsed="app.menu.collapsed"
 | 
			
		||||
@@ -10,22 +9,26 @@
 | 
			
		||||
    @collapse="handleMenuCollapse(true)"
 | 
			
		||||
    @expand="handleMenuCollapse(false)"
 | 
			
		||||
  >
 | 
			
		||||
    <global-logo v-if="theme.isVerticalNav" />
 | 
			
		||||
    <n-menu
 | 
			
		||||
      :value="activeKey"
 | 
			
		||||
      :collapsed="app.menu.collapsed"
 | 
			
		||||
      :collapsed-width="theme.menuStyle.collapsedWidth"
 | 
			
		||||
      :collapsed-icon-size="22"
 | 
			
		||||
      :options="menus"
 | 
			
		||||
      @update:value="handleUpdateMenu"
 | 
			
		||||
    />
 | 
			
		||||
    <div class="flex-col-stretch h-full">
 | 
			
		||||
      <global-logo v-if="theme.isVerticalNav" />
 | 
			
		||||
      <n-scrollbar class="flex-1-hidden">
 | 
			
		||||
        <n-menu
 | 
			
		||||
          :value="activeKey"
 | 
			
		||||
          :collapsed="app.menu.collapsed"
 | 
			
		||||
          :collapsed-width="theme.menuStyle.collapsedWidth"
 | 
			
		||||
          :collapsed-icon-size="22"
 | 
			
		||||
          :options="menus"
 | 
			
		||||
          @update:value="handleUpdateMenu"
 | 
			
		||||
        />
 | 
			
		||||
      </n-scrollbar>
 | 
			
		||||
    </div>
 | 
			
		||||
  </n-layout-sider>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router';
 | 
			
		||||
import { NMenu, NLayoutSider } from 'naive-ui';
 | 
			
		||||
import { NLayoutSider, NScrollbar, NMenu } from 'naive-ui';
 | 
			
		||||
import type { MenuOption } from 'naive-ui';
 | 
			
		||||
import { useThemeStore, useAppStore } from '@/store';
 | 
			
		||||
import { menus } from '@/router';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,32 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="mb-6px px-4px cursor-pointer">
 | 
			
		||||
  <div
 | 
			
		||||
    class="mb-6px px-4px cursor-pointer"
 | 
			
		||||
    @click="handleRouter"
 | 
			
		||||
    @mouseenter="handleMouseEvent('enter')"
 | 
			
		||||
    @mouseleave="handleMouseEvent('leave')"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      class="flex-center flex-col py-12px rounded-2px"
 | 
			
		||||
      :class="{ 'text-primary bg-primary bg-opacity-10': isActive }"
 | 
			
		||||
      :class="{ 'text-primary bg-primary bg-opacity-10': isActive, 'text-primary': isHover }"
 | 
			
		||||
    >
 | 
			
		||||
      <component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" />
 | 
			
		||||
      <p v-show="!isMini" class="pt-8px text-12px">{{ label }}</p>
 | 
			
		||||
      <p
 | 
			
		||||
        class="pt-8px text-12px overflow-hidden transition-height duration-200 ease-in-out"
 | 
			
		||||
        :class="[isMini ? 'h-0 pt-0' : 'h-20px pt-8px']"
 | 
			
		||||
      >
 | 
			
		||||
        {{ label }}
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import type { PropType, VNodeChild } from 'vue';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { useBoolean } from '@/hooks';
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  routeName: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true
 | 
			
		||||
@@ -33,7 +46,44 @@ defineProps({
 | 
			
		||||
  isMini: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false
 | 
			
		||||
  },
 | 
			
		||||
  hoverRoute: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: ''
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:hoverRoute']);
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
 | 
			
		||||
 | 
			
		||||
const hoverRouteName = ref(props.hoverRoute);
 | 
			
		||||
function setHoverRouteName(name: string) {
 | 
			
		||||
  hoverRouteName.value = name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleRouter() {
 | 
			
		||||
  router.push({ name: props.routeName });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleMouseEvent(type: 'enter' | 'leave') {
 | 
			
		||||
  if (type === 'enter') {
 | 
			
		||||
    setTrue();
 | 
			
		||||
    setHoverRouteName(props.routeName);
 | 
			
		||||
  } else {
 | 
			
		||||
    setFalse();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.hoverRoute,
 | 
			
		||||
  newValue => {
 | 
			
		||||
    setHoverRouteName(newValue);
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
watch(hoverRouteName, newValue => {
 | 
			
		||||
  emit('update:hoverRoute', newValue);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-center h-36px border-t-1px border-[#eee] cursor-pointer" @click="toggleMenu">
 | 
			
		||||
  <div class="flex-center h-36px text-[#333639] dark:text-[#ffffffd1] cursor-pointer" @click="toggleMenu">
 | 
			
		||||
    <icon-ph-caret-double-right-bold v-if="app.menu.collapsed" class="text-16px" />
 | 
			
		||||
    <icon-ph:caret-double-left-bold v-else class="text-16px" />
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,101 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="
 | 
			
		||||
      drawer-shadow
 | 
			
		||||
      absolute-lt
 | 
			
		||||
      flex-col-stretch
 | 
			
		||||
      h-full
 | 
			
		||||
      overflow-hidden
 | 
			
		||||
      transition-width
 | 
			
		||||
      duration-300
 | 
			
		||||
      ease-in-out
 | 
			
		||||
      bg-white
 | 
			
		||||
      dark:bg-[#18181c]
 | 
			
		||||
    "
 | 
			
		||||
    :style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
 | 
			
		||||
    @mouseleave="handleResetHoverRoute"
 | 
			
		||||
  >
 | 
			
		||||
    <header class="header-height flex-y-center justify-between">
 | 
			
		||||
      <h2 class="pl-8px text-16px text-primary font-bold">{{ title }}</h2>
 | 
			
		||||
 | 
			
		||||
      <div class="px-8px text-16px cursor-pointer" @click="toggleFixedMixMenu">
 | 
			
		||||
        <icon-mdi:pin-off v-if="app.menu.fixedMix" />
 | 
			
		||||
        <icon-mdi:pin v-else />
 | 
			
		||||
      </div>
 | 
			
		||||
    </header>
 | 
			
		||||
    <div class="flex-1-hidden">
 | 
			
		||||
      <n-scrollbar>
 | 
			
		||||
        <n-menu :value="activeKey" :options="childMenus" @update:value="handleUpdateMenu" />
 | 
			
		||||
      </n-scrollbar>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useRouter, useRoute } from 'vue-router';
 | 
			
		||||
import { NScrollbar, NMenu } from 'naive-ui';
 | 
			
		||||
import type { MenuOption } from 'naive-ui';
 | 
			
		||||
import { useThemeStore, useAppStore } from '@/store';
 | 
			
		||||
import { useAppTitle } from '@/hooks';
 | 
			
		||||
import { menus } from '@/router';
 | 
			
		||||
import type { GlobalMenuOption } from '@/interface';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  hoverRoute: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: ''
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['reset-hover-route']);
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const theme = useThemeStore();
 | 
			
		||||
const app = useAppStore();
 | 
			
		||||
const { toggleFixedMixMenu } = useAppStore();
 | 
			
		||||
const title = useAppTitle();
 | 
			
		||||
 | 
			
		||||
const childMenus = computed(() => {
 | 
			
		||||
  const children: MenuOption[] = [];
 | 
			
		||||
  menus.some(item => {
 | 
			
		||||
    const flag = item.routeName === props.hoverRoute && Boolean(item.children?.length);
 | 
			
		||||
    if (flag) {
 | 
			
		||||
      children.push(...item.children!);
 | 
			
		||||
    }
 | 
			
		||||
    return flag;
 | 
			
		||||
  });
 | 
			
		||||
  return children;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const showDrawer = computed(() => childMenus.value.length || app.menu.fixedMix);
 | 
			
		||||
 | 
			
		||||
const activeKey = computed(() => getActiveKey());
 | 
			
		||||
 | 
			
		||||
function getActiveKey() {
 | 
			
		||||
  return route.name as string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleResetHoverRoute() {
 | 
			
		||||
  emit('reset-hover-route');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleUpdateMenu(key: string, item: MenuOption) {
 | 
			
		||||
  const menuItem = item as GlobalMenuOption;
 | 
			
		||||
  router.push(menuItem.routePath);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headerHeight = computed(() => {
 | 
			
		||||
  const { height } = theme.headerStyle;
 | 
			
		||||
  return `${height}px`;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped>
 | 
			
		||||
.drawer-shadow {
 | 
			
		||||
  box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
 | 
			
		||||
}
 | 
			
		||||
.header-height {
 | 
			
		||||
  height: v-bind(headerHeight);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import MixMenu from './MixMenu.vue';
 | 
			
		||||
import MixMenuCollapse from './MixMenuCollapse.vue';
 | 
			
		||||
import MixMenuDrawer from './MixMenuDrawer.vue';
 | 
			
		||||
 | 
			
		||||
export { MixMenu, MixMenuCollapse };
 | 
			
		||||
export { MixMenu, MixMenuCollapse, MixMenuDrawer };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,16 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="h-full transition-width duration-500 ease-in-out"
 | 
			
		||||
    :class="[app.menu.collapsed ? 'mix-menu-collapsed-width' : 'mix-menu-width']"
 | 
			
		||||
  >
 | 
			
		||||
    <div class="flex-col-stretch h-full">
 | 
			
		||||
  <div class="flex h-full bg-white dark:bg-[#18181c]">
 | 
			
		||||
    <div
 | 
			
		||||
      class="flex-col-stretch flex-1 h-full transition-width duration-300 ease-in-out"
 | 
			
		||||
      :class="[app.menu.collapsed ? 'mix-menu-collapsed-width' : 'mix-menu-width']"
 | 
			
		||||
    >
 | 
			
		||||
      <global-logo />
 | 
			
		||||
      <div class="flex-1-hidden">
 | 
			
		||||
        <n-scrollbar>
 | 
			
		||||
          <mix-menu
 | 
			
		||||
            v-for="item in firstDegreeMenus"
 | 
			
		||||
            :key="item.routeName"
 | 
			
		||||
            v-model:hover-route="hoverRoute"
 | 
			
		||||
            :route-name="item.routeName"
 | 
			
		||||
            :label="item.label"
 | 
			
		||||
            :icon="item.icon"
 | 
			
		||||
@@ -20,17 +21,23 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <mix-menu-collapse />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
      class="relative h-full transition-width duration-300 ease-in-out"
 | 
			
		||||
      :style="{ width: app.menu.fixedMix ? theme.menuStyle.width + 'px' : '0px' }"
 | 
			
		||||
    >
 | 
			
		||||
      <mix-menu-drawer :hover-route="hoverRoute" @reset-hover-route="resetHoverRoute" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import type { VNodeChild } from 'vue';
 | 
			
		||||
import { NScrollbar } from 'naive-ui';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { useAppStore, useThemeStore } from '@/store';
 | 
			
		||||
import { menus } from '@/router';
 | 
			
		||||
import { MixMenu, MixMenuCollapse } from './components';
 | 
			
		||||
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
 | 
			
		||||
import { GlobalLogo } from '../../../common';
 | 
			
		||||
 | 
			
		||||
const theme = useThemeStore();
 | 
			
		||||
@@ -60,6 +67,11 @@ const activeParentRouteName = computed(() => {
 | 
			
		||||
  }
 | 
			
		||||
  return name;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const hoverRoute = ref('');
 | 
			
		||||
function resetHoverRoute() {
 | 
			
		||||
  hoverRoute.value = '';
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped>
 | 
			
		||||
.mix-menu-width {
 | 
			
		||||
 
 | 
			
		||||
@@ -146,6 +146,7 @@ export const customRoutes: CustomRoute[] = [
 | 
			
		||||
    name: RouteNameMap.get('exception'),
 | 
			
		||||
    path: EnumRoutePath.exception,
 | 
			
		||||
    component: BasicLayout,
 | 
			
		||||
    redirect: { name: RouteNameMap.get('exception-403') },
 | 
			
		||||
    meta: {
 | 
			
		||||
      requiresAuth: true,
 | 
			
		||||
      title: EnumRouteTitle.exception,
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,8 @@ interface AppState {
 | 
			
		||||
interface MenuState {
 | 
			
		||||
  /** 菜单折叠 */
 | 
			
		||||
  collapsed: boolean;
 | 
			
		||||
  /** 混合菜单vertical-mix是否固定二级菜单 */
 | 
			
		||||
  fixedMix: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MultiTabRoute = Partial<RouteLocationNormalizedLoaded> & {
 | 
			
		||||
@@ -40,7 +42,8 @@ const appStore = defineStore({
 | 
			
		||||
  id: 'app-store',
 | 
			
		||||
  state: (): AppState => ({
 | 
			
		||||
    menu: {
 | 
			
		||||
      collapsed: false
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      fixedMix: false
 | 
			
		||||
    },
 | 
			
		||||
    multiTab: {
 | 
			
		||||
      routes: [],
 | 
			
		||||
@@ -62,6 +65,10 @@ const appStore = defineStore({
 | 
			
		||||
    handleMenuCollapse(collapsed: boolean) {
 | 
			
		||||
      this.menu.collapsed = collapsed;
 | 
			
		||||
    },
 | 
			
		||||
    /** 设置混合菜单是否固定 */
 | 
			
		||||
    toggleFixedMixMenu() {
 | 
			
		||||
      this.menu.fixedMix = !this.menu.fixedMix;
 | 
			
		||||
    },
 | 
			
		||||
    /** 切换折叠/展开菜单 */
 | 
			
		||||
    toggleMenu() {
 | 
			
		||||
      this.menu.collapsed = !this.menu.collapsed;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user