vue-element-admin | 登录及动态路由(权限管控)

一、项目下载

       1、下载安装vue-element-admin
             Github中星级最高的
       2、目录结构说明
在这里插入图片描述

二、项目运行

进入项目目录npm install安装依赖,npm run dev运行项目即可看到vue-element-admin内容

三、二次开发

       1、版本选择
              想要使用vue-element-admin进行二次开发,由于其内容繁杂较多,不适用,利用vue-admin-template进行二次开发,这里边包含了最基础的配置及内容,简洁适用
       2、登录(后端接口)
              ①在vue.config.js中,修改proxy,target为服务器地址及端口(例:http://10.201.88.41:8012),代码如下

'use strict'
const path = require('path')
const defaultSettings = require('./src/settings.js')

function resolve(dir) {
  return path.join(__dirname, dir)
}

const name = defaultSettings.title || 'vue Admin Template' // page title

// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following methods:
// port = 9528 npm run dev OR npm run dev --port = 9528
const port = process.env.port || process.env.npm_config_port || 9528 // dev port

// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
  /**
   * You will need to set publicPath if you plan to deploy your site under a sub path,
   * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
   * then publicPath should be set to "/bar/".
   * In most cases please use '/' !!!
   * Detail: https://cli.vuejs.org/config/#publicpath
   */
  publicPath: '/',
  outputDir: 'dist',
  assetsDir: 'static',
  lintOnSave: process.env.NODE_ENV === 'development',
  productionSourceMap: false,
  devServer: {
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    proxy: {
      // change xxx-api/login => mock/login
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      // [process.env.VUE_APP_BASE_API]: {
      //   target: `http://127.0.0.1:${port}/mock`,
      //   changeOrigin: true,
      //   pathRewrite: {
      //     ['^' + process.env.VUE_APP_BASE_API]: ''
      //   }
      // }
      [process.env.VUE_APP_BASE_API]: {
        target: `http://10.201.88.41:8012`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    }
    // after: require('./mock/mock-server.js')
  },
  configureWebpack: {
    // provide the app's title in webpack's name field, so that
    // it can be accessed in index.html to inject the correct title.
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  },
  chainWebpack(config) {
    config.plugins.delete('preload') // TODO: need test
    config.plugins.delete('prefetch') // TODO: need test

    // set svg-sprite-loader
    config.module
      .rule('svg')
      .exclude.add(resolve('src/icons'))
      .end()
    config.module
      .rule('icons')
      .test(/\.svg$/)
      .include.add(resolve('src/icons'))
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]'
      })
      .end()

    // set preserveWhitespace
    config.module
      .rule('vue')
      .use('vue-loader')
      .loader('vue-loader')
      .tap(options => {
        options.compilerOptions.preserveWhitespace = true
        return options
      })
      .end()

    config
    // https://webpack.js.org/configuration/devtool/#development
      .when(process.env.NODE_ENV === 'development',
        config => config.devtool('cheap-source-map')
      )

    config
      .when(process.env.NODE_ENV !== 'development',
        config => {
          config
            .plugin('ScriptExtHtmlWebpackPlugin')
            .after('html')
            .use('script-ext-html-webpack-plugin', [{
            // `runtime` must same as runtimeChunk name. default is `runtime`
              inline: /runtime\..*\.js$/
            }])
            .end()
          config
            .optimization.splitChunks({
              chunks: 'all',
              cacheGroups: {
                libs: {
                  name: 'chunk-libs',
                  test: /[\\/]node_modules[\\/]/,
                  priority: 10,
                  chunks: 'initial' // only package third parties that are initially dependent
                },
                elementUI: {
                  name: 'chunk-elementUI', // split elementUI into a single package
                  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
                  test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
                },
                commons: {
                  name: 'chunk-commons',
                  test: resolve('src/components'), // can customize your rules
                  minChunks: 3, //  minimum common number
                  priority: 5,
                  reuseExistingChunk: true
                }
              }
            })
          config.optimization.runtimeChunk('single')
        }
      )
  }
}

              ②在请求拦截类utils/request.js中修改请求拦截内容,如果需要后台反问需要token做身份标识在request的config中设置config.headers.Authorization,后端返回的响应码和默认不一致在response中进行修改,代码如下

import axios from 'axios'
import { Message } from 'element-ui'
// import store from '@/store'
import { getToken } from './auth'
// import { MessageBox, Message } from 'element-ui'
// import store from '@/store'
// import { getToken } from '@/utils/auth'

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    // if (store.getters.token) {
    //   // let each request carry token
    //   // ['X-Token'] is a custom headers key
    //   // please modify it according to the actual situation
    //   config.headers['X-Token'] = getToken()
    // }
    console.log('info request token:' + getToken())
    if (getToken()) {
      config.headers.Authorization = 'Bearer ' + getToken()
    } else {
      config.headers.Authorization = 'Basic ZGlwczpzd2QtZGlwcw=='
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const res = response.data
    // if the custom code is not 20000, it is judged as an error.
    if (res.msgCode !== '000000') {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      // if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
      //   // to re-login
      //   MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
      //     confirmButtonText: 'Re-Login',
      //     cancelButtonText: 'Cancel',
      //     type: 'warning'
      //   }).then(() => {
      //     store.dispatch('user/resetToken').then(() => {
      //       location.reload()
      //     })
      //   })
      // }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err:' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    console.log(error.message)
    return Promise.reject(error)
  }
)

export default service

       ③在spi/user.js 修改登录对应后端接口的url,添加参数params

import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/auth-server/oauth/user/token',
    method: 'post',
    params: data
  })
}

export function getInfo(account) {
  return request({
    url: '/sysadmin-server/menus/current',
    method: 'get',
    params: { account }
  })
}

export function logout() {
  return request({
    url: '/user/logout',
    method: 'post'
  })
}

以上便可正常访问到后端接口数据

       3、动态路由(权限划分)
              一般的管理系统都会涉及到权限的划分问题,根据用户获取其对应的菜单,在页面中只展现对应的菜单即可,那么此时就涉及到动态路由的问题
              路由=基础路由(所有用户共有)+ 动态路由(根据用户从后端获取来的路由)
①基础路由
router/index.js中只需要保留基础路由,其他的需要动态显示的路由全部删除掉,404页面的路由必须放在所有路由的最底端
【注】:路由取消显示#的问题:mode: ‘history’

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '@/layout'

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: 'Dashboard', icon: 'dashboard' }
    }]
  },

  {
    path: '/views/example',
    component: Layout,
    redirect: '/views/example/table',
    name: 'Example',
    meta: { title: 'Example', icon: 'example' },
    children: [
      {
        path: 'table',
        name: 'Table',
        component: () => import('@/views/table/index'),
        meta: { title: 'Table', icon: 'table' }
      },
      {
        path: 'tree',
        name: 'Tree',
        component: () => import('@/views/tree/index'),
        meta: { title: 'Tree', icon: 'tree' }
      }
    ]
  },

  {
    path: '/views/form',
    component: Layout,
    children: [
      {
        path: 'index',
        name: 'Form',
        component: () => import('@/views/form/index'),
        meta: { title: 'Form', icon: 'form' }
      }
    ]
  },

  {
    path: '/views/external-link',
    component: Layout,
    children: [
      {
        path: 'https://panjiachen.github.io/vue-element-admin-site/#/',
        meta: { title: 'External Link', icon: 'link' }
      }
    ]
  },
  // 404 page must be placed at the end !!!
  {
    path: '*',
    redirect: '/404',
    hidden: true
  }
]

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes,
  // 去除路由中的#问题
  mode: 'history'
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

②动态路由的获取
store/modules/user.js,在登录之后,通过getInfo这个接口,获取到后端传来的菜单,并保存(commit)在vuex中

import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken, getAccount, setAccount, removeAccount } from '@/utils/auth'
import { resetRouter } from '@/router'

const state = {
  token: getToken(),
  name: getAccount(),
  avatar: '',
  menus: []
}

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  // SET_NAME: (state, name) => {
  //   state.name = name
  // },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_MENUS: (state, menus) => {
    state.menus = menus
  },
  SET_NAME: (state, name) => {
    state.name = name
  }
}

const actions = {
  // user login
  login({ commit }, userInfo) {
    const { account, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ account: account.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.access_token)
        // commit('SET_NAME', account.trim())
        commit('SET_NAME', account.trim())
        setToken(data.access_token)
        setAccount(account.trim())
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // get user info
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.name).then(response => {
        const { data } = response
        if (!data) {
          reject('Verification failed, please Login again.')
        }
        const menus = data
        console.log(data)
        // 获取到的菜单json如注释内容
        // const menus = [{
        //   path: '/testIndex',
        //   component: 'Layout',
        //   redirect: '/testIndex/table',
        //   name: 'TestIndex',
        //   meta: { title: '多级菜单测试', icon: 'link' },
        //   children: [
        //     {
        //       path: 'firstMenu',
        //       name: 'FirstMenu',
        //       component: '/test/firstMenu',
        //       meta: { title: '第一个菜单', icon: 'table' }
        //     },
        //     {
        //       path: 'secondMenu',
        //       name: 'SecondMenu',
        //       component: '/test/secondMenu',
        //       meta: { title: '第二个菜单', icon: 'tree' }
        //     }
        //   ]
        // }]
        // const menus = []

        // commit('SET_NAME', name)
        // commit('SET_AVATAR', avatar)
        commit('SET_MENUS', menus)

        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        commit('SET_TOKEN', '')
        removeToken()
        resetRouter()
        removeAccount()
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({ commit }) {
    return new Promise(resolve => {
      commit('SET_TOKEN', '')
      removeToken()
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

在保存到vuex中后需要设置对外获取途径store/getters.js

const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  menus: state => state.user.menus
}
export default getters

       ③动态生成权限路由(核心)
              a、根据环境配置导入组件,在vue中,将菜单路径作为参数,实现路由地址的注入
在 src/router 文件夹下,建立两个文件,各只需添加一行代码, 定义导入方法,此时设置@/,那么在后端传来的路由数据中就需要标明/views这个目前项目中放置组件的目录结构

src/router/_import_development.js

// 开发环境导入组件
module.exports = file => require('@/' + file + '.vue').default // vue-loader at least v13.0.0+

src/router/_import_production.js

// 生产环境导入组件
module.exports = file => () => import('@/' + file + '.vue')

              b、动态菜单配置文件permission.js
                     ->获取组件
                            import Layout from ‘@/layout’
                            const _import = require(’./router/import’ + process.env.NODE_ENV + ‘.js’) // 获取组件的方法
                     ->在router.beforeEach路由钩子中,过滤路由,并生成路由
                            // 1.过滤路由:const menus = filterAsyncRouter(store.getters.menus)
                            // 2.动态添加路由:router.addRoutes(menus)
                            // 3.将路由数据传递给全局变量,做侧边栏菜单渲染工作:global.antRouter = menus
代码如下:

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

import Layout from '@/layout'
const _import = require('./router/_import_' + process.env.NODE_ENV + '.js') // 获取组件的方法

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.menus && store.getters.menus.length > 0) {
        next()
      } else {
        try {
          // get user info
          await store.dispatch('user/getInfo')
          if (store.getters.menus.length < 1) {
            global.antRouter = []
            next()
          }
          const menus = filterAsyncRouter(store.getters.menus) // 1.过滤路由
          router.addRoutes(menus) // 2.动态添加路由
          global.antRouter = menus // 3.将路由数据传递给全局变量,做侧边栏菜单渲染工作
          next({
            ...to,
            replace: true
          }) // hack方法 确保addRoutes已完成 ,set the replace
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* has no token*/

    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

function filterAsyncRouter(asyncRouterMap) {
  const accessedRouters = asyncRouterMap.filter(route => {
    if (route.component) {
      if (route.component === 'Layout') {
        route.component = Layout
      } else {
        route.component = _import(route.component) // 导入组件
      }
    }
    if (route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children)
    }
    return true
  })
  return accessedRouters
}

                     c、最后一步合并路由侧边栏渲染
                            return this.$router.options.routes.concat(global.antRouter) // 最后渲染时,基础路由和动态路由合并
layout/components/Sidebar/index.vue

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'sidebar'
    ]),
    routes() {
      // return this.$router.options.routes
      return this.$router.options.routes.concat(global.antRouter) // 最后渲染时,基础路由和动态路由合并
    },
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

此时动态菜单全部设置完成
       3A、页面刷新,会出现500,并没有像预期刷新页面获取用户信息正常显示
              原因:用户信息保存在token中,每次刷新页面后token会变化,此时将没有用户信息,获取不到数据,所以需要存放在cookie中
utils/auth.js 编写account操作的内容

import Cookies from 'js-cookie'

const TokenKey = 'vue_admin_template_token'
const AccountKey = 'Account'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

export function getAccount() {
  return Cookies.get(AccountKey)
}

export function setAccount(account) {
  return Cookies.set(AccountKey, account)
}

export function removeAccount() {
  return Cookies.remove(AccountKey)
}

在接口返回信息的时候setAccount进用户名即可

import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken, getAccount, setAccount, removeAccount } from '@/utils/auth'
import { resetRouter } from '@/router'

const state = {
  token: getToken(),
  name: getAccount(),
  avatar: '',
  menus: []
}

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  // SET_NAME: (state, name) => {
  //   state.name = name
  // },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_MENUS: (state, menus) => {
    state.menus = menus
  },
  SET_NAME: (state, name) => {
    state.name = name
  }
}

const actions = {
  // user login
  login({ commit }, userInfo) {
    const { account, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ account: account.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.access_token)
        // commit('SET_NAME', account.trim())
        commit('SET_NAME', account.trim())
        setToken(data.access_token)
        setAccount(account.trim())
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // get user info
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.name).then(response => {
        const { data } = response
        if (!data) {
          reject('Verification failed, please Login again.')
        }
        const menus = data
        console.log(data)
        // const menus = [{
        //   path: '/testIndex',
        //   component: 'Layout',
        //   redirect: '/testIndex/table',
        //   name: 'TestIndex',
        //   meta: { title: '多级菜单测试', icon: 'link' },
        //   children: [
        //     {
        //       path: 'firstMenu',
        //       name: 'FirstMenu',
        //       component: '/test/firstMenu',
        //       meta: { title: '第一个菜单', icon: 'table' }
        //     },
        //     {
        //       path: 'secondMenu',
        //       name: 'SecondMenu',
        //       component: '/test/secondMenu',
        //       meta: { title: '第二个菜单', icon: 'tree' }
        //     }
        //   ]
        // }]
        // const menus = []

        // commit('SET_NAME', name)
        // commit('SET_AVATAR', avatar)
        commit('SET_MENUS', menus)

        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        commit('SET_TOKEN', '')
        removeToken()
        resetRouter()
        removeAccount()
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({ commit }) {
    return new Promise(resolve => {
      commit('SET_TOKEN', '')
      removeToken()
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}


项目全部代码见csdn下载//download.csdn.net/download/Kasey_L/12254734

发布了24 篇原创文章 · 获赞 1 · 访问量 524

猜你喜欢

转载自blog.csdn.net/Kasey_L/article/details/104939839