Explorar el Código

!260 add 支持顶部导航
Merge pull request !260 from Lau/dev

疯狂的狮子Li hace 4 meses
padre
commit
eb8bdd5655

+ 6 - 0
src/assets/styles/ruoyi.scss

@@ -282,3 +282,9 @@ h6 {
 .top-right-btn {
   margin-left: auto;
 }
+
+/* horizontal el menu */
+.el-menu--horizontal .el-menu-item .svg-icon + span,
+.el-menu--horizontal .el-sub-menu__title .svg-icon + span {
+  margin-left: 3px;
+}

+ 17 - 0
src/enums/NavTypeEnum.ts

@@ -0,0 +1,17 @@
+/**
+ * 导航栏布局枚举
+ */
+export enum NavTypeEnum {
+  /**
+   * 左侧导航
+   */
+  LEFT = 'left',
+  /**
+   * 顶部导航
+   */
+  TOP = 'top',
+  /**
+   * 混合导航
+   */
+  MIX = 'mix'
+}

+ 44 - 6
src/layout/components/Navbar.vue

@@ -1,9 +1,14 @@
 <template>
-  <div class="navbar">
+  <div class="navbar" :class="'nav' + navType">
     <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggle-click="toggleSideBar" />
-    <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
-    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
 
+    <breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
+    <top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
+
+    <template v-if="navType == NavTypeEnum.TOP">
+      <logo v-show="showLogo" :collapse="false"></logo>
+      <top-bar id="topbar-container" class="topbar-container" />
+    </template>
     <div class="right-menu flex align-center">
       <template v-if="appStore.device !== 'mobile'">
         <el-select
@@ -99,6 +104,9 @@ import { TenantVO } from '@/api/types';
 import notice from './notice/index.vue';
 import router from '@/router';
 import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
+import Logo from "@/layout/components/Sidebar/Logo.vue";
+import TopBar from './TopBar'
 
 const appStore = useAppStore();
 const userStore = useUserStore();
@@ -109,6 +117,9 @@ const newNotice = ref(<number>0);
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const userId = ref(userStore.userId);
+const navType = computed(() => settingsStore.navType);
+const showLogo = computed(() => settingsStore.sidebarLogo);
+
 const companyName = ref(undefined);
 const tenantList = ref<TenantVO[]>([]);
 // 是否切换了租户
@@ -201,6 +212,12 @@ watch(
 </script>
 
 <style lang="scss" scoped>
+.navbar.navtop {
+  .hamburger-container {
+    display: none !important;
+  }
+}
+
 :deep(.el-select .el-input__wrapper) {
   height: 30px;
 }
@@ -223,14 +240,22 @@ watch(
   position: relative;
   //background: #fff;
   box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  display: flex;
+  align-items: center;
+  // padding: 0 8px;
+  box-sizing: border-box;
 
   .hamburger-container {
     line-height: 46px;
     height: 100%;
-    float: left;
+    //float: left;
     cursor: pointer;
     transition: background 0.3s;
     -webkit-tap-highlight-color: transparent;
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    margin-right: 8px;
 
     &:hover {
       background: rgba(0, 0, 0, 0.025);
@@ -238,7 +263,8 @@ watch(
   }
 
   .breadcrumb-container {
-    float: left;
+    //float: left;
+    flex-shrink: 0;
   }
 
   .topmenu-container {
@@ -246,16 +272,28 @@ watch(
     left: 50px;
   }
 
+  .topbar-container {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    overflow: hidden;
+    margin-left: 8px;
+  }
+
+
   .errLog-container {
     display: inline-block;
     vertical-align: top;
   }
 
   .right-menu {
-    float: right;
+    //float: right;
     height: 100%;
     line-height: 50px;
     display: flex;
+    align-items: center;
+    margin-left: auto;
 
     &:focus {
       outline: none;

+ 121 - 14
src/layout/components/Settings/index.vue

@@ -1,5 +1,40 @@
 <template>
   <el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
+    <h3 class="drawer-title">菜单导航设置</h3>
+    <div class="nav-wrap">
+      <el-tooltip content="左侧菜单" placement="bottom">
+        <div
+          class="item left"
+          @click="handleNavType(NavTypeEnum.LEFT)"
+          :style="{ '--theme': theme }"
+          :class="{ activeItem: navType == NavTypeEnum.LEFT }"
+        >
+          <b></b><b></b>
+        </div>
+      </el-tooltip>
+
+      <el-tooltip content="混合菜单" placement="bottom">
+        <div
+          class="item mix"
+          @click="handleNavType(NavTypeEnum.MIX)"
+          :style="{ '--theme': theme }"
+          :class="{ activeItem: navType == NavTypeEnum.MIX }"
+        >
+          <b></b><b></b>
+        </div>
+      </el-tooltip>
+      <el-tooltip content="顶部菜单" placement="bottom">
+        <div
+          class="item top"
+          @click="handleNavType(NavTypeEnum.TOP)"
+          :style="{ '--theme': theme }"
+          :class="{ activeItem: navType == NavTypeEnum.TOP }"
+        >
+          <b></b><b></b>
+        </div>
+      </el-tooltip>
+    </div>
+
     <h3 class="drawer-title">主题风格设置</h3>
 
     <div class="setting-drawer-block-checbox">
@@ -45,13 +80,6 @@
 
     <h3 class="drawer-title">系统布局配置</h3>
 
-    <div class="drawer-item">
-      <span>开启 TopNav</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
-      </span>
-    </div>
-
     <div class="drawer-item">
       <span>开启 Tags-Views</span>
       <span class="comp-style">
@@ -101,6 +129,7 @@ import { useSettingsStore } from '@/store/modules/settings';
 import { usePermissionStore } from '@/store/modules/permission';
 import { handleThemeStyle } from '@/utils/theme';
 import { SideThemeEnum } from '@/enums/SideThemeEnum';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 import defaultSettings from '@/settings';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -113,7 +142,7 @@ const theme = ref(settingsStore.theme);
 const sideTheme = ref(settingsStore.sideTheme);
 const storeSettings = computed(() => settingsStore);
 const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
-
+const navType = ref(settingsStore.navType);
 // 是否暗黑模式
 const isDark = useDark({
   storageKey: 'useDarkKey',
@@ -130,11 +159,26 @@ watch(isDark, () => {
 });
 const toggleDark = () => useToggle(isDark);
 
-const topNavChange = (val: any) => {
-  if (!val) {
-    appStore.toggleSideBarHide(false);
-    permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
-  }
+/** 菜单导航设置 */
+watch(
+  () => navType,
+  (val: string) => {
+    if (val.value === NavTypeEnum.TOP) {
+      appStore.toggleSideBarHide(true);
+      permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
+    } else if (val.value === NavTypeEnum.LEFT) {
+      appStore.toggleSideBarHide(false);
+      permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
+    } else if (val.value === NavTypeEnum.MIX) {
+      appStore.toggleSideBarHide(false);
+    }
+  },
+  { immediate: true, deep: true }
+);
+
+const handleNavType = (val: NavTypeEnum) => {
+  settingsStore.navType = val;
+  navType.value = val;
 };
 
 const dynamicTitleChange = () => {
@@ -158,7 +202,6 @@ const handleTheme = (val: string) => {
 const saveSetting = () => {
   proxy?.$modal.loading('正在保存到本地,请稍候...');
   const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
-  settings.value.topNav = storeSettings.value.topNav;
   settings.value.tagsView = storeSettings.value.tagsView;
   settings.value.tagsIcon = storeSettings.value.tagsIcon;
   settings.value.fixedHeader = storeSettings.value.fixedHeader;
@@ -166,6 +209,7 @@ const saveSetting = () => {
   settings.value.dynamicTitle = storeSettings.value.dynamicTitle;
   settings.value.sideTheme = storeSettings.value.sideTheme;
   settings.value.theme = storeSettings.value.theme;
+  settings.value.navType = storeSettings.value.navType;
   setTimeout(() => {
     proxy?.$modal.closeLoading();
   }, 1000);
@@ -243,4 +287,67 @@ defineExpose({
     margin: -3px 8px 0px 0px;
   }
 }
+
+// 导航模式
+.nav-wrap {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  margin-top: 10px;
+  margin-bottom: 20px;
+
+  .activeItem {
+    border: 2px solid #{'var(--theme)'} !important;
+  }
+
+  .item {
+    position: relative;
+    margin-right: 16px;
+    cursor: pointer;
+    width: 56px;
+    height: 48px;
+    border-radius: 4px;
+    background: #f0f2f5;
+    border: 2px solid transparent;
+  }
+
+  .left {
+    b:first-child {
+      display: block;
+      height: 30%;
+      background: #fff;
+    }
+    b:last-child {
+      width: 30%;
+      background: #1b2a47;
+      position: absolute;
+      height: 100%;
+      top: 0;
+      border-radius: 4px 0 0 4px;
+    }
+  }
+  .mix {
+    b:first-child {
+      border-radius: 4px 4px 0 0;
+      display: block;
+      height: 30%;
+      background: #1b2a47;
+    }
+    b:last-child {
+      width: 30%;
+      background: #1b2a47;
+      position: absolute;
+      height: 70%;
+      border-radius: 0 0 0 4px;
+    }
+  }
+  .top {
+    b:first-child {
+      display: block;
+      height: 30%;
+      background: #1b2a47;
+      border-radius: 4px 4px 0 0;
+    }
+  }
+}
 </style>

+ 29 - 12
src/layout/components/Sidebar/Logo.vue

@@ -2,18 +2,17 @@
   <div
     class="sidebar-logo-container"
     :class="{ collapse: collapse }"
-    :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
   >
     <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
         <img v-if="logo" :src="logo" class="sidebar-logo" />
-        <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+        <h1 v-else class="sidebar-title">
           {{ title }}
         </h1>
       </router-link>
       <router-link v-else key="expand" class="sidebar-logo-link" to="/">
         <img v-if="logo" :src="logo" class="sidebar-logo" />
-        <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+        <h1 class="sidebar-title">
           {{ title }}
         </h1>
       </router-link>
@@ -26,6 +25,7 @@ import variables from '@/assets/styles/variables.module.scss';
 import logo from '@/assets/logo/logo.png';
 import { useSettingsStore } from '@/store/modules/settings';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 defineProps({
   collapse: {
@@ -37,6 +37,28 @@ defineProps({
 const title = import.meta.env.VITE_APP_LOGO_TITLE;
 const settingsStore = useSettingsStore();
 const sideTheme = computed(() => settingsStore.sideTheme);
+
+// 获取Logo背景色
+const getLogoBackground = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-bg)'
+  }
+  if (settingsStore.navType == NavTypeEnum.TOP) {
+    return variables.menuLightBackground
+  }
+  return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBackground
+})
+
+// 获取Logo文字颜色
+const getLogoTextColor = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-text)'
+  }
+  if (settingsStore.navType == NavTypeEnum.TOP) {
+    return variables.logoLightTitleColor
+  }
+  return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor
+})
 </script>
 
 <style lang="scss" scoped>
@@ -51,10 +73,9 @@ const sideTheme = computed(() => settingsStore.sideTheme);
 
 .sidebar-logo-container {
   position: relative;
-  width: 100%;
   height: 50px;
   line-height: 50px;
-  background: #2b2f3a;
+  background: v-bind(getLogoBackground);
   text-align: center;
   overflow: hidden;
 
@@ -67,21 +88,17 @@ const sideTheme = computed(() => settingsStore.sideTheme);
       height: 32px;
       vertical-align: middle;
       margin-right: 12px;
+      margin-left: 12px;
     }
 
     & .sidebar-title {
       display: inline-block;
       margin: 0;
-      color: #fff;
+      color: v-bind(getLogoTextColor);
       font-weight: 600;
       line-height: 50px;
       font-size: 14px;
-      font-family:
-        Avenir,
-        Helvetica Neue,
-        Arial,
-        Helvetica,
-        sans-serif;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
       vertical-align: middle;
     }
   }

+ 103 - 0
src/layout/components/TopBar/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
+    <sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
+
+    <el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
+      <template #title>
+        <span>更多菜单</span>
+      </template>
+      <sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
+    </el-sub-menu>
+  </el-menu>
+</template>
+
+<script setup>
+import SidebarItem from '../Sidebar/SidebarItem'
+import {useAppStore} from '@/store/modules/app'
+import {useSettingsStore} from '@/store/modules/settings'
+import {usePermissionStore} from '@/store/modules/permission'
+
+const route = useRoute()
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+
+const sidebarRouters = computed(() => permissionStore.sidebarRouters)
+const theme = computed(() => settingsStore.theme)
+const device = computed(() => appStore.device)
+const activeMenu = computed(() => {
+  const { meta, path } = route
+  if (meta.activeMenu) {
+    return meta.activeMenu
+  }
+  return path
+})
+
+const visibleNumber = ref(5)
+const topMenus = computed(() => {
+  return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
+})
+const moreRoutes = computed(() => {
+  return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value)
+})
+function setVisibleNumber() {
+  let width = document.body.getBoundingClientRect().width
+  if (width >= 1000) {
+    width -= 500
+  }
+  visibleNumber.value = parseInt(width / 3 / 85)
+}
+
+onMounted(() => {
+  window.addEventListener('resize', setVisibleNumber)
+})
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', setVisibleNumber)
+})
+
+onMounted(() => {
+  setVisibleNumber()
+})
+</script>
+
+<style lang="scss">
+
+/* menu item */
+#app .topbar-menu.el-menu--horizontal .el-sub-menu__title, #app .topbar-menu.el-menu--horizontal .el-menu-item {
+  padding: 0 10px !important;
+}
+
+.topbar-menu.el-menu--horizontal > .el-menu-item {
+  float: left;
+  height: 50px !important;
+  line-height: 50px !important;
+  color: #303133 !important;
+  padding: 0 5px !important;
+  margin: 0 10px !important;
+}
+
+.el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
+  color: v-bind(theme);
+}
+
+/* sub-menu item */
+.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
+  float: left;
+  line-height: 50px !important;
+  color: #303133 !important;
+  margin: 0 15px -3px!important;
+}
+
+/* topbar more arrow */
+.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
+  position: static;
+  margin-left: 8px;
+  margin-top: 0px;
+  display: block !important;
+}
+
+/* menu__title el-menu-item */
+.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
+  height: 60px;
+}
+</style>

+ 8 - 0
src/layout/index.vue

@@ -26,6 +26,7 @@ import SideBar from './components/Sidebar/index.vue';
 import { AppMain, Navbar, Settings, TagsView } from './components';
 import { useAppStore } from '@/store/modules/app';
 import { useSettingsStore } from '@/store/modules/settings';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 import { initWebSocket } from '@/utils/websocket';
 import { initSSE } from '@/utils/sse';
 
@@ -35,6 +36,13 @@ const sidebar = computed(() => useAppStore().sidebar);
 const device = computed(() => useAppStore().device);
 const needTagsView = computed(() => settingsStore.tagsView);
 const fixedHeader = computed(() => settingsStore.fixedHeader);
+const layout = computed(() => settingsStore.navType);
+
+// 根据布局模式判断是否显示侧边栏
+const showSidebar = computed(() => {
+  if (sidebar.value.hide) return false;
+  return layout.value === NavTypeEnum.LEFT || layout.value === NavTypeEnum.MIX;
+});
 
 const classObj = computed(() => ({
   hideSidebar: !sidebar.value.opened,

+ 4 - 3
src/settings.ts

@@ -1,4 +1,5 @@
 import { LanguageEnum } from '@/enums/LanguageEnum';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 const setting: DefaultSettings = {
   /**
@@ -18,9 +19,9 @@ const setting: DefaultSettings = {
   showSettings: true,
 
   /**
-   * 是否显示顶部导航
+   * 默认布局
    */
-  topNav: false,
+  navType: NavTypeEnum.LEFT,
 
   /**
    * 是否显示 tagsView
@@ -35,7 +36,7 @@ const setting: DefaultSettings = {
   /**
    * 是否固定头部
    */
-  fixedHeader: false,
+  fixedHeader: true,
 
   /**
    * 是否显示logo

+ 5 - 3
src/store/modules/settings.ts

@@ -3,6 +3,7 @@ import defaultSettings from '@/settings';
 import { useDynamicTitle } from '@/utils/dynamicTitle';
 import { useStorage } from '@vueuse/core';
 import { ref } from 'vue';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 export const useSettingsStore = defineStore('setting', () => {
   const storageSetting = useStorage<LayoutSetting>('layout-setting', {
@@ -13,13 +14,13 @@ export const useSettingsStore = defineStore('setting', () => {
     sidebarLogo: defaultSettings.sidebarLogo,
     dynamicTitle: defaultSettings.dynamicTitle,
     sideTheme: defaultSettings.sideTheme,
-    theme: defaultSettings.theme
+    theme: defaultSettings.theme,
+    navType: defaultSettings.navType
   });
   const title = ref<string>(defaultSettings.title);
   const theme = ref<string>(storageSetting.value.theme);
   const sideTheme = ref<string>(storageSetting.value.sideTheme);
   const showSettings = ref<boolean>(defaultSettings.showSettings);
-  const topNav = ref<boolean>(storageSetting.value.topNav);
   const tagsView = ref<boolean>(storageSetting.value.tagsView);
   const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
   const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
@@ -27,6 +28,7 @@ export const useSettingsStore = defineStore('setting', () => {
   const dynamicTitle = ref<boolean>(storageSetting.value.dynamicTitle);
   const animationEnable = ref<boolean>(defaultSettings.animationEnable);
   const dark = ref<boolean>(defaultSettings.dark);
+  const navType = ref<NavTypeEnum>(storageSetting.value.navType || NavTypeEnum.LEFT);
 
   const setTitle = (value: string) => {
     title.value = value;
@@ -37,7 +39,6 @@ export const useSettingsStore = defineStore('setting', () => {
     theme,
     sideTheme,
     showSettings,
-    topNav,
     tagsView,
     tagsIcon,
     fixedHeader,
@@ -45,6 +46,7 @@ export const useSettingsStore = defineStore('setting', () => {
     dynamicTitle,
     animationEnable,
     dark,
+    navType,
     setTitle
   };
 });

+ 3 - 2
src/types/global.d.ts

@@ -1,5 +1,6 @@
 import type { PropType as VuePropType, ComponentInternalInstance as ComponentInstance } from 'vue';
 import { LanguageEnum } from '@/enums/LanguageEnum';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 declare global {
   /** vue Instance */
@@ -90,9 +91,9 @@ declare global {
   }
   declare interface LayoutSetting {
     /**
-     * 是否显示顶部导航
+     * 默认布局
      */
-    topNav: boolean;
+    navType: NavTypeEnum;
 
     /**
      * 是否显示多标签导航