Vue项目实现动态路由和动态导航菜单栏

了解vuex和vue ls

vuex:
用于前端存储数据,Vuex是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。它通过在内存中存储数据来实现数据的存储和共享。 Vuex支持存储任意类型的数据,包括对象、数组等。Vuex中的状态存储是响应式的,当Vue组件从store中读取状态时,若store中的状态发生变化,相应的组件也会得到更新。而全局对象的状态存储是静态的,无法自动更新组件中的状态
网页刷新可能会导致Vuex状态丢失的问题
Vue-ls 是 Vue 的一个插件,用于操作 Local Storage(本地存储)、Session Storage(会话存储)、Memory(内存存储)

//npm版本安装
npm install vue-ls --save
npm install vuex --save

//yarn版本安装
npm install -g yarn  //已安装可以省略
yarn install //初始化
yarn add vuex//安装vuex
yarn add vue-ls

了解vuex的使用

有篇大佬博文写的非常好推荐去看https://blog.csdn.net/qq_30769437/article/details/126202906

main.js中

import Vue from 'vue'
import Storage from 'vue-ls';
import router from './routers/index'
Vue.use(Storage);//使用vue-ls插件
//省略其他
new Vue({
   router:router,
   store ,
   created: bootstrap,
  render: h => h(App),
}).$mount('#app')

//举例vuex的用法  user.js
import Vue from 'vue'
import { login, getInfo, logout } from '@/api/login'
import { postAction, getAction, deleteAction, putAction, downFilePost, downFileTimeOut } from "@/api/manage"
import moment from 'moment';
import { ACCESS_TOKEN } from '@/store/mutation-types'
import {resetRouter} from '@/routers/index';

const publicKey = process.env.VUE_APP_LOGIN_PUBLIC_KEY;

//声明了一个存储对象
const user = {
  state: {
    token: '',
    name: '',
    roles: [],
    permissions:[],
    address:'',
  },
  //这是一个对象赋值的方法 通过  commit('SET_NAME', '') 这种调用
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_NAME: (state, name ) => {
      state.name = name
    },
    SET_ROLES: (state, roles) => {
      state.roles = roles
    },
    SET_PERMISSIONS:(state, permissions) =>{
      state.permissions = permissions;
    },
    SET_ADDRESS:(state, address) =>{
      state.address = address;
    }
  },
 //响应外部vue组件的动作处理数据 
 // 通过外部vue中的方法调用
 //...mapActions(['Login', 'Logout']),
 //store.dispatch('store.dispatch','')
  actions: {
    // 登录
    Login ({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        var loginUr='/bookstore/auth/login'
        console.log("param:userInfo",userInfo);
        postAction(loginUr, userInfo)
        .then(res => {
          if (res.code == 200) {
            var result=res.result;
            Vue.ls.set(ACCESS_TOKEN, result.token)
            commit('SET_NAME', result.name)
            commit('SET_ADDRESS', result.address)
            resolve(res)
          } else {
            reject(res)
          }
        }).catch(error => {
          reject(error)
        })
      })
    },
    // 登出
    Logout ({ commit, state }) {
      return new Promise((resolve) => {
        var loginUr='/bookstore/auth/logout'
        getAction(loginUr, {})
        .then(res => {
            resolve()
        }).catch(error => {
          reject(error)
        }).finally(() => {
          commit('SET_TOKEN', '')
          commit('SET_NAME', '')
          commit('SET_ADDRESS', '')
          commit('SET_ROUTERS', [])
          // commit('SET_PERMISSIONS',[])
          Vue.ls.remove(ACCESS_TOKEN)
          resetRouter()
        })
      
      })
    },
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        var loginUr='/bookstore/auth/info'
        getAction(loginUr, {})
        .then(res => {
          if (res.code == 200) {
            var result=res.result;
            resolve(result)
          } else {
            reject(res)
          }
        }).catch(error => {
          reject(error)
        })
      })
    }
  }
}

export default user



store目录index.js

import Vue from 'vue'
import Vuex from 'vuex'
import { constantRouterMap } from '@/config/router.config'//定义的初始路由
import { generatorDynamicRouter } from '@/routers/generator-routers'//
import user from './modules/user'
import permission from './modules/async-router'

import getters from './getters'

//必须先使用否则会报错
Vue.use(Vuex)
export default new Vuex.Store({
  modules: {
    user,
  },
  state: {},
  mutations: {},
  actions: {},
  getters
})

getter.js

//作用就是为了方便
//import store from './store'
//通过store.getters.addRouters可以直接获取state.permission.addRouters
const getters = {
  token: state => state.user.token,
  name: state => state.user.name,
  userInfo: state => state.user.info,
  addRouters: state => state.permission.addRouters,
  routers: state => state.permission.routers,
  currentPrescriptionId: state => state.doctorStation.currentPrescriptionId
}

export default getters

Login.vue中调用

import 'moment/locale/zh-cn';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
import { postAction, getAction, deleteAction, putAction, downFilePost, downFileTimeOut } from "@/api/manage"
import moment from 'moment';
import { mapState } from 'vuex'
import { mapActions } from 'vuex'


export default {
  name: 'Login',
  data () {
    return {
      labelCol: {
        xs: { span: 4, offset: 2 },
        sm: { span: 4, offset: 2 }
      },
      wrapperCol: {
        xs: { span: 10 },
        sm: { span: 10 }
      },
      url: {
        login: '/auth/login',
        nvl: '/auth/nvl'
      },
      userForm: {
        pass: '',
        name: '',
      },
      rules: {
        pass: [
          { required: true, message: '密码不能为空', trigger: 'blur' },
          { min: 6, max: 12, message: '密码长度必须是6-12位', trigger: 'blur' }
        ],
        name: [
          { required: true, message: '账号不能为空', trigger: 'change' },
          { min: 3, message: '账号长度必须大于3位', trigger: 'blur' }],
      },
    }

  },
  mounted: function () {
  },
  computed: {
  //固定写法获取state中的数据
    ...mapState({
      // 动态主路由
      mainMenu1 (state) {
        return state.menuRouters
      }
    }),
  },
  methods: {
    //vuex 中定义好的方法
    ...mapActions(['Login', 'Logout']),//
    //登录成功
    loginSuccess (res) {
      console.log("登录完毕即将跳转")
      this.$router.push({ path: '/home' })
    },
    submitForm (formName) {
      this.$refs[formName].validate(valid => {
        if (valid) {
          var param = {
            code: this.userForm.name,
            password: this.userForm.pass
          }
          const { Login } = this
          //调用登录
          Login(param)
            .then((res) => this.loginSuccess(res))
            .catch(err => {
              this.$message.warn("登录失败:" + err.remark);
            })
            .finally(() => {
            })
        } else {
          this.$message.warn("校验失败!请输入正确的格式再登录!");
          return false;
        }
      });
    },
    },
  }
}

通过以上可以简单学会vuex的使用,这是动态路由的前提
动态路由的原理是
2每次路由跳转会通过permission.js进行判断和加载,若vuex中无用户路由数据则请求后端,
根据后端返回的用户的菜单地址进行处理拼装成路由格式,存储到vuex中,并添加到路由里面,若vuex中有用户路由数据,直接放行

permission.js

import router from './routers'
import store from './store'
import { ACCESS_TOKEN } from '@/store/mutation-types'

const whiteList = ['login', 'register'] // 白名单
const defaultRoutePath = '/home'

//遍历循环
router.beforeEach((to, from, next) => {
  console.log('start!!!',to,from)
  //判断是否登录成功获取登录信息 这里可以用token判断
  if(Vue.ls.get(ACCESS_TOKEN)){
    if (to.path === '/login') {
      next({ path: defaultRoutePath })
      //NProgress.done() 浏览器进度条
    } else {
      console.log('addRouters',store.getters.addRouters)
      if(store.getters.addRouters.length === 0){
        //这里会请求后端和组装路由
         store.dispatch('GenerateRoutes', '').then(() => {
        //讲路由添加进去
        var newRoutes=store.getters.addRouters;
        //  console.log('newRoutes',newRoutes)
         for (let index = 0; index < newRoutes.length; index++) {
          const element = newRoutes[index];
          //将路由添加进去
          router.addRoute(element);
        }
        //url编码
         const redirect = decodeURIComponent(from.query.redirect || to.path)
         if (to.path === redirect) {
           // console.log('去redirect',redirect)
            //中断当前导航,执行新的导航
            next({ ...to, replace: true })
          } else {
           // 跳转到目的路由
           //console.log('去home',to.path)
             next({ path: defaultRoutePath })
         }
          }) .catch(() => {
            console.log('错误请重新登录')
              store.dispatch('Logout').then(() => {
              next({ path: '/login'})
             })
          })
      }else{
        //放行
          next()
      }
    }
   }else{`在这里插入代码片`
    if (whiteList.includes(to.name)) {
      // 在免登录白名单,直接进入
      next()
    } else {
      console.log('认证失败重新登录')
      next({ path: '/login' })
    }
   }
})

router.afterEach(() => {
})

async-router.js

/**
 * 向后端请求用户的菜单,动态生成路由
 */
import { constantRouterMap } from '@/config/router.config'
import { generatorDynamicRouter } from '@/routers/generator-routers'//

const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: []
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      state.addRouters = routers
      //state.routers = constantRouterMap.concat(routers)
      console.log('state.addRouters', state.addRouters )
      console.log('state.routers', state.routers  )
    }
  },
  actions: {
    GenerateRoutes ({ commit }, data) {
      //入口
      console.log('GenerateRoutes:');
      return new Promise(resolve => {
        const { token } = data
        generatorDynamicRouter(token).then(routers => {
          console.log('获取返回的routers:',routers);
          commit('SET_ROUTERS', routers)
          resolve()
        })
      })
    }
  }
}

export default permission

constantRouterMap

export const constantRouterMap = [
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/bookstore/Login.vue')
  },
  {
    path: '/register',
    name: 'register',
    component: () => import('../views/bookstore/Register.vue')
  },
  // {
  //   path: '/',//必须有
  //   component:  () => import('../layouts/MainLayout.vue'),//一般是layout布局
  //  // redirect: '/home',//跳转到
  //   children:[
      // {
      //   path: 'home',//必须没有才会拼接
      //   name: 'home',
      //   component: () => import('../views/bookstore/Home.vue')
      // },
      // {
      //   path: 'order',
      //   name: 'order',
      //   component: () => import('../views/bookstore/Order.vue')
      // },
      // {
      //   path: 'books',
      //   name: 'books',
      //   component: () => import('../views/bookstore/Books.vue')
      // },
      // {
      //   path: 'car',
      //   name: 'car',
      //   component: () => import('../views/bookstore/ShoppingCar.vue')
      // },
      // {
      //   path: 'adminBook',
      //   name: 'adminBook',
      //   component: () => import('../views/bookstore/AdminBook.vue')
      // },
      // {
      //   path: 'adminOrder',
      //   name: 'adminOrder',
      //   component: () => import('../views/bookstore/AdminOrder.vue')
      // },
    // ]
  // }
]

routers目录下的generator-routers.js


import { BlankLayout } from '@/layouts/BlankLayout.vue'
import { MainLayout } from '@/layouts/MainLayout.vue'
import { postAction, getAction, deleteAction, putAction, downFilePost, downFileTimeOut } from "@/api/manage"

// 前端路由表
const constantRouterComponents = {
  // 基础页面 layout 必须引入
  MainLayout: MainLayout,
  BlankLayout: BlankLayout,
  'Home':() =>import('../views/bookstore/Home.vue'), 
  'Order':() =>  import('../views/bookstore/Order.vue'),
  'Book':() => import('../views/bookstore/Books.vue'),
  'ShoppingCar':() =>  import('../views/bookstore/ShoppingCar.vue'),
  'AdminBook':() =>  import('../views/bookstore/AdminBook.vue'),
  'AdminOrder':() =>  import('../views/bookstore/AdminOrder.vue'),
}



// 菜单的根级菜单
const rootRouter = {  
    path: '/',//必须有
    component:  () => import('../layouts/MainLayout.vue'),//一般是layout布局
    // redirect: '/home',//登录后默认首页改这里
    children:[]
}

/**
 * 动态生成菜单
 * @param token
 */
export const generatorDynamicRouter =(token) => {
  return new Promise((resolve, reject) => {
    //查询用户的菜单
    var getNal='/bookstore/auth/nvl'
    getAction(getNal, {})
    .then(res => {
      if (res.code == 200) {
        const menuNav = []
        const childrenNav = generator(res.result)
        rootRouter.children = childrenNav
        menuNav.push(rootRouter)
        resolve(menuNav)
      } else {
        reject(res.remark)
      }
    }).catch(err => {
        reject(err)
      })
    })
  }

/**
 * 格式化树形结构数据 生成 vue-router 层级路由表
 *
 * @param routerMap
 * @returns {*}
 */
export const generator = (routerMap) => {
  return routerMap.map(item =>  {
    const currentRouter = {
      path: item.path,
      name: item.name,
      component:  (constantRouterComponents[item.component || item.key]) || (() => import(`../views/bookstore/${item.component}`)),
      isVisible: item.isVisible,
      title:item.title,
      icon:item.icon
    }
    return currentRouter
  })
}

/**
 * 数组转树形结构
 * @param list 源数组
 * @param tree 树
 * @param parentId 父ID
 */
// const listToTree = (list, tree, parentId) => {
//   list.forEach(item => {
//     // 判断是否为父级菜单
//     if (item.parentId === parentId) {
//       const child = {
//         ...item,
//         key: item.key || item.name,
//         children: []
//       }
      
//       if (item.isHome) {
//         rootRouter.redirect = item.path
//       }
//       // 迭代 list, 找到当前菜单相符合的所有子菜单
//       listToTree(list, child.children, item.id)
//       // 删掉不存在 children 值的属性
//       if (child.children.length <= 0) {
//         delete child.children
//       }
//       // 加入到树中
//       tree.push(child)
//     }
//   })
// }

MainLayout.vue 动态菜单需要用layout布局实现,路由放行后会加载layout页面并生成加公共动态菜单栏

<template>
  <a-layout class="ant-layout">
    <!-- 头部导航栏-->
    <a-layout-header style="background: #fff">
      <div class="logo" />
      <a-menu
        theme="light"
        mode="horizontal"
        :open-keys="openKeys"
        @click="menuClick"
      >
        <a-menu-item key="1" disabled>你好,{{ name }}</a-menu-item>
        <template v-for="item in routers">
          <a-menu-item :key="`${item.path}`">
            <a-space>
              <a-icon :type="`${item.icon}`" />
              <router-link
                :to="{
                  path: `${item.path}`,
                }"
              >
                {{ item.title }}
              </router-link>
            </a-space>
          </a-menu-item>
        </template>
        <a-menu-item key="2">退出登录</a-menu-item>
      </a-menu>
    </a-layout-header>

    <a-layout-content>
      <RouterView />
    </a-layout-content>
  </a-layout>
</template>
  <script >
import { RouterLink, RouterView } from "vue-router";
import { mapState, mapActions } from 'vuex'

export default {
  name: 'MainLayout',
  data () {
    return {
      isCollapsed: false,
      onlyOneChild: null,
      openKeys: [''],
      menus: [],
      routers: [],
      menusRoot: [],
      url: {
        getUserInfo: '/bookstore/sysUser/getUserInfo'
      },
      sysUser: {
        name: '',
        code: '',
      }
    }
  },
  computed: {
    //获取vuex中的数据
    ...mapState({
      mainMenu: state => state.permission.addRouters,
      name: state => state.user.name,
    }),
  },
  watch: {
    collapsed: {
      handler: function (val) {
        this.isCollapsed = val
      }
    }
  },
  created () {
    this.menusRoot = this.mainMenu[0].children;
    this.routers = this.menusRoot.filter(item => {
      if (item.isVisible === true) {
        return item;
      }
    });
    console.log('routers', this.routers)
    console.log('name', this.name)
  },
  methods: {
    ...mapActions(['Login', 'Logout']),//
    // 点击菜单触发事件
    menuClick ({ item, key, keyPath }) {
      if (key == 2) {
        const { Logout } = this
        Logout()
          .then((res) => this.$router.push({ path: '/login' })
            .catch(err => {
              this.$message.warn("退出失败:" + err.remark);
            })
            .finally(() => {
            })
          )
      }
    },
  }
}

  </script>
  <style >
</style>

效果图
后端返回数据,模拟从数据库获取普通用户和管理员路由

  @Override
    public List<Map> getNvl(SysUser sysUser) {
        List<Map> list=new ArrayList<>();
        Map map1=new HashMap();
        map1.put("name","home");
        map1.put("component","Home");
        map1.put("icon","book");
        map1.put("path","home");
        map1.put("title","首页");
        map1.put("isVisible",true);
        Map map2=new HashMap();
        map2.put("name","books");
        map2.put("component","Books");
        map2.put("icon","filter");
        map2.put("path","books");
        map2.put("title","图书分类");
        map2.put("isVisible",false);
        Map map3=new HashMap();
        map3.put("name","car");
        map3.put("component","ShoppingCar");
        map3.put("icon","shopping-cart");
        map3.put("path","car");
        map3.put("title","购物车");
        map3.put("isVisible",true);
        Map map4=new HashMap();
        map4.put("name","order");
        map4.put("component","Order");
        map4.put("icon","shop");
        map4.put("path","order");
        map4.put("title","我的订单");
        map4.put("isVisible",true);
        //管理员可见
        if (sysUser.getIsAdmin()==1){
            Map map5=new HashMap();
            map5.put("name","adminBook");
            map5.put("component","AdminBook");
            map5.put("icon","folder");
            map5.put("path","adminBook");
            map5.put("isVisible",true);
            map5.put("title","图书管理");
            Map map6=new HashMap();
            map6.put("name","adminOrder");
            map6.put("component","AdminOrder");
            map6.put("icon","dollar");
            map6.put("path","adminOrder");
            map6.put("title","订单管理");
            map6.put("isVisible",true);
            list.add(map5);
            list.add(map6);
        }
        list.add(map1);
        list.add(map2);
        list.add(map3);
        list.add(map4);
        return list;
    }

前端效果: 打印出存储的路由信息
在这里插入图片描述
在这里插入图片描述

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值