手撸一个vue后台管理系统框架(一)路由+Tabs篇

本章技术栈:Vue-Router + VueX + Tabs + 动态菜单渲染

背景:上周写了一篇基于 Iframe实现单页面多tab切换界面无刷新 的功能,现在SPA盛行的时代,感觉Iframe实现SPA有点Low了(不过基于传统多页面实现SPA也是无奈之举),所以最近想着基于VUE实现多tab功能,随便也实现了菜单栏动态渲染、路由管理、状态管理等项目框架基础功能。这样看来,这都可以用作一般中小型web后台管理系统的框架基本骨架了。基于这套骨架后面我会持续增加登录(Token)、TTP请求(axios)、用户、角色、权限管理等。还会封装一些分页、上传等组建,打造一款真正的开箱即用的后台管理系统框架,让开发者更多的关注项目需求,提高开发效率,节省开发成本。
先上效果图
gif5新文件.gif

读完这篇文章你能收获什么?
  1. 可以基于这套骨架搭建属于自己的后台管理系统框架
  2. 可以在自己的Vue项目中加入Tabs功能
  3. 了解Vue-Router、VueX基本用法
  4. 了解Element-ui中Menu、Tabs等组建基本用法

源码注释很详细:https://github.com/xuqiaoba/vue-frame

核心代码:
  • 左侧菜单栏渲染:
<template>
  <el-menu
    :default-active="activeItem"
    class="el-menu-vertical-demo"
    background-color="#545c64"
    text-color="#fff"
    router
    :unique-opened='true'
    @select="clickMenuItem"
  >
    <template v-for="(item,index) in menu">
      <el-submenu v-if="item.hasChilder" :index="item.index" :key="index">
        <template slot="title">
          <i class="el-icon-document"></i>
          <span>{{item.name}}</span>
        </template>
        <template v-for="(v,i) in item.children">
          <el-menu-item :index="v.index" :key="i">{{v.name}}</el-menu-item>
        </template>
      </el-submenu>
      <template v-else>
        <el-menu-item :key="index" :index="item.index" >
          <template slot="title">
              <i class="el-icon-location"></i>
              <span>{{item.name}}</span>
            </template>
        </el-menu-item>
      </template>
    </template>
  </el-menu>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  data () {
    return {
      currFatherIndex: ''
    }
  },
  mounted () {
    this.getMenu()
  },
  methods: {
    ...mapActions('menu', {
      getMenu: 'getMenu',
      clickMenuItem: 'clickMenuItem'
    })
  },
  computed: {
    ...mapState('menu', {
      menu: 'menu',
      activeItem: 'activeItem'
    })
  }
}
</script>

<style scoped>
.el-menu > ul,
.el-menu {
  height: 100%;
}
.el-aside {
  height: 100%;
}
</style>
  • 右侧Tabs组件:
<template>
    <!-- 参考element-ui中Tabs组件 -->
   <el-tabs :value="activeItem" @tab-remove="tabRemove" class='content-body' @tab-click="tabClick">
        <el-tab-pane v-for="item in tabs" :label="item.label" :key="item.index" :name="item.index"  :closable="item.closable">
        </el-tab-pane>
   </el-tabs>
</template>

<script>
import { mapActions, mapState, mapMutations } from 'vuex'
export default {
  computed: {
    ...mapState('menu', {
      tabs: 'tabs',
      activeItem: 'activeItem'
    })
  },
  created () {
    console.log(this.tabs)
  },
  methods: {
    ...mapActions('menu', {
      closeTab: 'closeTab'
    }),
    ...mapMutations('menu', {
      switchTab: 'switchTab'
    }),
    tabClick (e) {
      this.switchTab(e.name)
      this.$router.push({ path: e.name })
    },
    tabRemove (e) {
      let t = this
      // 异步阻塞一下,否则activeItem还是当前关闭tab的值
      setTimeout(function () {
        t.$router.push({ path: t.activeItem })
      }, 1)
      this.closeTab(e)
    }
  }
}
</script>
<style scoped>
.content-body {
  height: 40px !important;
}
</style>
  • VueX状态管理:
import Store from './store'
// 菜单列表,可通过后台返回,返回格式类似就行,还可增加icon图标等字段
const menumap = [
  { name: '首页', hasChilder: false, index: 'index', children: [] },
  { name: '菜单一', hasChilder: false, index: 'one', children: [] },
  { name: '菜单二', hasChilder: false, index: 'two', children: [] },
  {
    name: '菜单三',
    hasChilder: true,
    index: 'three',
    children: [
      { name: '子菜单3-1', hasChilder: false, index: 'three3-1' },
      { name: '子菜单3-2', hasChilder: false, index: 'three3-2' }
    ]
  },
  {
    name: '菜单四',
    hasChilder: true,
    index: 'four',
    children: [
      { name: '子菜单4-1', hasChilder: false, index: 'four4-1' },
      { name: '子菜单4-2', hasChilder: false, index: 'four4-2' }
    ]
  }
]

Store.registerModule('menu', {
  namespaced: true,
  state: {
    menu: [],
    // 默认tabs里面有‘首页’,且没有closable属性,不能删除
    tabs: [
      {
        label: '首页',
        index: 'index'
      }
    ],
    activeItem: 'index' // 默认选中首页
  },
  getters: {
  },
  mutations: {
    initMenu (state, menu) {
      state.menu = menu
    },
    initTabs (state, tabs) {
      state.tabs = tabs
    },
    addTab (state, tab) {
      state.tabs.push(tab)
    },
    switchTab (state, nowIndex) {
      state.activeItem = nowIndex
    }
  },
  actions: {
    getMenu (context) {
      // 查询所有菜单(如果做权限管理,即根据不同用户(角色),显示不同的菜单,可以在后台就判断权限,然后对应有权限的菜单列表)
      context.commit('initMenu', menumap)
    },
    clickMenuItem (context, index) {
      console.log(index)
      // ‘index’一直固定在tabs里面,所有不需要‘addTab’
      if (index !== 'index') {
        // find()函数用来查找目标元素,找到就返回该元素,找不到返回undefined。
        var tab = context.state.tabs.find(f => f.index === index)
        // tabs里面没有,就去总菜单去查询
        if (!tab) {
          let menu = {}
          // 首先查询一级菜单
          menu = context.state.menu.find(f => f.index === index)
          // 如果当前menu不在一级菜单,则查询所有二级菜单
          if (!menu) {
            // flat()用于将嵌套的数组“拉平”,变成一维数组。该方法返回一个新数组,对原数据没有影响
            menu = context.state.menu.map(a => a.children).flat().find(f => f.index === index)
          }
          let newTab = {
            label: menu.name,
            index: menu.index,
            closable: true
          }
          context.commit('addTab', newTab)
        }
      }
      context.commit('switchTab', index)
    },
    closeTab (context, index) {
      console.log(index)
      // findIndex()函数也是查找目标元素,找到就返回元素的位置,找不到就返回-1。
      let indexNum = context.state.tabs.findIndex(f => f.index === index)
      console.log(indexNum)
      // 当前选中菜单
      let activeItem = context.state.activeItem
      // 除移除的tab之外的所有tabs
      let newTabs = context.state.tabs.filter(f => f.index !== index)
      // 重新初始化tabs
      context.commit('initTabs', newTabs)
      // 如果删除的tab正好是当前选中的tab,则执行“switchTab”,改变activeItem
      if (activeItem === index) {
      // 如果indexNum===0,则表示switchTab到index(但是‘首页’不可能被删除,所以如果indexNum不可能为0),否则跳转到删除tab的上一个tab
        context.commit('switchTab', indexNum === 0 ? 'index' : newTabs[indexNum - 1].index)
      }
    }
  }
})
  • Vue-Router路由管理:
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    redirect: 'index',
    component: () => import('../views/Home.vue'),
    children: [
      {
        path: 'index',
        name: 'index',
        component: () => import('../views/Index.vue')
      },
      {
        path: 'one',
        name: 'one',
        component: () => import('../views/One.vue')
      },
      {
        path: 'two',
        name: 'two',
        component: () => import('../views/Two.vue')
      },
      {
        path: 'three3-1',
        name: 'three3-1',
        component: () => import('../views/three/Three3-1.vue')
      },
      {
        path: 'three3-2',
        name: 'three3-2',
        component: () => import('../views/three/Three3-2.vue')
      },
      {
        path: 'four4-1',
        name: 'four4-1',
        component: () => import('../views/four/Four4-1.vue')
      },
      {
        path: 'four4-2',
        name: 'four4-2',
        component: () => import('../views/four/Four4-2.vue')
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

// 解决路由下的界面,点击该路由多次时
// 报错:NavigationDuplicated{_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/one") is not allowed", stack: "Error↵    at new NavigationDuplicated (webpack-int…e_modules/element-ui/lib/mixins/emitter.js:29:22)"}
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push (location) {
  return originalPush.call(this, location).catch(err => err)
}
export default router
未完待续......

欢迎交流,欢迎 Star (^_^)
经验总结,代码加工厂!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章