Vue-admin-template新增TagViews标签页功能,附完整代码

news2024/11/16 3:16:33

前言

vue-admin-template里面本身是没有TagViews标签页的,只有完整版的vue-element-admin才有,翻找网上的其他教程,要么代码不完整,要么有bug,本篇文章就教大家如何在vue-admin-template的基础上新增TagViews
在这里插入图片描述

步骤

1. 从vue-element-admin复制文件

-新增文件夹\src\layout\components\TagsView,并新建文件index.vueScrollPane.vue
在这里插入图片描述
index.vue

<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane
      ref="scrollPane"
      class="tags-view-wrapper"
      @scroll="handleScroll"
    >
      <router-link
        v-for="tag in visitedViews"
        ref="tag"
        :key="tag.path"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        tag="span"
        class="tags-view-item"
        :style="activeStyle(tag)"
        @click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
        @contextmenu.prevent.native="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span
          v-if="!isAffix(tag)"
          class="el-icon-close"
          @click.prevent.stop="closeSelectedTag(tag)"
        />
      </router-link>
    </scroll-pane>
    <ul
      v-show="visible"
      :style="{ left: left + 'px', top: top + 'px' }"
      class="contextmenu"
    >
      <!-- <li @click="refreshSelectedTag(selectedTag)">
        <i class="el-icon-refresh-right"></i> 刷新页面
      </li> -->
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
        <i class="el-icon-close"></i> 关闭当前
      </li>
      <li @click="closeOthersTags">
        <i class="el-icon-circle-close"></i> 关闭其他
      </li>
      <li v-if="!isFirstView()" @click="closeLeftTags">
        <i class="el-icon-back"></i> 关闭左侧
      </li>
      <li v-if="!isLastView()" @click="closeRightTags">
        <i class="el-icon-right"></i> 关闭右侧
      </li>
      <li @click="closeAllTags(selectedTag)">
        <i class="el-icon-circle-close"></i> 全部关闭
      </li>
    </ul>
  </div>
</template>

<script>
import ScrollPane from "./ScrollPane";
import path from "path";

export default {
  components: { ScrollPane },
  data() {
    return {
      visible: false,
      top: 0,
      left: 0,
      selectedTag: {},
      affixTags: [],
    };
  },
  computed: {
    visitedViews() {
      return this.$store.state.tagsView.visitedViews;
    },
    // 没有用到权限验证
    // routes() {
    // return this.$store.state.permission.routes
    // },
    theme() {
      return this.$store.state.settings.theme;
    },
  },
  watch: {
    $route() {
      this.addTags();
      this.moveToCurrentTag();
    },
    visible(value) {
      if (value) {
        document.body.addEventListener("click", this.closeMenu);
      } else {
        document.body.removeEventListener("click", this.closeMenu);
      }
    },
  },
  mounted() {
    this.initTags();
    this.addTags();
     // 刷新页面标签不丢失
     this.beforeUnload();
  },
  methods: {
    beforeUnload() {
      // 监听页面刷新
      window.addEventListener("beforeunload", () => {
        // visitedViews数据结构太复杂无法直接JSON.stringify处理,先转换需要的数据
        // console.log(this.visitedViews,'this.visitedViews')
        let tabViews = this.visitedViews.map((item) => {
          return {
            fullPath: item.fullPath,
            hash: item.hash,
            meta: { ...item.meta },
            name: item.name,
            params: { ...item.params },
            path: item.path,
            query: { ...item.query },
            title: item.title,
          };
        });
        sessionStorage.setItem("tabViews", JSON.stringify(tabViews));
      });
      // 页面初始化加载判断缓存中是否有数据
      let oldViews = JSON.parse(sessionStorage.getItem("tabViews")) || [];
      //  console.log(oldViews,'this.visitedViews2')
      if (oldViews.length > 0) {
        this.$store.state.tagsView.visitedViews = oldViews;
      }
    },
    isActive(route) {
      return route.path === this.$route.path;
    },
    activeStyle(tag) {
      if (!this.isActive(tag)) return {};
      return {
        "background-color": this.theme,
        "border-color": this.theme,
      };
    },
    isAffix(tag) {
      return tag.meta && tag.meta.affix;
    },
    isFirstView() {
      try {
        return (
          this.selectedTag.fullPath === this.visitedViews[1].fullPath ||
          this.selectedTag.fullPath === "/index"
        );
      } catch (err) {
        return false;
      }
    },
    isLastView() {
      try {
        return (
          this.selectedTag.fullPath ===
          this.visitedViews[this.visitedViews.length - 1].fullPath
        );
      } catch (err) {
        return false;
      }
    },
    filterAffixTags(routes, basePath = "/") {
      let tags = [];
      if (this.routes) {
        routes.forEach((route) => {
          if (route.meta && route.meta.affix) {
            const tagPath = path.resolve(basePath, route.path);
            tags.push({
              fullPath: tagPath,
              path: tagPath,
              name: route.name,
              meta: { ...route.meta },
            });
          }
          if (route.children) {
            const tempTags = this.filterAffixTags(route.children, route.path);
            if (tempTags.length >= 1) {
              tags = [...tags, ...tempTags];
            }
          }
        });
      }
      return tags;
    },
    initTags() {
      const affixTags = (this.affixTags = this.filterAffixTags(this.routes));
      for (const tag of affixTags) {
        // Must have tag name
        if (tag.name) {
          this.$store.dispatch("tagsView/addVisitedView", tag);
        }
      }
    },
    addTags() {
      const { name } = this.$route;
      if (name) {
        this.$store.dispatch("tagsView/addView", this.$route);
        if (this.$route.meta.link) {
          this.$store.dispatch("tagsView/addIframeView", this.$route);
        }
      }
      return false;
    },
    moveToCurrentTag() {
      const tags = this.$refs.tag;
      this.$nextTick(() => {
        for (const tag of tags) {
          if (tag.to.path === this.$route.path) {
            this.$refs.scrollPane.moveToTarget(tag);
            // when query is different then update
            if (tag.to.fullPath !== this.$route.fullPath) {
              this.$store.dispatch("tagsView/updateVisitedView", this.$route);
            }
            break;
          }
        }
      });
    },
    refreshSelectedTag(view) {
      this.$tab.refreshPage(view);
      // if (this.$route.meta.link) {
        this.$store.dispatch("tagsView/delIframeView", this.$route).then((res)=>{
          console.log(res);
          console.log(view);
          const {fullPath}=view;
          this.$nextTick(()=>{
            this.$router.replace({
              path:'/redirect'+fullPath
            })
          })
        });
      // }
    },
    closeSelectedTag(view) {
      this.$tab.closePage(view).then(({ visitedViews }) => {
        if (this.isActive(view)) {
          this.toLastView(visitedViews, view);
        }
      });
    },
    closeRightTags() {
      this.$tab.closeRightPage(this.selectedTag).then((visitedViews) => {
        if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
          this.toLastView(visitedViews);
        }
      });
    },
    closeLeftTags() {
      this.$tab.closeLeftPage(this.selectedTag).then((visitedViews) => {
        if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
          this.toLastView(visitedViews);
        }
      });
    },
    closeOthersTags() {
      console.log('关闭其他标签');
      // this.$router.push(this.selectedTag).catch(() => {});
      this.$router.push(this.selectedTag);
      this.$tab.closeOtherPage(this.selectedTag).then(() => {
        this.moveToCurrentTag();
      }).catch(() => {});
    },
    closeAllTags(view) {
      this.$tab.closeAllPage().then(({ visitedViews }) => {
        if (this.affixTags.some((tag) => tag.path === this.$route.path)) {
          return;
        }
        this.toLastView(visitedViews, view);
      });
    },
    toLastView(visitedViews, view) {
      const latestView = visitedViews.slice(-1)[0];
      if (latestView) {
        this.$router.push(latestView.fullPath);
      } else {
        // now the default is to redirect to the home page if there is no tags-view,
        // you can adjust it according to your needs.
        if (view.name === "Dashboard") {
          // to reload home page
          this.$router.replace({ path: "/redirect" + view.fullPath });
        } else {
          this.$router.push("/");
        }
      }
    },
    openMenu(tag, e) {
      const menuMinWidth = 105;
      const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
      const offsetWidth = this.$el.offsetWidth; // container width
      const maxLeft = offsetWidth - menuMinWidth; // left boundary
      const left = e.clientX - offsetLeft + 15; // 15: margin right

      if (left > maxLeft) {
        this.left = maxLeft;
      } else {
        this.left = left;
      }

      this.top = e.clientY;
      this.visible = true;
      this.selectedTag = tag;
    },
    closeMenu() {
      this.visible = false;
    },
    handleScroll() {
      this.closeMenu();
    },
  },
};
</script>

<style lang="scss" scoped>
.tags-view-container {
  height: 34px;
  width: 100%;
  background: #fff;
  border-bottom: 1px solid #d8dce5;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  .tags-view-wrapper {
    .tags-view-item {
      display: inline-block;
      position: relative;
      cursor: pointer;
      height: 26px;
      line-height: 26px;
      border: 1px solid #d8dce5;
      color: #495060;
      background: #fff;
      padding: 0 8px;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 4px;
      &:first-of-type {
        margin-left: 15px;
      }
      &:last-of-type {
        margin-right: 15px;
      }
      &.active {
        background-color: #42b983;
        color: #fff;
        border-color: #42b983;
        &::before {
          content: "";
          background: #fff;
          display: inline-block;
          width: 8px;
          height: 8px;
          border-radius: 50%;
          position: relative;
          margin-right: 2px;
        }
      }
    }
  }
  .contextmenu {
    margin: 0;
    background: #fff;
    z-index: 3000;
    position: absolute;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 400;
    color: #333;
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
    li {
      margin: 0;
      padding: 7px 16px;
      cursor: pointer;
      &:hover {
        background: #eee;
      }
    }
  }
}
</style>

<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
  .tags-view-item {
    .el-icon-close {
      width: 16px;
      height: 16px;
      vertical-align: 2px;
      border-radius: 50%;
      text-align: center;
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(0.6);
        display: inline-block;
        vertical-align: -3px;
      }
      &:hover {
        background-color: #b4bccc;
        color: #fff;
      }
    }
  }
}
</style>

ScrollPane.vue

<template>
  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
    <slot />
  </el-scrollbar>
</template>

<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing

export default {
  name: 'ScrollPane',
  data() {
    return {
      left: 0
    }
  },
  computed: {
    scrollWrapper() {
      return this.$refs.scrollContainer.$refs.wrap
    }
  },
  mounted() {
    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
  },
  beforeDestroy() {
    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
  },
  methods: {
    handleScroll(e) {
      const eventDelta = e.wheelDelta || -e.deltaY * 40
      const $scrollWrapper = this.scrollWrapper
      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
    },
    emitScroll() {
      this.$emit('scroll')
    },
    moveToTarget(currentTag) {
      const $container = this.$refs.scrollContainer.$el
      const $containerWidth = $container.offsetWidth
      const $scrollWrapper = this.scrollWrapper
      const tagList = this.$parent.$refs.tag

      let firstTag = null
      let lastTag = null

      // find first tag and last tag
      if (tagList.length > 0) {
        firstTag = tagList[0]
        lastTag = tagList[tagList.length - 1]
      }

      if (firstTag === currentTag) {
        $scrollWrapper.scrollLeft = 0
      } else if (lastTag === currentTag) {
        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
      } else {
        // find preTag and nextTag
        const currentIndex = tagList.findIndex(item => item === currentTag)
        const prevTag = tagList[currentIndex - 1]
        const nextTag = tagList[currentIndex + 1]

        // the tag's offsetLeft after of nextTag
        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing

        // the tag's offsetLeft before of prevTag
        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing

        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
        }
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.scroll-container {
  white-space: nowrap;
  position: relative;
  overflow: hidden;
  width: 100%;
  ::v-deep {
    .el-scrollbar__bar {
      bottom: 0px;
    }
    .el-scrollbar__wrap {
      height: 49px;
    }
  }
}
</style>

  • 从vue-element-admin复制文件src\store\modules\tagsView.js并放到相同目录下
    在这里插入图片描述
const state = {
  visitedViews: [],
  cachedViews: [],
  iframeViews: []
}

const mutations = {
  ADD_IFRAME_VIEW: (state, view) => {
    if (state.iframeViews.some(v => v.path === view.path)) return
    state.iframeViews.push(
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    )
  },
  ADD_VISITED_VIEW: (state, view) => {
    if (state.visitedViews.some(v => v.path === view.path)) return
    state.visitedViews.push(
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    )
  },
  ADD_CACHED_VIEW: (state, view) => {
    if (state.cachedViews.includes(view.name)) return
    if (view.meta && !view.meta.noCache) {
      state.cachedViews.push(view.name)
    }
  },
  DEL_VISITED_VIEW: (state, view) => {
    for (const [i, v] of state.visitedViews.entries()) {
      if (v.path === view.path) {
        state.visitedViews.splice(i, 1)
        break
      }
    }
    state.iframeViews = state.iframeViews.filter(item => item.path !== view.path)
  },
  DEL_IFRAME_VIEW: (state, view) => {
    state.iframeViews = state.iframeViews.filter(item => item.path !== view.path)
  },
  DEL_CACHED_VIEW: (state, view) => {
    const index = state.cachedViews.indexOf(view.name)
    index > -1 && state.cachedViews.splice(index, 1)
  },

  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
    state.visitedViews = state.visitedViews.filter(v => {
      return v.meta.affix || v.path === view.path
    })
    state.iframeViews = state.iframeViews.filter(item => item.path === view.path)
  },
  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
    const index = state.cachedViews.indexOf(view.name)
    if (index > -1) {
      state.cachedViews = state.cachedViews.slice(index, index + 1)
    } else {
      state.cachedViews = []
    }
  },
  DEL_ALL_VISITED_VIEWS: state => {
    // keep affix tags
    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
    state.visitedViews = affixTags
    state.iframeViews = []
  },
  DEL_ALL_CACHED_VIEWS: state => {
    state.cachedViews = []
  },
  UPDATE_VISITED_VIEW: (state, view) => {
    for (let v of state.visitedViews) {
      if (v.path === view.path) {
        v = Object.assign(v, view)
        break
      }
    }
  },
  DEL_RIGHT_VIEWS: (state, view) => {
    const index = state.visitedViews.findIndex(v => v.path === view.path)
    if (index === -1) {
      return
    }
    state.visitedViews = state.visitedViews.filter((item, idx) => {
      if (idx <= index || (item.meta && item.meta.affix)) {
        return true
      }
      const i = state.cachedViews.indexOf(item.name)
      if (i > -1) {
        state.cachedViews.splice(i, 1)
      }
      if(item.meta.link) {
        const fi = state.iframeViews.findIndex(v => v.path === item.path)
        state.iframeViews.splice(fi, 1)
      }
      return false
    })
  },
  DEL_LEFT_VIEWS: (state, view) => {
    const index = state.visitedViews.findIndex(v => v.path === view.path)
    if (index === -1) {
      return
    }
    state.visitedViews = state.visitedViews.filter((item, idx) => {
      if (idx >= index || (item.meta && item.meta.affix)) {
        return true
      }
      const i = state.cachedViews.indexOf(item.name)
      if (i > -1) {
        state.cachedViews.splice(i, 1)
      }
      if(item.meta.link) {
        const fi = state.iframeViews.findIndex(v => v.path === item.path)
        state.iframeViews.splice(fi, 1)
      }
      return false
    })
  }
}

const actions = {
  addView({ dispatch }, view) {
    dispatch('addVisitedView', view)
    dispatch('addCachedView', view)
  },
  addIframeView({ commit }, view) {
    commit('ADD_IFRAME_VIEW', view)
  },
  addVisitedView({ commit }, view) {
    commit('ADD_VISITED_VIEW', view)
  },
  addCachedView({ commit }, view) {
    commit('ADD_CACHED_VIEW', view)
  },
  delView({ dispatch, state }, view) {
    return new Promise(resolve => {
      dispatch('delVisitedView', view)
      dispatch('delCachedView', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  },
  delVisitedView({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_VISITED_VIEW', view)
      resolve([...state.visitedViews])
    })
  },
  delIframeView({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_IFRAME_VIEW', view)
      resolve([...state.iframeViews])
    })
  },
  delCachedView({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_CACHED_VIEW', view)
      resolve([...state.cachedViews])
    })
  },
  delOthersViews({ dispatch, state }, view) {
    return new Promise(resolve => {
      dispatch('delOthersVisitedViews', view)
      dispatch('delOthersCachedViews', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  },
  delOthersVisitedViews({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_OTHERS_VISITED_VIEWS', view)
      resolve([...state.visitedViews])
    })
  },
  delOthersCachedViews({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_OTHERS_CACHED_VIEWS', view)
      resolve([...state.cachedViews])
    })
  },
  delAllViews({ dispatch, state }, view) {
    return new Promise(resolve => {
      dispatch('delAllVisitedViews', view)
      dispatch('delAllCachedViews', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  },
  delAllVisitedViews({ commit, state }) {
    return new Promise(resolve => {
      commit('DEL_ALL_VISITED_VIEWS')
      resolve([...state.visitedViews])
    })
  },
  delAllCachedViews({ commit, state }) {
    return new Promise(resolve => {
      commit('DEL_ALL_CACHED_VIEWS')
      resolve([...state.cachedViews])
    })
  },
  updateVisitedView({ commit }, view) {
    commit('UPDATE_VISITED_VIEW', view)
  },
  delRightTags({ commit }, view) {
    return new Promise(resolve => {
      commit('DEL_RIGHT_VIEWS', view)
      resolve([...state.visitedViews])
    })
  },
  delLeftTags({ commit }, view) {
    return new Promise(resolve => {
      commit('DEL_LEFT_VIEWS', view)
      resolve([...state.visitedViews])
    })
  },
}

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

2. 修改\src\layout\components\AppMain.vue

src\ayout\components\AppMain.vue新增修改以下内容
在这里插入图片描述
代码:

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <!-- <router-view :key="key"  /> -->
      <router-view  :include="cachedViews" />

    </transition>
  </section>
</template>

<script>
export default {
  name: 'AppMain',
  computed: {
    // key() {
    //   return this.$route.path
    // },
    cachedViews() {
      return this.$store.state.tagsView.cachedViews
    }
  }
}
</script>

<style scoped>
.app-main {
  /*50 = navbar  */
  min-height: calc(100vh - 50px);
  width: 100%;
  position: relative;
  overflow: hidden;
}
.fixed-header+.app-main {
  padding-top: 50px;
}
</style>

<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
  .fixed-header {
    padding-right: 15px;
  }
}
</style>

3. 修改\src\layout\components\index.js

在这里插入图片描述

export { default as TagsView } from './TagsView'

4. 修改\src\layout\index.vue

在这里插入图片描述

5. 修改 \src\store\getters.js

visitedViews: state => state.tagsView.visitedViews,
  cachedViews: state => state.tagsView.cachedViews,

在这里插入图片描述

6. 修改\src\store\index.js

新增以下代码

import tagsView from './modules/tagsView'

const store = new Vuex.Store({
  modules: {
    // ...
    tagsView,
  },
})

7. 修改vue-admin-template\src\settings.js

新增一行代码

tagsView: true,

8. 修改\src\store\modules\settings.js

在这里插入图片描述

9. 新建 plugins文件夹,tab.js,index.js

在src下面新建文件夹plugins,下面新建tab.jsindex.js
tab.js

import store from '@/store'
import router from '@/router';

export default {
  // 刷新当前tab页签
  refreshPage(obj) {
    const { path, query, matched } = router.currentRoute;
    if (obj === undefined) {
      matched.forEach((m) => {
        if (m.components && m.components.default && m.components.default.name) {
          if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
            obj = { name: m.components.default.name, path: path, query: query };
          }
        }
      });
    }
    return store.dispatch('tagsView/delCachedView', obj).then(() => {
      const { path, query } = obj
      router.replace({
        path: '/redirect' + path,
        query: query
      })
    })
  },
  // 关闭当前tab页签,打开新页签
  closeOpenPage(obj) {
    store.dispatch("tagsView/delView", router.currentRoute);
    if (obj !== undefined) {
      return router.push(obj);
    }
  },
  // 关闭指定tab页签
  closePage(obj) {
    if (obj === undefined) {
      return store.dispatch('tagsView/delView', router.currentRoute).then(({ lastPath }) => {
        return router.push(lastPath || '/');
      });
    }
    return store.dispatch('tagsView/delView', obj);
  },
  // 关闭所有tab页签
  closeAllPage() {
    return store.dispatch('tagsView/delAllViews');
  },
  // 关闭左侧tab页签
  closeLeftPage(obj) {
    return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute);
  },
  // 关闭右侧tab页签
  closeRightPage(obj) {
    return store.dispatch('tagsView/delRightTags', obj || router.currentRoute);
  },
  // 关闭其他tab页签
  closeOtherPage(obj) {
    return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute);
  },
  // 添加tab页签
  openPage(title, url, params) {
    var obj = { path: url, meta: { title: title } }
    store.dispatch('tagsView/addView', obj);
    return router.push({ path: url, query: params });
  },
  // 修改tab页签
  updatePage(obj) {
    return store.dispatch('tagsView/updateVisitedView', obj);
  }
}

index.js

import tab from './tab'

export default {
  install(Vue) {
    // 页签操作
    Vue.prototype.$tab = tab
  }
}

10. 修改main.js

main.js新增两行代码:

import plugins from './plugins' // plugins
Vue.use(plugins)

效果图

大功告成:
在这里插入图片描述
在这里插入图片描述

源码

源码里面包括了角色权限动态路由
https://gitee.com/yyy1203/vue-admin-template-permission.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/23106.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式应用kafka + EFLFK集群部署

前言 Kafka是由Apache软件基金会开发的一个开源流处理平台&#xff0c;由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统&#xff0c;它可以处理消费者在网站中的所有动作流数据。 这种动作&#xff08;网页浏览&#xff0c;搜索和其他用户的行动&#xff09;…

骨传导耳机优缺点有哪些?骨传导耳机科普与推荐

骨传导耳机是一种可以开放耳朵的耳机&#xff0c;所以对于耳朵比较敏感的人来说&#xff0c;这种耳机是比较友好的&#xff0c;同时因为它的佩戴方式&#xff0c;在运动圈内也很受欢迎。只不过骨传导耳机是一种新兴的耳机&#xff0c;所以很多人并不太了解它的优缺点。 我作为…

书店销售管理系统----数据库原理及应用综合实验

枯木逢春犹再发&#xff0c;人无两度再少年&#x1f342; 系统主要模块如下&#xff1a; &#xff08;1&#xff09; 书店销售管理系统设计与实现—图书入库管理及查询统计 图书入库管理&#xff1a;维护入库图书信息&#xff08;如图书编号、书名、作者、价格、图书分类、出版…

vue-element-admin后台前端解决方案(基于 vue 和 element-ui)

vue-element-admin后台前端解决方案参考文档下载安装目录结构参考文档 vue-element-admin官网&#xff0c;更多详细内容可以查看社区学习文档。 下载安装 可以把 vue-element-admin当做工具箱或者集成方案仓库&#xff0c;在 vue-admin-template 的基础上进行二次开发&#…

Java 8 给我们更好的消灭空指针解决方案

前言 大家好&#xff0c;在平时的业务开发中&#xff0c;空指针是我们经常遇到的问题&#xff0c; 他可能会导致我们的流程无法正常进行或者一些意外情况的发生。 这就是我们需要避免空指针的原因&#xff0c;那我们有哪些方式去解决这个问题呢&#xff1f; 空指针场景 包装…

Linux系统安装DB2数据库的详细步骤

1、DB2数据库的安装 一、将DB2的安装介质上传至/home目录&#xff0c;并解压&#xff1a; tar –zxvf v9.5fp3_linuxx64_server.tar.gz 二、执行LANGC 三、进入解压后的server目录&#xff08;cd server/&#xff09;&#xff0c;执行./db2setup,步骤如下&#xff1a; # cd…

数据结构-线性表与链性表(二)

目录 一、学习背景 二、简绍 三、线性表 一、什么是线性表 二、操作 1、插入 2、删除 3、查询 三、数组应用案例中源码分析 1、插入 2、删除 3、get与set 4、扩容 二、单向链表 单向链表结构 循环链表 三、数组和链表比较 1、时间复杂度角度 2、其他维度 3、…

【JS】原生js实现矩形框的绘制/拖动/缩放

1、要点及功能描述 通过js监听mouse事件来实现矩形框的绘制&#xff0c;再通过区分点击的是边角还是其他位置来实现矩形框的缩放和拖动&#xff0c;并且在拖动和缩放时&#xff0c;都做了边界限制&#xff0c;当缩放或拖动 到边界时&#xff0c;就不能继续拉缩放拖动了。当然在…

【个人简介】一枚在上海的AndroidiOSWindow逆向电子工程师

> Hello World!, I am Humenger 「 From Shanghai, China 」 「 Android Reverse engineer, applied electronic technology Shan Dong University, China 」 &#x1f41d;主要涉及平台: Android(70%),iOS(15%),Window(5%),macOS(3%),其他(7%) &#x1f98b;主要涉…

易基因|RNA m7G甲基化测序(m7G-MeRIP-seq)

N7-甲基鸟苷&#xff08;N7-methylguanosine&#xff0c;m7G&#xff09;是真核生物tRNA、rRNA和mRNA 5cap中最丰富的修饰之一。作为一种重要的表观遗传修饰&#xff0c;m7G RNA甲基化在基因表达、加工代谢、蛋白质合成、转录稳定等方面发挥着重要的作用&#xff0c;参与疾病发…

Pinely Round 1 (Div. 1 + Div. 2) E - Make It Connected思维分类讨论

昨晚的problem e 一直wa。因为答案&#xff0c;不唯一&#xff0c;调起来只能肉眼debug。被干emo了qwq。好在赛后看到 ugly2333的 思路和我差不多&#xff0c;最后还是要选取度数较小的最优, 好像从度数的角度出发&#xff0c;不容易wa。 题意&#xff1a; 给你一个图&#xf…

什么是组织孤岛?它会带来哪些影响?可以这样去对付它

作为一个在不同地点和时区与不同团队合作的远程工作者&#xff0c;我有过公平的孤岛经历。 是的&#xff0c;它们扼杀了任何组织的成长。那么&#xff0c;在使你&#xff08;和组织中的每个人&#xff09;失去生产力、困惑、自私和不快乐之后。 在这篇文章中&#xff0c;我将…

ADRV9009中armBinary反汇编IDA参数设置

armBinary.bin文件如果不做处理的话就是一堆16进制数,扔到IDA里也只是一堆有颜色的16进制数,需要进行一些参数设置。 1 选择IDA32位打开armBinary.bin文件 2 load a new file设置 Processor type选择ARM Little-endian [ARM],点击Edit ARM architecture options进行相应修…

Linux 中的内部命令和外部命令

Linux 中的内部命令和外部命令 作者&#xff1a;Grey 原文地址&#xff1a; 博客园&#xff1a;Linux 中的内部命令和外部命令 CSDN&#xff1a;Linux 中的内部命令和外部命令 什么是 bash shell ? bash shell&#xff0c;就是一个程序&#xff0c;就是 Linux 系统安装的…

漫谈 Java 平台上的反应式编程

反应式编程&#xff08;Reactive Programming&#xff09;是一套完整的编程体系&#xff0c;既有其指导思想&#xff0c;又有相应的框架和库的支持&#xff0c;并且在生产环境中有大量实际的应用。在支持度方面&#xff0c;既有大公司参与实践&#xff0c;也有强大的开源社区的…

【Linux】-- 开发工具yum、vim、gcc、g++、gdb、make、makefile使用介绍

目录 一、yum 1.了解yum &#xff08;1&#xff09;RPM &#xff08;2&#xff09;yum 2.yum使用 &#xff08;1&#xff09;查看软件包 &#xff08;2&#xff09;安装软件 &#xff08;3&#xff09;卸载软件 二.Linux编辑器-vim 1. vim概念 &#xff08;1&am…

flink集群搭建

1、安装包flink-1.10.0-bin-scala_2.11.tgz 2、tar -zxf flink-1.10.0-bin-scala_2.11.tgz 解压到指定目录 解压之后的文件名称是flink-1.10.0 3、flink-1.10.0的目录结构如下&#xff1a; bin/&#xff1a;flink的相关命令 conf/&#xff1a;flink的配置文件 examples/&a…

业务数据分析-Excel公式与函数(三)

目录 概念 运算符 地址的引用 逻辑函数 文本函数 统计函数 查找与引用函数 日期函数 常见出错信息 概念 公式&#xff1a;Excel的核心功能&#xff0c;功能强大 如果要定义的话&#xff0c;可以说是 以开头的&#xff0c;对地址进行引用的计算形式 说的高大上一点的…

方法2—并行数据流转换为一种特殊串行数据流模块的设计

并行数据流转换为一种特殊串行数据流模块的设计&#xff0c;设计两个可综合的电路模块1&#xff0c;第一个可综合模块&#xff0c;M1。2&#xff0c;描述M2模块3&#xff0c;描述M0模块的Verilog代码4&#xff0c;描述顶层模块5&#xff0c;电路生成的门级网表&#xff0c;netl…

Camtasia2023简单易用的电脑录屏视频剪辑软件

教学、演示、培训视频轻松制作!Camtasia非常容易学习 你不需要一个大的预算或花哨的视频编辑技能。只需录制屏幕并添加一些特效即可。无论您是有经验还是这是第一次制作视频 Camtasia都会为您提供制作高质量视频所需的一切。创建观看者实际观看的内容。视频将为您提供更多的互动…