前言

在开发后台管理类项目中发现,前端菜单栏路由是由后端返回的动态路由,为了更好的了解实现的过程,所以研究了一下,写一下开发的思路。项目中主要使用的技术栈是vue3 、element-plus、vite、vuex


一、项目地址

项目源码请访问:github项目地址

二、项目思路描述

1.后台返回一个json格式的路由表,我这里直接写死了数据,使用Promise返回,大家可参考,也可以自己造;
2.因为后端传回来的都是字符串格式的,但是前端这里需要的是一个组件对象,所以要写个方法遍历一下,将字符串转换为组件对象;
3.利用vue-router的beforeEach、addRoutes、cookie、localStorage来配合上边两步实现效果;
4.左侧菜单栏根据拿到转换好的路由列表进行展示;

三、操作步骤

3.1拦截路由

在拦截路由之前,我们还得先定义好静态路由,然后从后端取到动态路由之后,进行合并。

静态路由

静态路由主要是登录页面和重定向页面等。代码如下

import {createRouter,createWebHistory } from 'vue-router';
import Layout from '@/layout'

//静态路由
export const constantRoutes = [
  {
    name:"/",
    path: '/',//根目录路由为/
    component: Layout,//指定使用Layout组件布局
    redirect: '/',//重定向至/home页面
    hidden:true,
    children: [{//子菜单信息
      path: '/',//路径
      name: 'home',
      component: () => import('@/views/home'),//指定组件
      meta: { title: '首页', access: 0, affix: true }
    }]
  },
  {
    path: '/login',
    name: 'login',
    hidden: true,
    component: () => import('@/views/login')
  }
];

const router =createRouter({
  history:createWebHistory(),
  routes:constantRoutes,
  //使用浏览器的回退或者前进时,重新返回时保留页面滚动位置,跳转页面的话,不触发。
  scrollBehavior(to,from,savePosition){
    if(savePosition){
      return savePosition;
    }else{
      return {top:0};
    }
  }
});

export default router;

后端动态路由

这个路由在实际项目中是通过后端接口返回,在日常的开发中可以根据后端格式写手动写死,前后端联调的时候换成接口就行了。


//获取后台路由(动态路由)
export function getRouters(){
    return new Promise((resolve,reject)=>{
        const menuList = [
            {
                "name": "Tool",
                "path": "/tool",
                // "redirect": "noRedirect",
                "component": "Layout",
                "alwaysShow": true,
                "meta": {
                    "title": "系统工具",
                    "icon": "tool",
                    "noCache": false,
                    "link": null
                },
                "children": [{
                    "name": "Build",
                    "path": "build",
                    "component": "tool/build/index",
                    "meta": {
                        "title": "表单构建",
                        "icon": "build",
                        "noCache": false,
                        "link": 'build'
                    }
                }, {
                    "name": "Gen",
                    "path": "gen",
                    "component": "tool/gen/index",
                    "meta": {
                        "title": "代码生成",
                        "icon": "code",
                        "noCache": false,
                        "link": null
                    }
                }, {
                    "name": "Swagger",
                    "path": "swagger",
                    "component": "tool/swagger/index",
                    "meta": {
                        "title": "系统接口",
                        "icon": "swagger",
                        "noCache": false,
                        "link": null
                    }
                }]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"/organization",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"组织管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":null
                },
                "children":[
                    {
                        "name":"Organization",
                        "path":"organization",
                        "hidden":false,
                        "component":"organization/index",
                        "meta":{
                            "title":"组织管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"organization"
                        }
                    }
                ]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"部门管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":''
                },
                "children":[
                    {
                        "name":"Department",
                        "path":"department",
                        "hidden":false,
                        "component":"department/index",
                        "meta":{
                            "title":"部门管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"department"
                        }
                    }
                ]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"岗位管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":''
                },
                "children":[
                    {
                        "name":"Station",
                        "path":"station",
                        "hidden":false,
                        "component":"station/index",
                        "meta":{
                            "title":"岗位管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"station"
                        }
                    }
                ]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"应用管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":''
                },
                "children":[
                    {
                        "name":"Application",
                        "path":"application",
                        "hidden":false,
                        "component":"application/index",
                        "meta":{
                            "title":"应用管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"application"
                        }
                    }
                ]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"菜单管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":''
                },
                "children":[
                    {
                        "name":"Menulist",
                        "path":"menulist",
                        "hidden":false,
                        "component":"menuList/index",
                        "meta":{
                            "title":"菜单管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"menulist"
                        }
                    }
                ]
            },
            {
                "name":"DayRecord",
                "path":"/dayRecord",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                "alwaysShow":"true",
                "meta":{
                    "title":"日志管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":'dayRecord'
                },
                "children":[
                    {
                        "name":"LoginRecord",
                        "path":"loginRecord",
                        "hidden":false,
                        "component":"dayRecord/loginRecord/index",
                        "meta":{
                            "title":"登录日志",
                            "icon":"tool",
                            "nocache":false,
                            "link":"loginRecord"
                        }
                    },
                    {
                        "name":"HandleRecord",
                        "path":"handleRecord",
                        "hidden":false,
                        "component":"dayRecord/handleRecord/index",
                        "meta":{
                            "title":"操作日志",
                            "icon":"tool",
                            "nocache":false,
                            "link":"handleRecord"
                        }
                    },
                ]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"应用组管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":''
                },
                "children":[
                    {
                        "name":"AppGroup",
                        "path":"appGroup",
                        "hidden":false,
                        "component":"appGroup/index",
                        "meta":{
                            "title":"应用组管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"appGroup"
                        }
                    },
                    {
                        "name":"AssignUser",
                        "path":"assignUser/:id",
                        "hidden":true,
                        "component":"appGroup/assignUser/index",
                        "meta":{
                            title:"分配用户",
                            "link":"assignUser/:id"
                        }
                    }
                ]
            },
            {
                "name":"",
                "path":"",
                "hidden":false,
                "redirect":"noRedirect",
                "component":"Layout",
                // "alwaysShow":"true",
                "meta":{
                    "title":"用户管理",
                    "icon":"tool",
                    "nocache":false,
                    "link":''
                },
                "children":[
                    {
                        "name":"UserList",
                        "path":"userList",
                        "hidden":false,
                        "component":"user/index",
                        "meta":{
                            "title":"用户管理",
                            "icon":"tool",
                            "nocache":false,
                            "link":"userList"
                        }
                    }
                ]
            },

          ]
        resolve(menuList);
    })
}

3.2登录表单验证

登录页面主要涉及密码的处理,使用的加密方法是crypto-js然后登录之后设置对应的token到浏览器作为判断是否登录的条件,同时前端可以加一个token过期时间。

<template>
  <div class="app-container">
    <div class="app-content">
      <div class="app-image">产业大脑项目</div>
      <el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="rules" label-width="120px" class="demo-ruleForm">
        <el-form-item label="用户账号" prop="username">
          <el-input v-model="ruleForm.username" type="username" autocomplete="off" />
        </el-form-item>
        <el-form-item label="用户密码" prop="password">
          <el-input v-model="ruleForm.password" type="password" autocomplete="off"/>
        </el-form-item>
        <el-form-item label="验证码" prop="code">
          <el-input v-model="ruleForm.code" />
        </el-form-item>
        <el-form-item prop="remember">
          <el-checkbox v-model="ruleForm.remember">记住密码</el-checkbox>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitForm()">登录</el-button>
          <el-button @click="resetForm()">没有注册?,去注册</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>
<script setup>
import {toRefs,getCurrentInstance,reactive} from 'vue';
import {useStore} from "vuex";
import {useRouter} from "vue-router";
import Cookies from 'js-cookie';
import {encrypt} from "../../utils/jsencrypt.js";

const store = useStore();
const router = useRouter();
const {proxy} = getCurrentInstance();
const data = reactive({
  ruleForm:{
    username:'',
    password:"",
    code:'',
    uuid:1,
    remember:false
  },
  rules:{
    username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
    password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
  }
});

function submitForm(){
  proxy.$refs['ruleFormRef'].validate(valid=>{
    if(valid){
      if(ruleForm.value.remember){
        // Cookies.set('username',ruleForm.value.username,{exprise:30})
        // Cookies.set('password',encrypt(ruleForm.value.password),{exprise:30});
        // Cookies.set('remember',ruleForm.value.remember,{exprise:30});
      }else{
        //移除
        Cookies.remove('username');
        Cookies.remove('password');
        Cookies.remove('remember');
      }
      ruleForm.value.password = encrypt(ruleForm.value.password);
      //调用 store里面actions登录方法
      store.dispatch('Login',ruleForm.value).then(res=>{
        console.log(res);
        if(res.code === 200){
          router.push('/');
        }else{
          router.push('/login');
          // proxy.resetForm('ruleFormRef');不起作用,不知道为啥。
        }
      });
      // store.dispatch('GenerateRoutes').then(accessRoutes=>{})
    }
  })
}
function resetForm(){

}
const {rules,ruleForm} = toRefs(data);

</script>

<style lang="scss" scoped>
  .app-container{
    width: 100%;
    height: 100%;
    .app-content{
      position: relative;
      width: 30%;
      height: 200px;
      margin: 0 auto;
      padding-top:100px;
      .app-image{
        position: absolute;
        top: 42px;
        left: 230px;
        font-weight: 700;
        color: blue;
      }
    }
  }

</style>

3.3permission.js文件

该文件要引入到main.js文件中使用,可以看到在permission中也调用了store中的处理后端路由的方法,这个跟login中的不同的点就是,登录是处理路由和用户信息来实现登录,而permission.js中则是用来使用路由拦截的方式,如果没有登录而是在地址栏直接输入对应的地址的话,可以进行拦截判断是否合法登录,获取当前的用户信息和路由权限,如果没有权限获取没有用户信息和错误就会退出登录。重新登录

import router from './router'
import store from './store'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'

NProgress.configure({ showSpinner: false });

//白名单,不进行拦截处理
const whiteList = ['/login', '/auth-redirect', '/bind', '/register'];

//路由拦截
router.beforeEach((to, from, next) => {
    NProgress.start()
    if (getToken()) {
        to.meta.title && store.dispatch('setTitle', to.meta.title)
        /* has token*/
        if (to.path === '/login') {
            console.log(111)
            next({ path: '' })
            NProgress.done()
        } else {
            if (store.getters.roles.length === 0) {
                // isRelogin.show = true
                // 判断当前用户是否已拉取完user_info信息
                store.dispatch('GetInfo').then(() => {
                    // isRelogin.show = false
                    store.dispatch('GenerateRoutes').then(accessRoutes => {
                        // 根据roles权限生成可访问的路由表
                        accessRoutes.forEach(route => {
                            if (!isHttp(route.path)) {
                                router.addRoute(route) // 动态添加可访问路由表
                            }
                        })
                        next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
                    })
                }).catch(err => {
                	//捕捉错误,退出登录
                    store.dispatch('LogOut').then(() => {
                        ElMessage.error(err)
                        next({ path: '/' })
                    })
                })
            } else {
                next()
            }
        }
    } else {
        // 没有token
        if (whiteList.indexOf(to.path) !== -1) {
            // 在免登录白名单,直接进入
            next()
        } else {
            next(`/login`) // 否则全部重定向到登录页
            NProgress.done()
        }
    }
})

router.afterEach(() => {
    NProgress.done()
})

3.4取后台路由并处理

处理后端路由,通过调用vuex里的GenerateRoutes方法,处理对应的路由信息,因为方法稍微负责,在这里就不细说了。

import { constantRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout'
// 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue')

const permission = {
    state: {
        routes: [],
        addRoutes: [],
        defaultRoutes: [],
        topbarRouters: [],
        sidebarRouters: []
    },
    mutations: {
        SET_ROUTES: (state, routes) => {
            state.addRoutes = routes
            state.routes = constantRoutes.concat(routes)
        },
        SET_DEFAULT_ROUTES: (state, routes) => {
            state.defaultRoutes = constantRoutes.concat(routes)
        },
        SET_TOPBAR_ROUTES: (state, routes) => {
            state.topbarRouters = routes
        },
        SET_SIDEBAR_ROUTERS: (state, routes) => {
            state.sidebarRouters = routes;
        },
    },
    actions: {
        // 生成路由
        GenerateRoutes({ commit }) {
            return new Promise(resolve => {
                getRouters().then((res) => {
                    console.log(res);
                    const sdata = JSON.parse(JSON.stringify(res))
                    const rdata = JSON.parse(JSON.stringify(res))
                    const defaultData = JSON.parse(JSON.stringify(res))
                    const sidebarRoutes = filterAsyncRouter(sdata)
                    const rewriteRoutes = filterAsyncRouter(rdata, false, true)
                    const defaultRoutes = filterAsyncRouter(defaultData)
                    commit('SET_ROUTES', rewriteRoutes)
                    commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
                    commit('SET_DEFAULT_ROUTES', sidebarRoutes)
                    commit('SET_TOPBAR_ROUTES', defaultRoutes)
                    resolve(rewriteRoutes);
                })
            })
        }
    }
}

// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
    return asyncRouterMap.filter(route => {
        if (type && route.children) {
            route.children = filterChildren(route.children)
        }
        if (route.component) {
            // Layout ParentView 组件特殊处理
            if (route.component === 'Layout') {
                route.component = Layout
            } else if (route.component === 'ParentView') {
                // route.component = ParentView
                console.log('ParentView')
            } else if (route.component === 'InnerLink') {
                // route.component = InnerLink
                console.log('InnerLink')
            } else {
                route.component = loadView(route.component)
            }
        }
        if (route.children != null && route.children && route.children.length) {
            route.children = filterAsyncRouter(route.children, route, type)
        } else {
            delete route['children']
            delete route['redirect']
        }
        return true
    })
}

function filterChildren(childrenMap, lastRouter = false) {
    var children = []
    childrenMap.forEach((el, index) => {
        if (el.children && el.children.length) {
            if (el.component === 'ParentView' && !lastRouter) {
                el.children.forEach(c => {
                    c.path = el.path + '/' + c.path
                    if (c.children && c.children.length) {
                        children = children.concat(filterChildren(c.children, c))
                        return
                    }
                    children.push(c)
                })
                return
            }
        }
        if (lastRouter) {
            el.path = lastRouter.path + '/' + el.path
        }
        children = children.concat(el)
    })
    return children
}
//这一步是取出来view里面的文件找到对应文件的懒加载函数,并执行。
export const loadView = (view) => {
    let res;
    for (const path in modules) {
        const dir = path.split('views/')[1].split('.vue')[0];
        if (dir === view) {
            res = () => modules[path]();
        }
    }
    return res;
}

export default permission

3.5 layout布局

layout布局是侧边栏菜单显示的重要的一步。

整体布局

//路径:src/layout/index.vue
<template>
  <el-container class="app-wrapper" >
      <el-aside :width="asideWidth" class="sidebar-container">
        <div style="text-align:center;margin-top: 15px;color: #FFFFFF;width: 100%;height: 80px">
          <el-icon><Avatar /></el-icon><div v-if="$store.getters.sidebarType" style="margin-left: 10px">后台管理系统</div>
        </div>
        <Menu />
      </el-aside>
      <el-container class="container" :class="{hidderContainer:!$store.getters.sidebarType}">
        <el-header class="header-container">
          <Header/>
        </el-header>
        <el-main style="background: #fff;margin: 0 15px">
          <router-view></router-view>
        </el-main>
      </el-container>
  </el-container>
</template>

<script setup>
import Menu from './Menu'
import {computed, ref} from 'vue'
import Header from './header/index'
import {useStore} from "vuex";
const store = useStore();

const asideWidth = computed(()=>{
  return store.getters.sidebarType === true? '180px' : '67px'
})
</script>


<style lang="scss" scoped>
.app-wrapper{
  width: 100vw;
  height:100vh;
  background: #f5f5f5;
  margin: unset;
}
.app-container {
  position: relative;
  width: 100%;
  height: 100%;
}
.container {
  width: calc(100% - $sideBarWidth);
  height: 100%;

  position: fixed;
  top: 0;
  right: 0;
  z-index: 9;
  transition: all 0.28s;
  .header-container{
    height: 50px;
    line-height: 50px;
    background: #fff;
    margin-bottom: 15px;
    padding: 0 10px;
    ::v-deep .el-breadcrumb{
      line-height: unset;
    }
  }
  &.hidderContainer {
    width: calc(100% - $hideSideBarWidth);
  }
}
::v-deep .el-header {
  padding: 0;
}
::v-deep .el-sub-menu .el-menu-item{
  min-width: unset;
}

.el-aside {
  height: 100vh;
  overflow-y: auto;
  -ms-overflow-style: none; /* Edge */
  scrollbar-width: none; /* Firefox */
  &::-webkit-scrollbar {
    display: none; /* WebKit */
  }
}
</style>

菜单栏

//路径:src/layout/menu.vue
<template>
  <el-menu
      active-text-color="#ffd04b"
      background-color="#545c64"
      class="el-menu-vertical-demo"
      :default-active="defaultRouter"
      text-color="#fff"
      router
      unique-opened
      :collapse="!$store.getters.sidebarType"
  >
    <el-sub-menu :index="(index+1).toString()" v-for="(item,index) in menusList" :key="index">
      <template #title>
        <el-icon>
          <component :is="iconList[index]"></component>
        </el-icon>
        <span>{{ item.meta.title }}</span>
      </template>
      <el-menu-item
          :index= "item.path + '/' + it.path"
          v-for="(it,index) in item.children"
          :key="index"
          @click="savePath(item.path,it.path)"
      >
          <template #title>
            <el-icon>
              <component :is="icon[index]"></component>
            </el-icon>
            <span>{{ it.meta.title }}</span>
          </template>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<script setup>
import { getRouters } from '@/api/menu'
import { ref } from 'vue'
const iconList = ref(['user','setting','shop','tickets','pie-chart','Bell','checked','chicken','coin']);
const icon = ref(['menu','Edit','Files','folder','fold']);
const defaultRouter = ref(sessionStorage.getItem('path')|| '/tool/build');
const menusList = ref([]);
const initMenusList = async () => {
  menusList.value = await getRouters()
}

function savePath(x,y){
  console.log(x,y);
  sessionStorage.setItem('path',`${x}/${y}`);
}
initMenusList()
</script>

<style lang="scss" scoped></style>

头部实现

//路径:src/layout/header/index
<template>
  <div class="nav">
    <hamburger/>
    <bread-crumb/>
    <div class="nav-right">
      <avatar/>
    </div>
  </div>
</template>

<script setup>
import Hamburger from './components/hamburger'
import BreadCrumb from "./components/breadCrumb";
import avatar from './components/avatar'
// 自定义图标


</script>

<style lang="scss" scoped>
.nav{
  height: 50px;
  line-height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  .nav-right{
    flex: 1;
    display: flex;
    justify-content: flex-end;
    align-items: center;

  }
}

</style>



3.6其他功能

其他功能可以从github项目中下载,查看具体的代码。

四、效果图

效果图

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐