vue 动态路由(解决刷新后白屏问题) 配置递归多级菜单栏

3,396 阅读6分钟

以前写的是写什么玩意,全是bug,现在初步修正一下内容:登录bug,动态路由的添加与清空,动态路由刷新后白屏的情况。

效果图: II22H6K8Q%SGVAM0(CL69HG.png

动态路由(前端控制):在Main.ts(我是在router里面写)里的路由守卫来做

实现了根据用户角色来获取相对应的路由,实现了权限管理。但是!一刷新问题就来了,页面直接一片空白。

网上冲浪一整天,网上相关问题解决方案我都尝试过来,感觉他们全是复制粘贴的,可行性都没有:

1.网上最多的解决方案:next({…to, replace: true }),会造成死循环,尽管解决了死循环问题,还是没用 2.将路由信息保存本地(或者利用vuex数据持久化插件vuex-persistedstate+ 数据加密插件secure-ls 一起使用的都有),可以拿到地址,但是匹配不了路由。

问题的关键:beforeEach是异步,在路由守卫之前,路由就已经匹配结束了,刷新后自然匹配不到路由,所以刷新后就是空白的(若不是,点击添加的动态路由路径也会跳转空白)。

我的解决办法:1.将所有动态路由数据中含有该角色的筛选出来,2.把动态添加路由的方法也转化成异步函数:

router => index.js 部分数据:

// 将所有动态路由中含有该角色的筛选出来
function AddRouter(res, current_role) {
    let newarr = [];
    res.forEach((item) => {
        if (item.meta && item.meta.roles.includes(current_role)) {
            newarr.push(item);
        }
    });
    // console.log(newarr);
    return newarr;
}
async function createNewRouter() {
    //前端控制路由,后端只是提供角色, 将静态路由和动态路由(根据后端获取的角色来筛选)组合在一起,然后放在vuex中去
    let DongtaiRouter = houduanluyou;
    let userInfo = JSON.parse(window.localStorage.getItem("userInfo")); //从后端获取到的角色
    console.log(userInfo);
    switch (userInfo?.name) {
        case "管理员":
            let add = AddRouter(DongtaiRouter, "admin");
            // console.log(add);
            // 动静态路由拼接放在vuex中去
            store.commit("QuietRouter", add);
            //添加动态路由
            for (let i = 0; i < add.length; i++) {
                router.addRoute(add[i]); //addRoute 接收的是对象
            }
            break;
        case "总经理":
            let add2 = AddRouter(DongtaiRouter, "manager");
            // console.log(add2);
            // 动静态路由拼接放在vuex中去
            store.commit("QuietRouter", add2);
            //添加动态路由
            for (let i = 0; i < add2.length; i++) {
                router.addRoute(add2[i]); //addRoute 接收的是对象
            }
            break;
        default:
            store.commit("QuietRouter", []);
            break;
    }
}
const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes, houduanluyou
})
async function init() {
    return new Promise((resolve, reject) => {
        createNewRouter() //添加动态路由
        console.log('初始化完成');
        resolve()
    })
}
Vue.use(Router)
init()

//全局路由守卫(跳转任何一次路由都会执行一次) 
router.beforeEach((to, from, next) => {
    if (to.path == '/') {//如果是登录页面路径,就直接next()
        next();
    } else {
        // to.matched有效防止直接更改url地址访问的情况(测试的时候最好不要用动态路由,没有角色匹配不了)
        if (to.matched.some(record => record.meta.requiresAuth)) { //判断当前要跳转的路径是否需要登录权限
            let falg = window.localStorage.getItem('falg')
            //console.log(falg);
            if (falg && falg != null) { //判断是否登录
                console.log('有权限,已登录');
                next()
            } else {
                alert('有权限,没登录');
                next({
                    path:'/',
                    query: { redirect: to.fullPath }  //在登录事件中获取新路径数据,登录成功后跳转到之前要跳转的页面
                })
            }
        }else{
            console.log('这个不需要权限,直接通过');
            next()
        }
    }
})

export { router, init } //将方法 init 抛出,在登录按钮中也要调用一次,否则上一角色退出后,另一个角色由于异步问题无法获取登录信息和路由

store => index.js 部分数据:

import createPersistedState from "vuex-persistedstate"; //数据持久化插件
import SecureLS from "secure-ls"; //数据加密插件
var ls = new SecureLS({
  encodingType: "aes",    //加密类型
  isCompression: false,   
  encryptionSecret: "encryption",   //PBKDF2值
});
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    jingtai:[],//静态路由
    routerarr:[], //静态路由+动态路由
  },
  getters: {
	  updata(state){
		  return state.routerarr
	  }
  },
  mutations: {
    QuietRouter(state,data){
     state.jingtai=router.options.routes
     state.routerarr=state.jingtai.concat(data)
    
    },
    // 退出时清空路由
    celRouters(state,data){
      state.jingtai=[]
      state.routerarr=[]
      console.log('执行了',state.routerarr);
    }
     
  },
  
  // 数据持久化(一般的数据可以实现,动态路由数据不行)
  // plugins: [createPersistedState()],
  // plugins: [
  //   createPersistedState({
  //     key: "encryptionStore",
  //     storage: {
  //       getItem: (key) => ls.get(key),
  //       setItem: (key, routerarr) => ls.set(key, routerarr),
  //       removeItem: (key) => ls.remove(key),
  //     },
  //   }),
  // ], //vuex持久化+加密
})

动态路由添加后,角色登出时,及时清空添加的动态路由:网上最多的方法就是 resetRouter,我没搞出来,可能我方法没搞对,有懂的大佬可以指点我一下,谢谢! 我是采用 location.reload() 强制刷新解决(也可直接清空vuex中的路由数据)。

递归组件(直接使用Vuex中的路由数据):

LeftMenu.vue 文件:

<template>
    <div >
        <el-row :style="row" class="tac" >
            <el-col :span="12">
                    <!-- Menu组件上提供了跳转方式 启用el-menu上的router属性;
                    第二步是设置导航的index属性,index的值就是要跳转的路径。
                    el-menu上的 :default-active="$route.path" 就是默认跳转到index的值-->
                <el-menu 
                    :style="menu"
                    :default-active="$route.path" 
                    class="el-menu-vertical-demo" 
                    router
                    :collapse="false">
                        <!-- 菜单具体项组件 将路由数据 routerarr 传递给子组件-->
                    <MenuItem :route='routerarr' ></MenuItem>
                </el-menu>
            </el-col>
        </el-row>
    </div>
</template>

MenuItem.vue 文件:

<template>
    <div>
            <el-submenu 
                :style="submenu"
                v-for="(child,index2) in route"
                :key="index2" 
                v-if="child.meta.hasSubMenu"
                :index="getPath(child.path)" >     // 这里index的值就是要跳转的路径
                <template slot="title">
                    <i :class="child.meta.icon"></i>
                    <span slot="title">{{child.meta.title}} </span>
                </template>
                    <!-- 递归组件本身 ,这里要注意使用的是 跟下面的属性name的值保持一致,相当于循环使用该组件 -->
                <MenuItem :basepath="getPath(child.path)" :route='child.children'></MenuItem>
            </el-submenu>
            <el-menu-item :style="menuitem" :index="getPath(child.path)" v-else> 
                <i :class="child.meta.icon"></i>
                 <span slot="title">{{child.meta.title}} </span>
            </el-menu-item>
        
    </div>
</template>
<script>
export default {
    name:'MenuItem',   //  递归的组件,用inport引入注册来使用则报错
    props:['route','basepath'],
    // 这个方法是用来拼接路径
    methods :{
        // routepath 为当前菜单的path值
        // getpath: 拼接 当前菜单的上一级菜单的path 和 当前菜单的path
        getPath: function(routePath){
            let path=''
            if(this.basepath==undefined){
                path =  routePath
            }else{
                path = this.basepath + (this.basepath?'/':'') + routePath
            }
            // console.log(path);
            return path
        }
    },
</script>

实现递归组件:

从 下面 的精简的代码可以看到: 一级菜单循环的是route,即最初我们HomeView.vue 文件传递给 LeftMenu.vue 文件再传递给MenuItem.vue文件的路由数据routerarr,循环出来的每一项定义为child 二级菜单循环的是child.children,循环出来的每一项定义为childRoute 三级菜单循环的是childRoute.children,循环出来的每一项定义为grandson 所以不难看出二级菜单、三级菜单循环的数据源都是前一个循环结果项的children,所以这就实现了 MenuItem.vue 文件

<!-- 一级菜单 --> 
<el-submenu v-for="child in route" v-if="child.meta.hasSubMenu"> 
    <!-- 二级菜单 --> 
    <el-submenu v-for="childRoute in child.children" v-if="childRoute.meta.hasSubMenu"> 
         <!-- 三级菜单 -->
        <el-submenu v-for="grandson in childRoute.children" v-if="grandson.meta.hasSubMenu"> 
        </el-submenu> 
    </el-submenu> 
</el-submenu>

菜单栏实现了,然后就是路由的跳转:这里我使用了NavMenu组件提供的跳转方式:使用步骤:1.启用el-menu上的router属性;2.第二步是设置菜单的路径index属性值。

1.启用el-menu上的router属性:

 <el-menu 
      :style="menu"
      :default-active="$route.path" 
      class="el-menu-vertical-demo" 
      router             
     :collapse="false">
     <!-- 菜单具体项组件 -->
     <MenuItem :route='routerarr' ></MenuItem>
 </el-menu>

2.设置菜单的index属性,index的值就是要跳转的路径:

首先我们知道每个菜单栏的路径:

首页        

员工管理    
    员工统计  index="/employee/employeeStatistics"
    员工管理  index="/employee/employeeManage"

考勤管理  
    考勤统计  index="/attendManage/attendStatistics"
    考勤列表  index="/attendManage/attendList"
    异常管理  index="/attendManage/exceptManage"

员工统计  
    员工统计  index="/timeManage/timeStatistics"
    员工统计  index="/timeManage/timeList"
        选项一  index="/timeManage/timeList/options1"
        选项二  index="/timeManage/timeList/options2"

我们代码中拿到菜单的路径是 child.path ,拿到这个值和我们每个菜单栏的路径并不符合,都缺少了上一级菜单的path值,所以我们需要将当前菜单path值传递给下一级菜单,然后将传递下来的值basepath和下一级菜单的path拼接在一起就是下一级菜单的正确路径,

完整代码如下

 <template>
    <div>
            <el-submenu 
                :style="submenu"
                v-for="(child,index2) in route"
                :key="index2" 
                v-if="child.meta.hasSubMenu"
                :index="getPath(child.path)" >
                <template slot="title">
                    <i :class="child.meta.icon"></i>
                    <span slot="title">{{child.meta.title}} </span>
                </template>
                <!-- 递归组件 -->
                <MenuItem :basepath="getPath(child.path)" :route='child.children'></MenuItem>
            </el-submenu>
            <el-menu-item :style="menuitem" :index="getPath(child.path)" v-else> 
                <i :class="child.meta.icon"></i>
                 <span slot="title">{{child.meta.title}} </span>
            </el-menu-item>
    </div>
</template>
<script>
export default {
    name:'MenuItem',
    props:['route','basepath'],
    data(){
        return{
            menuitem:{
                boxSize:'border-box',
                // paddingLeft:'40px',
            },
             submenu:{
                // paddingLeft:'20px',
                // paddingRight:'20px',
                boxSize:'border-box',
            },
        }
    },
    mounted(){
        console.log(this.route);
    },
    methods :{
        // routepath 为当前菜单的path值
        // getpath: 拼接 当前菜单的上一级菜单的path 和 当前菜单的path
        getPath: function(routePath){
            let path=''
            if(this.basepath==undefined){
                path =  routePath
            }else{
                path = this.basepath + (this.basepath?'/':'') + routePath
            }
            // console.log(path);
            return path
        }
    },
   
}
</script>

注意这个递归组件这里<MenuItem :basepath="getPath(child.path)" :route='child.children'></MenuItem>也是调用了getPath 方法来拼接,如果不调用,我们可以看到二级菜单的index值没问题,但是仔细看,发现工时管理-工时列表下的两个三级菜单index值还是有问题,缺少了工时管理这个一级菜单的path。因为basepath传递的只是上一级菜单的path,在递归二级菜单时,index的值是一级菜单的path值+二级菜单的path值;那当我们递归三级菜单时,index的值就是二级菜单的path值+三级菜单的path值,这也就是为什么工时管理-工时列表下的两个三级菜单index值存在问题。

////////////////////////////// 后面的都是废话,可不用看!

第三.来说说登录我遇到的坑吧

登录也是用的element表单提交

<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">

1.这里复制下来一定要 注意 el-from 里面的 label-width="100px" 属性,最好是删除这个属性,不然后面调整样式时有一个 margin-left='100px' 很难调整。

鸡肋:NProgress 进度条:

安装:cnpm install --save nprogress

main.js中引入:

import NProgress  from 'nprogress'
import 'nprogress/nprogress.css'

根据需求在main.js中进行一些配置:

NProgress.configure({     
  easing: 'ease',  // 动画方式    
  speed: 1000,  // 递增进度条的速度    
  showSpinner: false, // 是否显示加载ico    
  trickleSpeed: 1000, // 自动递增间隔    
  minimum: 1.0 // 初始化时的最小百分比
})

然后再main.js中全局路由守卫:

router.beforeEach((to, from , next) => {
  // 每次切换页面时,调用进度条
  NProgress.start();
  next();
});
router.afterEach(() => {  
  // 在即将进入新的页面组件前,关闭掉进度条
  NProgress.done()
})

样式:它默认显示为蓝色进度条,自定义进度条颜色,可在全局css中或在app.vue下写入自己自定义的css样式; 比如:

    /*  自定义进度条颜色 */
    #nprogress .bar {
     background: #F811B2 !important; 
  }

相关配置属性NProgress使用笔记