vue-admin-template项目学习笔记

1,463 阅读5分钟

vue-admin-template是一个开源的后台管理系统的基础框架,基于 vue 和 element-ui。项目中不仅利用Vue全家桶和UI搭建了BMS的整体架构和页面,还内置了 i18 国际化、动态路由、权限验证、多样化图标和mockjs解决方案。本篇主要讲述整体架构和页面的实现。

一、项目架构

二、路由、权限

1、路由表配置项

// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
hidden: true // (默认 false)
//当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: 'noRedirect'
// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: true
name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
meta: {
  roles: ['admin', 'editor'] // 设置该路由进入的权限,支持多个权限叠加
  title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
  icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon
  noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
  breadcrumb: false //  如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
  affix: true // 若果设置为true,它则会固定在tags-view中(默认 false)
  // 当路由设置了该属性,则会高亮相对应的侧边栏。
  activeMenu: '/article/list'
}

2、路由权限

具体思路:

  • 登录:登录响应时服务端会返回一个token,前端再根据token拉取一个 user_info 的接口来获取用户的详细信息(如用户角色role等)。
  • 权限验证:根据role动态计算出对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。
  • constantRoutes: 不需要动态判断权限的路由,如登录页等通用页面,在Vue实例化便挂载。
  • asyncRoutes: 需要动态判断权限并通过 addRoutes 动态添加的页面。
router.beforeEach((to, from, next) => {
  if (store.getters.token) { // 判断是否有token
    if (to.path === '/login') {
      next({ path: '/' });
    } else {
      if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(res => { // 拉取info
          const roles = res.data.role;
          store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 
          })
        }).catch(err => {
          console.log(err);
        });
      } else {
        next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
      next();
    } else {
      next('/login'); // 否则全部重定向到登录页
    }
  }
});
import { asyncRouterMap, constantRouterMap } from 'src/router';

function hasPermission(roles, route) {// 判断当前角色是否能进入该路由
  if (route.meta && route.meta.role) {
    return roles.some(role => route.meta.role.indexOf(role) >= 0)
  } else {
    return true
  }
}

const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: []
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      state.addRouters = routers;
      state.routers = constantRouterMap.concat(routers);// 将权限路由添加到普通路由上
    }
  },
  actions: {
    GenerateRoutes({ commit }, data) {// 计算权限路由
      return new Promise(resolve => {
        const { roles } = data;
        const accessedRouters = asyncRouterMap.filter(v => {
          if (roles.indexOf('admin') >= 0) return true;
          if (hasPermission(roles, v)) {
            if (v.children && v.children.length > 0) {
              v.children = v.children.filter(child => {
                if (hasPermission(roles, child)) {
                  return child
                }
                return false;
              });
              return v
            } else {
              return v
            }
          }
          return false;
        });
        commit('SET_ROUTERS', accessedRouters);
        resolve();
      })
    }
  }
};

export default permission;

三、layout

1、状态管理工具控制布局变量

  • app模块:侧边栏是否折叠opened(cookie上的sidebarStatus:打开为1,否则为0)、折叠与展开是否有动画withoutAnimation

  • settings模块:showSettings、fixedHeader、sidebarLogo(是否展示logo)

2、布局

    <!-- 左边-侧边栏 -->
    <sidebar class="sidebar-container" />
    <!-- 右边内容 -->
    <div class="main-container">
      <!-- 顶部导航栏 -->
      <div :class="{'fixed-header':fixedHeader}">
        <navbar />
      </div>
      <!-- 下方内容区 -->
      <app-main />
    </div>

四、侧边栏

  • 不可再分的叶子节点:
  • 可再分的非叶子节点:递归遍历层级

当路由的 children >1:自动会变成嵌套的模式;

当路由的 children =1:默认将子路由作为根路由显示在侧边栏中(如果想要展示父级,在根路由中设置alwaysShow: true)

<!-- 当节点无需隐藏时进入判断 -->
  <div v-if="!item.hidden">

    <!-- 叶子节点 -->
    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"  style="border-right:1px solid red">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <!-- 非叶子节点 -->
    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body style="border-bottom:1px solid green">
      <!-- 显示非叶子节点 -->
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
      </template>
      <!-- 递归子级 -->
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>

  </div>

五、顶部导航栏

面包屑:处理当前点击路由得到数组levelList,末级不可点击跳转,其他层级处理点击跳转。

  <el-breadcrumb class="app-breadcrumb" separator="/">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
      </el-breadcrumb-item>
    </transition-group>
  </el-breadcrumb>

六、内容区

    <transition name="fade-transform" mode="out-in">
      <router-view :key="key" />
    </transition>
computed: {
  key() {
    // 只要保证 key 唯一性就可以了,保证不同页面的 key 不相同
    return this.$route.fullPath
  }
 }

唯一的 key,来保证路由切换时都会重新渲染触发钩子