第十二章 動(dòng)態(tài)路由、權(quán)限纹烹、典型框架頁(參考)

  1. 首先需要了解的一些開發(fā)規(guī)則
  • 整個(gè)系統(tǒng)支持三種類型的子頁面(在數(shù)據(jù)庫里必須以menu.url字段來存儲):
    (1) user/login:這個(gè)是針對的Vue框架下的頁面組件,注意不能以/開頭召边,對應(yīng)views/user/login.vue
    (2) iframe:****:這個(gè)是Spring Boot框架提供的一些頁面铺呵,比如swagger和druid監(jiān)控頁面。
    (3) http[s]://www.baidu.com隧熙,這個(gè)是第三方頁面片挂。
  • 菜單按鈕權(quán)限的獲取被設(shè)計(jì)在路由守衛(wèi)中執(zhí)行初始化。
  • 本系統(tǒng)保留動(dòng)態(tài)菜單與路由守衛(wèi)的功能贞盯,但在前期不進(jìn)行啟用音念,啟用的時(shí)機(jī)要看具體的情況。
  1. 在store/modules下的app.js中添加一個(gè)動(dòng)態(tài)菜單設(shè)置狀態(tài)邻悬、動(dòng)態(tài)菜單症昏、用戶權(quán)限設(shè)置狀態(tài)随闽、用戶權(quán)限父丰。
export default {
  state: {
    dynamicRouteLoaded: false, // 菜單和路由是否已經(jīng)加載
    dynamicRoutes: [], // 用戶的權(quán)限菜單
    permissionLoaded: false, // 用戶權(quán)限是否已經(jīng)加載
    permissions: [] // 用戶的按鈕權(quán)限
  },
  getters: {
    dynamicRouteLoaded (state) {
      return state.dynamicRouteLoaded
    },
    dynamicRoutes (state) {
      return state.dynamicRoutes
    },
    permissionLoaded (state) {
      return state.permissionLoaded
    },
    permissions (state) {
      return state.permissions
    }
  },
  mutations: {
    dynamicRouteLoaded (state, dynamicRouteLoaded) { // 改變菜單和路由的加載狀態(tài)
      state.dynamicRouteLoaded = dynamicRouteLoaded
    },
    dynamicRoutes (state, dynamicRoutes) { // 設(shè)置用戶的權(quán)限菜單
      state.dynamicRoutes = dynamicRoutes
    },
    permissionLoaded (state, permissionLoaded) { // 改變用戶權(quán)限的加載狀態(tài)
      state.permissionLoaded = permissionLoaded
    },
    permissions (state, permissions) { // 設(shè)置用戶的按鈕權(quán)限
      state.permissions = permissions
    }
  },
  actions: {
  }
}

  1. 重寫登錄方法
login () {
      var data = {account: '', password: ''}
      this.$api.login.login(data).then(res => {
        if (res.code !== 200) {
          // 其它處理
        } else {
          Cookies.set('token', res.data.token) // 放置token到cookie
          sessionStorage.setItem('user', data.account) // 保存用戶到本地會話
          this.$store.commit('dynamicRouteLoaded', false) // 要求重新加載導(dǎo)航菜單
          this.$store.commit('permissionLoaded', false) // 要求重新加載導(dǎo)航菜單
          this.$router.push('/') // 登錄成功, 跳轉(zhuǎn)到主頁
        }
        this.loading = false
      })
    }
  1. 導(dǎo)航守衛(wèi)src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import store from '@/store'
import api from '@/http/api'
import { getIFramePath, getIFrameUrl } from '@/utils/iframe'

Vue.use(Router)

const router = new Router({
  routes: [
    {
      path: '/',
      name: '首頁',
      component: HelloWorld,
      children: [
        {
          path: '',
          name: '系統(tǒng)介紹',
          component: HelloWorld,
          meta: {
            icon: 'fa fa-home fa-lg',
            index: 0
          }
        }
      ]
    }
  ]
})

/**
 * 導(dǎo)航守衛(wèi)
 */
router.beforeEach((to, from, next) => {
  // 登錄成功之后,會把用戶信息保存在會話掘宪,用戶信息的存在時(shí)間為會話生命周期蛾扇,頁面關(guān)閉即失效
  let userName = sessionStorage.getItem('user')
  if (to.path === '/login') {
    // 如果是訪問登錄界面,如果用戶會話信息存在魏滚,代表已登錄過镀首,跳轉(zhuǎn)到主頁
    if (userName) {
      next({ path: '/' })
    } else {
      next()
    }
  } else {
    if (!userName) {
      // 如果訪問非登錄界面,且戶會話信息不存在鼠次,代表未登錄更哄,則跳轉(zhuǎn)到登錄界面
      next({ path: '/login' })
    } else {
      // 加載動(dòng)態(tài)菜單和路由
      addDynamicMenuAndRoutes(userName, to, from)
      next()
    }
  }
})

/**
 * 加載動(dòng)態(tài)菜單和路由
 */
function addDynamicMenuAndRoutes (userName, to, from) {
  // 處理IFrame嵌套頁面
  handleIFrameUrl(to.path)

  if (store.state.app.menuRouteLoaded) {
    return
  }

  api.menu.findNavTree({'userName': userName}).then(res => {
    // 添加動(dòng)態(tài)路由
    let dynamicRoutes = addDynamicRoutes(res.data)
    // 處理靜態(tài)組件綁定路由
    router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes)
    router.addRoutes(router.options.routes)
    // 保存加載狀態(tài)
    store.commit('menuRouteLoaded', true)
    // 保存菜單樹
    store.commit('setNavTree', res.data)
  }).then(res => {
    api.user.findPermissions({'name': userName}).then(res => {
      // 保存用戶權(quán)限標(biāo)識集合
      store.commit('setPerms', res.data)
    })
  }).catch(function (res) {
  })
}

/**
 * 處理IFrame嵌套頁面
 * 此處主要去匹配在store中存儲的iframe路徑列表, 通過匹配Path獲取iframe真正的url,將url存儲到store中
 */
function handleIFrameUrl (path) {
  // 嵌套頁面腥寇,保存iframeUrl到store成翩,供IFrame組件讀取展示
  let url = path
  let length = store.state.iframe.iframeUrls.length
  for (let i = 0; i < length; i++) {
    let iframe = store.state.iframe.iframeUrls[i]
    if (path != null && path.endsWith(iframe.path)) {
      url = iframe.url
      store.commit('setIFrameUrl', url)
      break
    }
  }
}

/**
* 添加動(dòng)態(tài)(菜單)路由
* @param {*} menuList 菜單列表
* @param {*} routes 遞歸創(chuàng)建的動(dòng)態(tài)(菜單)路由
*/
function addDynamicRoutes (menuList = [], routes = []) {
  var temp = []
  for (var i = 0; i < menuList.length; i++) {
    if (menuList[i].children && menuList[i].children.length >= 1) {
      temp = temp.concat(menuList[i].children)
    } else if (menuList[i].url && /\S/.test(menuList[i].url)) {
      menuList[i].url = menuList[i].url.replace(/^\//, '')
      // 創(chuàng)建路由配置
      var route = {
        path: menuList[i].url,
        component: null,
        name: menuList[i].name,
        meta: {
          icon: menuList[i].icon,
          index: menuList[i].id
        }
      }
      let path = getIFramePath(menuList[i].url)
      if (path) {
        // 如果是嵌套頁面, 通過iframe展示
        route['path'] = path
        route['component'] = resolve => require([`@/views/IFrame/IFrame`], resolve)
        // 存儲嵌套頁面路由路徑和訪問URL
        let url = getIFrameUrl(menuList[i].url)
        let iFrameUrl = {'path': path, 'url': url}
        store.commit('addIFrameUrl', iFrameUrl)
      } else {
        try {
          // 根據(jù)菜單URL動(dòng)態(tài)加載vue組件,這里要求vue組件須按照url路徑存儲
          // 如url="sys/user"赦役,則組件路徑應(yīng)是"@/views/sys/user.vue",否則組件加載不到
          let array = menuList[i].url.split('/')
          let url = ''
          for (let i = 0; i < array.length; i++) {
            url += array[i].substring(0, 1).toUpperCase() + array[i].substring(1) + '/'
          }
          url = url.substring(0, url.length - 1)
          route['component'] = resolve => require([`@/views/${url}`], resolve)
        } catch (e) {}
      }
      routes.push(route)
    }
  }
  if (temp.length >= 1) {
    addDynamicRoutes(temp, routes)
  } else {
    console.log(routes)
  }
  return routes
}

export default router

  • src\utils\iframe.js
/**
 * 嵌套頁面IFrame模塊
 */
import { baseUrl } from '@/utils/global'

/**
 * 嵌套頁面URL地址
 * iframe:**** -> ****
 * http[s]://**** -> ****
 * @param {*} url
 */
export function getIFramePath (url) {
  let iframeUrl = ''
  // 匹配以iframe:開頭的任意長度的字符串
  if (/^iframe:.*/.test(url)) {
    iframeUrl = url.replace('iframe:', '')
  } else if (/^http[s]?:\/\/.*/.test(url)) {
    iframeUrl = url.replace('http://', '')
    iframeUrl = url.replace('https://', '')
    if (iframeUrl.indexOf(':') !== -1) {
      iframeUrl = iframeUrl.substring(iframeUrl.lastIndexOf(':') + 1)
    }
  }
  return iframeUrl
}

/**
 * 獲取嵌套頁面路由的路徑
 * iframe:druid/index.html -> baseUrl + druid/index.html
 * http[s]://druid/index.html -> 不變
 * @param {*} url
 */
export function getIFrameUrl (url) {
  let iframeUrl = ''
  if (/^iframe:.*/.test(url)) {
    iframeUrl = baseUrl + url.replace('iframe:', '')
  } else if (/^http[s]?:\/\/.*/.test(url)) {
    iframeUrl = url
  }
  return iframeUrl
}
  • src/views/IFrame/IFrame.js
<template>
  <div class="iframe-container">
    <iframe :src="src" scrolling="auto" frameborder="0" class="frame" :onload="onloaded()">
    </iframe>
  </div>
</template>

<script>
export default {
  data () {
    return {
      src: '',
      loading: null
    }
  },
  methods: {
    // 獲取路徑
    resetSrc: function (url) {
      this.src = url
      this.load()
    },
    load: function () {
      this.loading = this.$loading({
        lock: true,
        text: 'loading...',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.5)',
        // fullscreen: false,
        target: document.querySelector('#main-container ')
      })
    },
    onloaded: function () {
      if (this.loading) {
        this.loading.close()
      }
    }
  },
  mounted () {
    this.resetSrc(this.$store.state.iframe.iframeUrl)
  },
  watch: {
    $route: {
      handler: function (val, oldVal) {
        // 如果是跳轉(zhuǎn)到嵌套頁面麻敌,切換iframe的url
        this.resetSrc(this.$store.state.iframe.iframeUrl)
      }
    }
  }
}
</script>

<style lang="scss">
.iframe-container {
  position: absolute;
  top: 0px;
  left: 0px;
  right: 0px;;
  bottom: 0px;
  .frame {
    width: 100%;
    height: 100%;
  }
}
</style>

真正投入使用的router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

const router = new Router({
  routes: [
    {
      path: '/',
      name: '首頁',
      component: HelloWorld,
      children: [
        {
          path: '',
          name: '系統(tǒng)介紹',
          component: HelloWorld,
          meta: {
            icon: 'fa fa-home fa-lg',
            index: 0
          }
        }
      ]
    }
  ]
})

/**
 * 導(dǎo)航守衛(wèi)
 */
router.beforeEach((to, from, next) => {
  let userName = sessionStorage.getItem('user')
  if (to.path === '/login') {
    if (userName) {
      next({ path: '/' })
    } else {
      next()
    }
  } else {
    if (!userName) {
      next({ path: '/login' })
    } else {
      next()
    }
  }
})

export default router

導(dǎo)航事件:

let path = getIFramePath(menu.url)
if (!path)
  path = menu.url
this.$router.push("/" + path);
七、頁面權(quán)限控制

權(quán)限標(biāo)識是對頁面資源進(jìn)行權(quán)限控制的唯一標(biāo)識掂摔,主要是增术羔、刪赢赊、改、查的權(quán)限控制级历。權(quán)限標(biāo)識主要包含四種释移,以用戶管理為例,權(quán)限標(biāo)識包括sys:user:add(新增)鱼喉、sys:user:edit(編輯)秀鞭、sys:user:delete(刪除)、sys:user:view(查看)扛禽。
src\permission/index.js

import store from '@/store'
/**
 * 判斷用戶是否擁有操作權(quán)限
 * 根據(jù)傳入的權(quán)限標(biāo)識锋边,查看是否存在用戶權(quán)限標(biāo)識集合
 * @param perms
 */
export function hasPermission (perms) {
    let hasPermission = false
    let permissions = store.state.user.perms
    for(let i=0, len=permissions.length; i<len; i++) {
        if(permissions[i] === perms) {
            hasPermission = true;
            break
        }
    }
    return hasPermission
}

頁面操作按鈕提供perms屬性綁定權(quán)限標(biāo)識,使用disable屬性綁定權(quán)限判斷方法的返回值编曼,權(quán)限判斷方法hasPerms(perms)通過查找上一步保存的用戶權(quán)限標(biāo)識集合是否包含perms來包含說明用戶擁有此相關(guān)權(quán)限豆巨,否則設(shè)置當(dāng)前操作按鈕為不可用狀態(tài)。
新建一個(gè)權(quán)限按鈕的組件:

<template>
  <el-button :size="size" :type="type" :icon="icon"
    :loading="loading" :disabled="!hasPerms(perms)" @click="handleClick">
    {{label}}
  </el-button>
</template>

<script>
import { hasPermission } from '@/permission/index.js'
export default {
  name: 'KtButton',
  props: {
    label: {  // 按鈕顯示文本
      type: String,
      default: 'Button'
    },
    icon: {  // 按鈕顯示圖標(biāo)
      type: String,
      default: ''
    },
    size: {  // 按鈕尺寸
      type: String,
      default: 'mini'
    },
    type: {  // 按鈕類型
      type: String,
      default: null
    },
    loading: {  // 按鈕加載標(biāo)識
      type: Boolean,
      default: false
    },
    disabled: {  // 按鈕是否禁用
      type: Boolean,
      default: false
    },
    perms: {  // 按鈕權(quán)限標(biāo)識掐场,外部使用者傳入
      type: String,
      default: null
    }
  },
  data() {
    return {
    }
  },
  methods: {
    handleClick: function () {
      // 按鈕操作處理函數(shù)
      this.$emit('click', {})
    }, 
    hasPerms: function (perms) {
      // 根據(jù)權(quán)限標(biāo)識和外部指示狀態(tài)進(jìn)行權(quán)限判斷
      return hasPermission(perms) & !this.disabled
    }
  },
  mounted() {
  }
}
</script>

<style scoped>

</style>
image.png

表格組件

六往扔、框架頁面設(shè)計(jì)
  1. Home頁設(shè)計(jì)
    Home.vue主頁由導(dǎo)航菜單、頭部區(qū)域和主內(nèi)容區(qū)域組成熊户。
<template>
  <div class="container">
      <!-- 導(dǎo)航菜單欄 -->
      <nav-bar></nav-bar>
      <!-- 頭部區(qū)域 -->
      <head-bar></head-bar>
      <!-- 主內(nèi)容區(qū)域 -->
      <main-content></main-content>
  </div>
</template>

<script>
import HeadBar from "./HeadBar"
import NavBar from "./NavBar"
import MainContent from "./MainContent"
export default {
  components:{
        HeadBar,
        NavBar,
        MainContent
  }
};
</script>

<style scoped lang="scss">
  .container {
    position:absolute;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
    // background: rgba(224, 234, 235, 0.1);
  }
</style>  
  1. 頂部菜單欄設(shè)計(jì)Headbar
<template> 
  <div class="headbar" :style="{'background':themeColor}"  
    :class="collapse?'position-collapse-left':'position-left'">
    <!-- 導(dǎo)航收縮 -->
    <span class="hamburg">
      <el-menu class="el-menu-demo" :background-color="themeColor" text-color="#fff" 
        :active-text-color="themeColor" mode="horizontal">
        <el-menu-item index="1" @click="onCollapse">
          <hamburger :isActive="collapse"></hamburger>
        </el-menu-item>
      </el-menu>
    </span>
    <!-- 導(dǎo)航菜單 -->
    <span class="navbar">
      <el-menu :default-active="activeIndex" class="el-menu-demo" 
          :background-color="themeColor" text-color="#fff" active-text-color="#ffd04b" mode="horizontal" @select="selectNavBar()">
        <el-menu-item index="1" @click="$router.push('/')">{{$t("common.home")}}</el-menu-item>
        <el-menu-item index="2" @click="openWindow('https://gitee.com/liuge1988/kitty/wikis/Home')">{{$t("common.doc")}}</el-menu-item>
        <el-menu-item index="3" @click="openWindow('https://www.cnblogs.com/xifengxiaoma/')">{{$t("common.blog")}}</el-menu-item>
      </el-menu>
    </span>
    <!-- 工具欄 -->
    <span class="toolbar">
      <el-menu class="el-menu-demo" :background-color="themeColor" text-color="#14889A" 
        :active-text-color="themeColor" mode="horizontal">
        <el-menu-item index="1">
          <!-- 主題切換 -->
          <theme-picker class="theme-picker" :default="themeColor" 
            @onThemeChange="onThemeChange">
          </theme-picker>
        </el-menu-item>
        <el-menu-item index="2" v-popover:popover-lang>
          <!-- 語言切換 -->
          <li style="color:#fff;" class="fa fa-language fa-lg"></li>
          <el-popover ref="popover-lang" placement="bottom-start" trigger="click" v-model="langVisible">
            <div class="lang-item" @click="changeLanguage('zh_cn')">簡體中文</div>
            <div class="lang-item" @click="changeLanguage('en_us')">English</div>
          </el-popover>
        </el-menu-item>
        <el-menu-item index="3" v-popover:popover-message>
          <!-- 我的私信 -->
          <el-badge :value="5" :max="99" class="badge">
            <li style="color:#fff;" class="fa fa-envelope-o fa-lg"></li>
          </el-badge>
          <el-popover ref="popover-message" placement="bottom-end" trigger="click">
            <message-panel></message-panel>
          </el-popover>
        </el-menu-item>
        <el-menu-item index="4" v-popover:popover-notice>
          <!-- 系統(tǒng)通知 -->
          <el-badge :value="4" :max="99" class="badge">
            <li style="color:#fff;" class="fa fa-bell-o fa-lg"></li>
          </el-badge>
          <el-popover ref="popover-notice" placement="bottom-end" trigger="click">
            <notice-panel></notice-panel>
          </el-popover>
        </el-menu-item>
        <el-menu-item index="5" v-popover:popover-personal>
          <!-- 用戶信息 -->
          <span class="user-info"><img :src="user.avatar" />{{user.nickName}}</span>
          <el-popover ref="popover-personal" placement="bottom-end" trigger="click" :visible-arrow="false">
            <personal-panel :user="user"></personal-panel>
          </el-popover>
        </el-menu-item>
      </el-menu>
    </span>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import mock from "@/mock/index"
import Hamburger from "@/components/Hamburger"
import ThemePicker from "@/components/ThemePicker"
import NoticePanel from "@/views/Core/NoticePanel"
import MessagePanel from "@/views/Core/MessagePanel"
import PersonalPanel from "@/views/Core/PersonalPanel"
export default {
  components:{
    Hamburger,
    ThemePicker,
    NoticePanel,
    MessagePanel,
    PersonalPanel
  },
  data() {
    return {
      user: {
      },
      activeIndex: '1',
      langVisible: false
    }
  },
  methods: {
    openWindow(url) {
      window.open(url)
    },
    selectNavBar(key, keyPath) {
      console.log(key, keyPath)
    },
    // 折疊導(dǎo)航欄
    onCollapse: function() {
      this.$store.commit('onCollapse')
    },
    // 切換主題
    onThemeChange: function(themeColor) {
      this.$store.commit('setThemeColor', themeColor)
    },
    // 語言切換
    changeLanguage(lang) {
      lang === '' ? 'zh_cn' : lang
      this.$i18n.locale = lang
      this.langVisible = false
    }
  },
  mounted() {
    var user = sessionStorage.getItem("user")
    if (user) {
      let params = {name:user}
      this.$api.user.findByName(params).then((res) => {
                if(res.code == 200) {
          this.user = res.data
          this.user.avatar = require("@/assets/user.png")
        }
      })
    }
  },
  computed:{
    ...mapState({
      themeColor: state=>state.app.themeColor,
      collapse: state=>state.app.collapse
    })
  }
}
</script>

<style scoped lang="scss">
.headbar {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 1030;
  height: 60px;
  line-height: 60px;
  border-color: rgba(180, 190, 190, 0.8);
  border-left-width: 1px;
  border-left-style: solid;
}
.hamburg {
  float: left;
}
.navbar {
  float: left;
}
.toolbar {
  float: right;
}
.lang-item {
  font-size: 16px;
  padding-left: 8px;
  padding-top: 8px;
  padding-bottom: 8px;
  cursor: pointer;
}
.lang-item:hover {
  font-size: 18px;
  background: #b0d6ce4d;
}
.user-info {
  font-size: 20px;
  color: #fff;
  cursor: pointer;
  img {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    margin: 10px 0px 10px 10px;
    float: right;
  }
}
.badge {
  line-height: 18px;
}
.position-left {
  left: 200px;
}
.position-collapse-left {
  left: 65px;
}
</style>

  1. 菜單欄設(shè)計(jì)NavBar.vue
<template>
    <div class="menu-bar-container">
    <!-- logo -->
    <div class="logo" :style="{'background-color':themeColor}" :class="collapse?'menu-bar-collapse-width':'menu-bar-width'"
      @click="$router.push('/')">
        <img v-if="collapse" src="@/assets/logo.png"/> <div>{{collapse?'':appName}}</div>
    </div>
    <!-- 導(dǎo)航菜單 -->
    <el-menu ref="navmenu" default-active="1" :class="collapse?'menu-bar-collapse-width':'menu-bar-width'"
      :collapse="collapse" :collapse-transition="false" :unique-opened="true  "
      @open="handleopen" @close="handleclose" @select="handleselect">
      <!-- 導(dǎo)航菜單樹組件萍膛,動(dòng)態(tài)加載菜單 -->
      <menu-tree v-for="item in navTree" :key="item.id" :menu="item"></menu-tree>
    </el-menu>
    </div>
</template>

<script>
import { mapState } from 'vuex'
import MenuTree from "@/components/MenuTree"
export default {
  components:{
        MenuTree
  },
  computed: {
    ...mapState({
      appName: state=>state.app.appName,
      themeColor: state=>state.app.themeColor,
      collapse: state=>state.app.collapse,
      navTree: state=>state.menu.navTree
    }),
    mainTabs: {
      get () { return this.$store.state.tab.mainTabs },
      set (val) { this.$store.commit('updateMainTabs', val) }
    },
    mainTabsActiveName: {
      get () { return this.$store.state.tab.mainTabsActiveName },
      set (val) { this.$store.commit('updateMainTabsActiveName', val) }
    }
  },
  watch: {
    $route: 'handleRoute'
  },
  created () {
    this.handleRoute(this.$route)
  },
  methods: {
    handleopen() {
      console.log('handleopen')
    },
    handleclose() {
      console.log('handleclose')
    },
    handleselect(a, b) {
      console.log('handleselect')
    },
    // 路由操作處理
    handleRoute (route) {
      // tab標(biāo)簽頁選中, 如果不存在則先添加
      var tab = this.mainTabs.filter(item => item.name === route.name)[0]
      if (!tab) {
        tab = {
          name: route.name,
          title: route.name,
          icon: route.meta.icon
        }
        this.mainTabs = this.mainTabs.concat(tab)
      }
      this.mainTabsActiveName = tab.name
      // 切換標(biāo)簽頁時(shí)同步更新高亮菜單
      if(this.$refs.navmenu != null) {
        this.$refs.navmenu.activeIndex = '' + route.meta.index
        this.$refs.navmenu.initOpenedMenu()
      }
    }
  }
}
</script>

<style scoped lang="scss">
.menu-bar-container {
  position: fixed;
  top: 0px;
  left: 0;
  bottom: 0;
  z-index: 1020;
  .el-menu {
    position:absolute;
    top: 60px;
    bottom: 0px;
    text-align: left;
    // background-color: #2968a30c;
  }
  .logo {
    position:absolute;
    top: 0px;
    height: 60px;   
    line-height: 60px;
    background: #545c64;
    cursor:pointer;
    img {
        width: 40px;
        height: 40px;
        border-radius: 0px;
        margin: 10px 10px 10px 10px;
        float: left;
    }
    div {
      font-size: 22px;
      color: white;
      text-align: left;
      padding-left: 20px;
    }
  }
  .menu-bar-width {
    width: 200px;
  }
  .menu-bar-collapse-width {
    width: 65px;
  }
}

</style>
  1. 子頁面框架設(shè)計(jì)MainContent
<template>
  <div id="main-container" class="main-container" :class="$store.state.app.collapse?'position-collapse-left':'position-left'">
    <!-- 標(biāo)簽頁 -->
    <div class="tab-container">
      <el-tabs class="tabs" :class="$store.state.app.collapse?'position-collapse-left':'position-left'"
        v-model="mainTabsActiveName" :closable="true" type="card"
        @tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
        <el-dropdown class="tabs-tools" :show-timeout="0" trigger="hover">
          <div style="font-size:20px;width:50px;"><i class="el-icon-arrow-down"></i></div>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="tabsCloseCurrentHandle">關(guān)閉當(dāng)前標(biāo)簽</el-dropdown-item>
            <el-dropdown-item @click.native="tabsCloseOtherHandle">關(guān)閉其它標(biāo)簽</el-dropdown-item>
            <el-dropdown-item @click.native="tabsCloseAllHandle">關(guān)閉全部標(biāo)簽</el-dropdown-item>
            <el-dropdown-item @click.native="tabsRefreshCurrentHandle">刷新當(dāng)前標(biāo)簽</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
        <el-tab-pane v-for="item in mainTabs"
          :key="item.name" :label="item.title" :name="item.name">
          <span slot="label"><i :class="item.icon"></i> {{item.title}} </span>
        </el-tab-pane>
      </el-tabs>
    </div>
    <!-- 主內(nèi)容區(qū)域 -->
    <div class="main-content">
      <keep-alive>
        <transition name="fade" mode="out-in">
            <router-view></router-view>
        </transition>
      </keep-alive>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
    }
  },
  computed: {
    mainTabs: {
      get () { return this.$store.state.tab.mainTabs },
      set (val) { this.$store.commit('updateMainTabs', val) }
    },
    mainTabsActiveName: {
      get () { return this.$store.state.tab.mainTabsActiveName },
      set (val) { this.$store.commit('updateMainTabsActiveName', val) }
    }
  },
  methods: {
    // tabs, 選中tab
    selectedTabHandle (tab) {
      tab = this.mainTabs.filter(item => item.name === tab.name)
      if (tab.length >= 1) {
        this.$router.push({ name: tab[0].name })
      }
    },
    // tabs, 刪除tab
    removeTabHandle (tabName) {
      this.mainTabs = this.mainTabs.filter(item => item.name !== tabName)
      if (this.mainTabs.length >= 1) {
        // 當(dāng)前選中tab被刪除
        if (tabName === this.mainTabsActiveName) {
          this.$router.push({ name: this.mainTabs[this.mainTabs.length - 1].name }, () => {
            this.mainTabsActiveName = this.$route.name
          })
        }
      } else {
        this.$router.push("/")
      }
    },
    // tabs, 關(guān)閉當(dāng)前
    tabsCloseCurrentHandle () {
      this.removeTabHandle(this.mainTabsActiveName)
    },
    // tabs, 關(guān)閉其它
    tabsCloseOtherHandle () {
      this.mainTabs = this.mainTabs.filter(item => item.name === this.mainTabsActiveName)
    },
    // tabs, 關(guān)閉全部
    tabsCloseAllHandle () {
      this.mainTabs = []
      this.$router.push("/")
    },
    // tabs, 刷新當(dāng)前
    tabsRefreshCurrentHandle () {
      var tempTabName = this.mainTabsActiveName
      this.removeTabHandle(tempTabName)
      this.$nextTick(() => {
        this.$router.push({ name: tempTabName })
      })
    }
  }
}
</script>

<style scoped lang="scss">
.main-container {
  padding: 0 5px 5px;
  position: absolute;
  top: 60px;
  left: 1px;
  right: 1px;
  bottom: 0px;
  // background: rgba(56, 5, 114, 0.5);
  .tabs {
    position: fixed;
    top: 60px;
    right: 50px;
    padding-left: 0px;
    padding-right: 2px;
    z-index: 1020;
    height: 40px;
    line-height: 40px;
    font-size: 14px;
    background: rgb(255, 253, 255);
    border-color: rgba(200, 206, 206, 0.5);
    // border-left-width: 1px;
    // border-left-style: solid;
    border-bottom-width: 1px;
    border-bottom-style: solid;
  }
 .tabs-tools {
    position: fixed;
    top: 60px;
    right: 0;
    z-index: 1020;
    height: 40px;
    // padding: 0 10px;
    font-size: 14px;
    line-height: 40px;
    cursor: pointer;
    border-color: rgba(200, 206, 206, 0.5);
    border-left-width: 1px;
    border-left-style: solid;
    border-bottom-width: 1px;
    border-bottom-style: solid;
    background: rgba(255, 255, 255, 1);
  }
  .tabs-tools:hover {
    background: rgba(200, 206, 206, 1);
  }
  .main-content {
    position: absolute;
    top: 45px;
    left: 5px;
    right: 5px;
    bottom: 5px;
    padding: 5px;
    // background: rgba(209, 212, 212, 0.5);
  }
}
.position-left {
  left: 200px;
}
.position-collapse-left {
  left: 65px;
}
</style>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市嚷堡,隨后出現(xiàn)的幾起案子蝗罗,更是在濱河造成了極大的恐慌,老刑警劉巖蝌戒,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件串塑,死亡現(xiàn)場離奇詭異,居然都是意外死亡北苟,警方通過查閱死者的電腦和手機(jī)桩匪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來友鼻,“玉大人傻昙,你說我怎么就攤上這事〔嗜樱” “怎么了妆档?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長借杰。 經(jīng)常有香客問我过吻,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任纤虽,我火速辦了婚禮乳绕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘逼纸。我一直安慰自己洋措,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布杰刽。 她就那樣靜靜地躺著菠发,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贺嫂。 梳的紋絲不亂的頭發(fā)上滓鸠,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天,我揣著相機(jī)與錄音第喳,去河邊找鬼糜俗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛曲饱,可吹牛的內(nèi)容都是我干的悠抹。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼扩淀,長吁一口氣:“原來是場噩夢啊……” “哼楔敌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起驻谆,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤卵凑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后旺韭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氛谜,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掏觉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年区端,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澳腹。...
    茶點(diǎn)故事閱讀 40,926評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡织盼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酱塔,到底是詐尸還是另有隱情沥邻,我是刑警寧澤,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布羊娃,位于F島的核電站唐全,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邮利,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一弥雹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧延届,春花似錦剪勿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至械念,卻和暖如春头朱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背龄减。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工髓窜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人欺殿。 一個(gè)月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓寄纵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脖苏。 傳聞我的和親對象是個(gè)殘疾皇子程拭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容