手擼一個vue後臺管理系統框架(一)路由+Tabs篇

本章技術棧:Vue-Router + VueX + Tabs + 動態菜單渲染

背景:上週寫了一篇基於 Iframe實現單頁面多tab切換界面無刷新 的功能,如今SPA盛行的時代,感受Iframe實現SPA有點Low了(不過基於傳統多頁面實現SPA也是無奈之舉),因此最近想着基於VUE實現多tab功能,隨便也實現了菜單欄動態渲染、路由管理、狀態管理等項目框架基礎功能。這樣看來,這均可以用做通常中小型web後臺管理系統的框架基本骨架了。基於這套骨架後面我會持續增長登陸(Token)、HTTP請求(axios)、用戶、角色、權限管理等。還會封裝一些分頁、上傳等組建,打造一款真正的開箱即用的後臺管理系統框架,讓開發者更多的關注項目需求,提升開發效率,節省開發成本。
先上效果圖
gif5新文件.gifvue

讀完這篇文章你能收穫什麼?
  1. 能夠基於這套骨架搭建屬於本身的後臺管理系統框架
  2. 能夠在本身的Vue項目中加入Tabs功能
  3. 瞭解Vue-Router、VueX基本用法
  4. 瞭解Element-ui中Menu、Tabs等組建基本用法
  5. 瀏覽器前進後退時監聽路由變化,更新當前tabs(10月25日晚新增)
  6. 利用<keep-alive>動態緩存指定組件(10月25日晚新增)

源碼註釋很詳細,詳情請戳:https://gitbook.cn/gitchat/activity/5db1a2469fc75b4c0af61ee4ios

核心代碼:
  • 左側菜單欄渲染:
<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 
      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) {
      if (index !== 'index') {
        var tab = context.state.tabs.find(f => f.index === index)
        if (!tab) {
          let menu = {}
          menu = context.state.menu.find(f => f.index === index)
          if (!menu) {
            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) {
      let indexNum = context.state.tabs.findIndex(f => f.index === index)
      let activeItem = context.state.activeItem
      let newTabs = context.state.tabs.filter(f => f.index !== index)
      context.commit('initTabs', newTabs)
      if (activeItem === index) {
        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
})

const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push (location) {
  return originalPush.call(this, location).catch(err => err)
}
export default router
10月29日晚,更新如下內容:
  1. 增長登陸界面,路由守衛,儲存 token;
  2. 封裝 Axios,實現http請求攔截和響應攔截;
  3. 加入highcharts和拖拽組件,實現推拽圖表功能;
  4. 增長樣式,美化界面;

總體效果圖git

666.gif

目前總體功能包括:web

1.登陸(路由守衛);
2.封裝 Axios 請求;
3.Tabs 動態管理,切換界面;
4.動態渲染菜單;
5.利用 <keep-alive>; 動態緩存指定組件;
6.Vue-Router 路由管理;
7.VueX 狀態管理;
8.圖表拖拽效果;vue-router

特色:結構簡單,通用性強,適合用做中小型 Web 後臺管理系統框架,開箱即用。vuex

  • Axios封裝
import axios from 'axios'
import { Message, Loading } from 'element-ui'
import router from '../router/index'

let loading // 定義loading變量

function startLoading () { // 使用Element loading-start 方法
  loading = Loading.service({
    lock: true,
    text: '加載中...',
    background: 'rgba(0, 0, 0, 0.7)'
  })
}
function endLoading () { // 使用Element loading-close 方法
  loading.close()
}

// 請求攔截  設置統一header
axios.interceptors.request.use(config => {
  // 加載
  startLoading()
  if (localStorage.eleToken) { config.headers.Authorization = localStorage.eleToken }
  return config
}, error => {
  return Promise.reject(error)
})

// 響應攔截  token過時處理
axios.interceptors.response.use(response => {
  endLoading()
  if (response.data.success) {
    return response.data.message
  } else {
    Message.error(response.data.message)
    return Promise.reject(response.data.message)
  }
}, error => {
  // 錯誤提醒
  endLoading()
  // Message.error(error.response.message)
  const { status } = error.response
  if (status === '401') {  //狀態嗎根據後臺返回而定
    Message.error('token值無效,請從新登陸')
    // 清除token
    localStorage.removeItem('eleToken')
    // 頁面跳轉
    router.push('/login')
  } else {
    Message.error('系統錯誤')
  }
  return Promise.reject(error)
})

export default axios

源碼註釋很詳細,詳情請戳:https://gitbook.cn/gitchat/activity/5db1a2469fc75b4c0af61ee4element-ui

未完待續......

歡迎交流,歡迎 Star (^_^)
經驗總結,代碼加工廠!axios

相關文章
相關標籤/搜索