RuoYi
2025-12-16 839e4f35f562395a3ce59bee6be16f526bb62079
菜单导航设置支持纯顶部
1 files added
9 files modified
334 ■■■■ changed files
ruoyi-ui/src/assets/styles/ruoyi.scss 12 ●●●●● patch | view | raw | blame | history
ruoyi-ui/src/assets/styles/sidebar.scss 2 ●●● patch | view | raw | blame | history
ruoyi-ui/src/components/Breadcrumb/index.vue 1 ●●●● patch | view | raw | blame | history
ruoyi-ui/src/components/TopNav/index.vue 6 ●●●● patch | view | raw | blame | history
ruoyi-ui/src/layout/components/Navbar.vue 54 ●●●● patch | view | raw | blame | history
ruoyi-ui/src/layout/components/Settings/index.vue 133 ●●●● patch | view | raw | blame | history
ruoyi-ui/src/layout/components/Sidebar/Logo.vue 10 ●●●●● patch | view | raw | blame | history
ruoyi-ui/src/layout/components/TopBar/index.vue 108 ●●●●● patch | view | raw | blame | history
ruoyi-ui/src/settings.js 4 ●●●● patch | view | raw | blame | history
ruoyi-ui/src/store/modules/settings.js 4 ●●●● patch | view | raw | blame | history
ruoyi-ui/src/assets/styles/ruoyi.scss
@@ -130,6 +130,18 @@
  border-radius: 4px;
}
/* el menu */
.el-menu-item,
.el-sub-menu {
  .svg-icon + span {
    margin-left: 5px;
  }
}
.el-menu--horizontal .el-menu--popup {
  min-width: 120px !important;
}
@media (max-width: 768px) {
  .pagination-container .el-pagination > .el-pagination__jump {
    display: none !important;
ruoyi-ui/src/assets/styles/sidebar.scss
@@ -61,7 +61,7 @@
    }
    .svg-icon {
      margin-right: 16px;
      margin-right: 10px !important;
    }
    .el-menu {
ruoyi-ui/src/components/Breadcrumb/index.vue
@@ -94,7 +94,6 @@
  display: inline-block;
  font-size: 14px;
  line-height: 50px;
  margin-left: 8px;
  .no-redirect {
    color: #97a8be;
    cursor: text;
ruoyi-ui/src/components/TopNav/index.vue
@@ -162,7 +162,7 @@
        this.$store.dispatch('app/toggleSideBarHide', true)
      }
    }
  },
  }
}
</script>
@@ -171,7 +171,7 @@
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #999093 !important;
  color: #303133 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
}
@@ -186,7 +186,7 @@
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #999093 !important;
  color: #303133 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
}
ruoyi-ui/src/layout/components/Navbar.vue
@@ -1,10 +1,13 @@
<template>
  <div class="navbar">
  <div class="navbar" :class="'nav' + navType">
    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container" />
    <top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
    <breadcrumb v-if="navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
    <top-nav v-if="navType == 2" id="topmenu-container" class="topmenu-container" />
    <template v-if="navType == 3">
      <logo v-show="showLogo" :collapse="false"></logo>
      <top-bar id="topbar-container" class="topbar-container" />
    </template>
    <div class="right-menu">
      <template v-if="device!=='mobile'">
        <search id="header-search" class="right-menu-item" />
@@ -50,6 +53,8 @@
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import TopBar from './TopBar'
import Logo from './Sidebar/Logo'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
@@ -61,7 +66,9 @@
  emits: ['setLayout'],
  components: {
    Breadcrumb,
    Logo,
    TopNav,
    TopBar,
    Hamburger,
    Screenfull,
    SizeSelect,
@@ -81,9 +88,14 @@
        return this.$store.state.settings.showSettings
      }
    },
    topNav: {
    navType: {
      get() {
        return this.$store.state.settings.topNav
        return this.$store.state.settings.navType
      }
    },
    showLogo: {
      get() {
        return this.$store.state.settings.sidebarLogo
      }
    }
  },
@@ -110,20 +122,33 @@
</script>
<style lang="scss" scoped>
.navbar.nav3 {
  .hamburger-container {
    display: none !important;
  }
}
.navbar {
  height: 50px;
  overflow: hidden;
  position: relative;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,21,41,.08);
  display: flex;
  align-items: center;
  // padding: 0 8px;
  box-sizing: border-box;
  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .3s;
    -webkit-tap-highlight-color:transparent;
    display: flex;
    align-items: center;
    flex-shrink: 0;
    margin-right: 8px;
    &:hover {
      background: rgba(0, 0, 0, .025)
@@ -131,12 +156,21 @@
  }
  .breadcrumb-container {
    float: left;
    flex-shrink: 0;
  }
  .topmenu-container {
    position: absolute;
    left: 50px;
  }
  .topbar-container {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    overflow: hidden;
    margin-left: 8px;
  }
  .errLog-container {
@@ -145,9 +179,11 @@
  }
  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;
    display: flex;
    align-items: center;
    margin-left: auto;
    &:focus {
      outline: none;
ruoyi-ui/src/layout/components/Settings/index.vue
@@ -4,6 +4,27 @@
      <div>
        <div class="setting-drawer-content">
          <div class="setting-drawer-title">
            <h3 class="drawer-title">菜单导航设置</h3>
          </div>
          <div class="nav-wrap">
            <el-tooltip content="左侧菜单" placement="bottom">
              <div class="item left" @click="handleNavType(1)" :style="{'--theme': theme}" :class="{ activeItem: navType == 1 }">
                <b></b><b></b>
              </div>
            </el-tooltip>
            <el-tooltip content="混合菜单" placement="bottom">
              <div class="item mix" @click="handleNavType(2)" :style="{'--theme': theme}" :class="{ activeItem: navType == 2 }">
                <b></b><b></b>
              </div>
            </el-tooltip>
            <el-tooltip content="顶部菜单" placement="bottom">
              <div class="item top" @click="handleNavType(3)" :style="{'--theme': theme}" :class="{ activeItem: navType == 3 }">
                <b></b><b></b>
              </div>
            </el-tooltip>
          </div>
          <div class="setting-drawer-title">
            <h3 class="drawer-title">主题风格设置</h3>
          </div>
          <div class="setting-drawer-block-checbox">
@@ -38,11 +59,6 @@
        <el-divider/>
        <h3 class="drawer-title">系统布局配置</h3>
        <div class="drawer-item">
          <span>开启 TopNav</span>
          <el-switch v-model="topNav" class="drawer-switch" />
        </div>
        <div class="drawer-item">
          <span>开启 Tags-Views</span>
@@ -93,6 +109,7 @@
    return {
      theme: this.$store.state.settings.theme,
      sideTheme: this.$store.state.settings.sideTheme,
      navType: this.$store.state.settings.navType,
      showSettings: false
    }
  },
@@ -106,21 +123,6 @@
          key: 'fixedHeader',
          value: val
        })
      }
    },
    topNav: {
      get() {
        return this.$store.state.settings.topNav
      },
      set(val) {
        this.$store.dispatch('settings/changeSetting', {
          key: 'topNav',
          value: val
        })
        if (!val) {
          this.$store.dispatch('app/toggleSideBarHide', false)
          this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes)
        }
      }
    },
    tagsView: {
@@ -180,6 +182,25 @@
      }
    }
  },
  watch: {
    navType: {
      handler(val) {
        if (val == 1) {
          this.$store.dispatch("app/toggleSideBarHide", false)
        }
        if (val == 2) {
        }
        if (val == 3) {
          this.$store.dispatch("app/toggleSideBarHide", true)
        }
        if ([1, 3].includes(val)) {
          this.$store.commit("SET_SIDEBAR_ROUTERS",this.$store.state.permission.defaultRoutes)
        }
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    themeChange(val) {
      this.$store.dispatch('settings/changeSetting', {
@@ -195,6 +216,13 @@
      })
      this.sideTheme = val
    },
    handleNavType(val) {
      this.$store.dispatch('settings/changeSetting', {
        key: 'navType',
        value: val
      })
      this.navType = val
    },
    openSetting() {
      this.showSettings = true
    },
@@ -206,7 +234,7 @@
      this.$cache.local.set(
        "layout-setting",
        `{
            "topNav":${this.topNav},
            "navType":${this.navType},
            "tagsView":${this.tagsView},
            "tagsIcon":${this.tagsIcon},
            "fixedHeader":${this.fixedHeader},
@@ -295,4 +323,67 @@
      float: right
    }
  }
// 导航模式
.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>
ruoyi-ui/src/layout/components/Sidebar/Logo.vue
@@ -1,13 +1,13 @@
<template>
  <div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
  <div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' && navType !== 3 ? variables.menuBackground : variables.menuLightBackground }">
    <transition name="sidebarLogoFade">
      <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 }">{{ title }} </h1>
        <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' && navType !== 3 ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ 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 }">{{ title }} </h1>
        <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' && navType !== 3 ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
      </router-link>
    </transition>
  </div>
@@ -31,6 +31,9 @@
    },
    sideTheme() {
      return this.$store.state.settings.sideTheme
    },
    navType() {
      return this.$store.state.settings.navType
    }
  },
  data() {
@@ -54,7 +57,6 @@
.sidebar-logo-container {
  position: relative;
  width: 100%;
  height: 50px;
  line-height: 50px;
  background: #2b2f3a;
ruoyi-ui/src/layout/components/TopBar/index.vue
New file
@@ -0,0 +1,108 @@
<template>
  <el-menu class="topbar-menu" :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-submenu index="more" class="el-submenu__hide-arrow" v-if="moreRoutes.length > 0">
      <template slot="title">更多菜单</template>
      <sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
    </el-submenu>
  </el-menu>
</template>
<script>
import SidebarItem from '../Sidebar/SidebarItem'
export default {
  components: { SidebarItem },
  data() {
    return {
      // 顶部栏初始数
      visibleNumber: 5
    }
  },
  computed: {
    theme() {
      return this.$store.state.settings.theme
    },
    topMenus() {
      return this.$store.state.permission.sidebarRouters.filter((f) => !f.hidden).slice(0, this.visibleNumber)
    },
    moreRoutes() {
      const sidebarRouters = this.$store.state.permission.sidebarRouters;
      return sidebarRouters.filter((f) => !f.hidden).slice(this.visibleNumber, sidebarRouters.length - this.visibleNumber)
    },
    // 默认激活的菜单
    activeMenu() {
      const { meta, path } = this.$route
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
  },
  beforeMount() {
    window.addEventListener('resize', this.setVisibleNumber)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.setVisibleNumber)
  },
  mounted() {
    this.setVisibleNumber()
  },
  methods: {
    // 根据宽度计算设置显示栏数
    setVisibleNumber() {
      const width = document.body.getBoundingClientRect().width / 3
      this.visibleNumber = parseInt(width / 85)
    }
  }
}
</script>
<style lang="scss">
.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-menu-item.is-active .svg-icon + span, .el-submenu.is-active .svg-icon + span{
  color: v-bind(theme);
}
.el-menu--horizontal .el-menu .el-menu-item, .el-menu--horizontal .el-menu .el-submenu__title {
  color: #303133 !important;
}
/* submenu item */
.topbar-menu.el-menu--horizontal > .el-submenu .el-submenu__title {
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #303133 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
}
/* 图标右间距 */
.topbar-menu .svg-icon {
  margin-right: 4px;
}
/* topbar more arrow */
.topbar-menu .el-submenu .el-submenu__icon-arrow {
  position: static;
  vertical-align: middle;
  margin-left: 8px;
  margin-top: 0px;
}
/* menu__title el-menu-item */
.topbar-menu.el-menu--horizontal .el-submenu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
  height: 55px;
  padding: 0 15px;
}
</style>
ruoyi-ui/src/settings.js
@@ -15,9 +15,9 @@
  showSettings: true,
  /**
   * 是否显示顶部导航
   * 菜单导航模式 1、纯左侧 2、混合(左侧+顶部) 3、纯顶部
   */
  topNav: false,
  navType: 1,
  /**
   * 是否显示 tagsView
ruoyi-ui/src/store/modules/settings.js
@@ -1,7 +1,7 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
const { sideTheme, showSettings, topNav, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const { sideTheme, showSettings, navType, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
@@ -9,7 +9,7 @@
  theme: storageSetting.theme || '#409EFF',
  sideTheme: storageSetting.sideTheme || sideTheme,
  showSettings: showSettings,
  topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
  navType: storageSetting.navType === undefined ? navType : storageSetting.navType,
  tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
  tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
  fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,