Vue实现多页签组件

直接看效果,增加了右键菜单,分别有重新加载、关闭左边、关闭右边、关闭其他功能。

也可以到我的github上看看代码(如果觉得这个组件有用的话,别忘了顺手给个小星星)

代码:https://github.com/Caijt/VuePageTab

演示:https://caijt.github.io/VuePageTab/

我这个多页签组件里面的删除缓存的方法不是使用keep-alive组件自带的include、exculde结合的效果,而是使用暴力删除缓存的方法,这个在上个博客中也有提到,用这种方法的话,可以实现更完整的多页签功能,例如同个路由可以根据参数的不同同时打开不同的页签,也能不用去写那些路由的name值。

先直接看组件代码(里面用了一些element-ui的组件,如果你们不用element-ui的话。可以去掉,自己实现)

<template>
 <div class="__common-layout-pageTabs">
  <el-scrollbar>
   <div class="__tabs">
    <div
     class="__tab-item"
     v-for="item in openedPageRouters"
     :class="{
      '__is-active': item.fullPath == $route.fullPath,
     }"
     :key="item.fullPath"
     @click="onClick(item)"
     @contextmenu.prevent="showContextMenu($event, item)"
    >
     {{ item.meta.title }}
     <span
      class="el-icon-close"
      @click.stop="onClose(item)"
      @contextmenu.prevent.stop=""
      :style="openedPageRouters.length <= 1 ? 'width:0;' : ''"
     ></span>
    </div>
   </div>
  </el-scrollbar>
  <div v-show="contextMenuVisible">
   <ul
    :style="{ left: contextMenuLeft + 'px', top: contextMenuTop + 'px' }"
    class="__contextmenu"
   >
    <li>
     <el-button type="text" @click="reload()" size="mini">
      重新加载
     </el-button>
    </li>
    <li>
     <el-button
      type="text"
      @click="closeOtherLeft"
      :disabled="false"
      size="mini"
      >关闭左边</el-button
     >
    </li>
    <li>
     <el-button
      type="text"
      @click="closeOtherRight"
      :disabled="false"
      size="mini"
      >关闭右边</el-button
     >
    </li>
    <li>
     <el-button type="text" @click="closeOther" size="mini"
      >关闭其他</el-button
     >
    </li>
   </ul>
  </div>
 </div>
</template>
<script>
export default {
 props: {
  keepAliveComponentInstance: {}, //keep-alive控件实例对象
  blankRouteName: {
   type: String,
   default: "blank",
  }, //空白路由的name值
 },
 data() {
  return {
   contextMenuVisible: false, //右键菜单是否显示
   contextMenuLeft: 0, //右键菜单显示位置
   contextMenuTop: 0, //右键菜单显示位置
   contextMenuTargetPageRoute: null, //右键所指向的菜单路由
   openedPageRouters: [], //已打开的路由页面
  };
 },
 watch: {
  //当路由变更时,执行打开页面的方法
  $route: {
   handler(v) {
    this.openPage(v);
   },
   immediate: true,
  },
 },
 mounted() {
  //添加点击关闭右键菜单
  window.addEventListener("click", this.closeContextMenu);
 },
 destroyed() {
  window.removeEventListener("click", this.closeContextMenu);
 },
 methods: {
  //打开页面
  openPage(route) {
   if (route.name == this.blankRouteName) {
    return;
   }
   let isExist = this.openedPageRouters.some(
    (item) => item.fullPath == route.fullPath
   );
   if (!isExist) {
    let openedPageRoute = this.openedPageRouters.find(
     (item) => item.path == route.path
    );
    //判断页面是否支持不同参数多开页面功能,如果不支持且已存在path值一样的页面路由,那就替换它
    if (!route.meta.canMultipleOpen && openedPageRoute != null) {
     this.delRouteCache(openedPageRoute.fullPath);
     this.openedPageRouters.splice(
      this.openedPageRouters.indexOf(openedPageRoute),
      1,
      route
     );
    } else {
     this.openedPageRouters.push(route);
    }
   }
  },
  //点击页面标签卡时
  onClick(route) {
   if (route.fullPath !== this.$route.fullPath) {
    this.$router.push(route.fullPath);
   }
  },
  //关闭页面标签时
  onClose(route) {
   let index = this.openedPageRouters.indexOf(route);
   this.delPageRoute(route);
   if (route.fullPath === this.$route.fullPath) {
    //删除页面后,跳转到上一页面
    this.$router.replace(
     this.openedPageRouters[index == 0 ? 0 : index - 1]
    );
   }
  },
  //右键显示菜单
  showContextMenu(e, route) {
   this.contextMenuTargetPageRoute = route;
   this.contextMenuLeft = e.layerX;
   this.contextMenuTop = e.layerY;
   this.contextMenuVisible = true;
  },
  //隐藏右键菜单
  closeContextMenu() {
   this.contextMenuVisible = false;
   this.contextMenuTargetPageRoute = null;
  },
  //重载页面
  reload() {
   this.delRouteCache(this.contextMenuTargetPageRoute.fullPath);
   if (this.contextMenuTargetPageRoute.fullPath === this.$route.fullPath) {
    this.$router.replace({ name: this.blankRouteName }).then(() => {
     this.$router.replace(this.contextMenuTargetPageRoute);
    });
   }
  },
  //关闭其他页面
  closeOther() {
   for (let i = 0; i < this.openedPageRouters.length; i++) {
    let r = this.openedPageRouters[i];
    if (r !== this.contextMenuTargetPageRoute) {
     this.delPageRoute(r);
     i--;
    }
   }
   if (this.contextMenuTargetPageRoute.fullPath != this.$route.fullPath) {
    this.$router.replace(this.contextMenuTargetPageRoute);
   }
  },
  //根据路径获取索引
  getPageRouteIndex(fullPath) {
   for (let i = 0; i < this.openedPageRouters.length; i++) {
    if (this.openedPageRouters[i].fullPath === fullPath) {
     return i;
    }
   }
  },
  //关闭左边页面
  closeOtherLeft() {
   let index = this.openedPageRouters.indexOf(
    this.contextMenuTargetPageRoute
   );
   let currentIndex = this.getPageRouteIndex(this.$route.fullPath);
   if (index > currentIndex) {
    this.$router.replace(this.contextMenuTargetPageRoute);
   }
   for (let i = 0; i < index; i++) {
    let r = this.openedPageRouters[i];
    this.delPageRoute(r);
    i--;
    index--;
   }
  },
  //关闭右边页面
  closeOtherRight() {
   let index = this.openedPageRouters.indexOf(
    this.contextMenuTargetPageRoute
   );
   let currentIndex = this.getPageRouteIndex(this.$route.fullPath);
   for (let i = index + 1; i < this.openedPageRouters.length; i++) {
    let r = this.openedPageRouters[i];
    this.delPageRoute(r);
    i--;
   }
   if (index < currentIndex) {
    this.$router.replace(this.contextMenuTargetPageRoute);
   }
  },
  //删除页面
  delPageRoute(route) {
   let routeIndex = this.openedPageRouters.indexOf(route);
   if (routeIndex >= 0) {
    this.openedPageRouters.splice(routeIndex, 1);
   }
   this.delRouteCache(route.fullPath);
  },
  //删除页面缓存
  delRouteCache(key) {
   let cache = this.keepAliveComponentInstance.cache;
   let keys = this.keepAliveComponentInstance.keys;
   for (let i = 0; i < keys.length; i++) {
    if (keys[i] == key) {
     keys.splice(i, 1);
     if (cache[key] != null) {
      delete cache[key];
     }
     break;
    }
   }
  },
 },
};
</script>
<style lang="scss">
.__common-layout-pageTabs {
 .__contextmenu {
  // width: 100px;
  margin: 0;
  border: 1px solid #e4e7ed;
  background: #fff;
  z-index: 3000;
  position: absolute;
  list-style-type: none;
  padding: 5px 0;
  border-radius: 4px;
  font-size: 14px;
  color: #333;
  box-shadow: 1px 1px 3px 0 rgba(0, 0, 0, 0.1);
  li {
   margin: 0;
   padding: 0px 15px;
   &:hover {
    background: #f2f2f2;
    cursor: pointer;
   }
   button {
    color: #2c3e50;
   }
  }
 }

 $c-tab-border-color: #dcdfe6;
 position: relative;
 &::before {
  content: "";
  border-bottom: 1px solid $c-tab-border-color;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 100%;
 }
 .__tabs {
  display: flex;
  .__tab-item {
   white-space: nowrap;
   padding: 8px 6px 8px 18px;
   font-size: 12px;
   border: 1px solid $c-tab-border-color;
   border-left: none;
   border-bottom: 0px;
   line-height: 14px;
   cursor: pointer;
   transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
    padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
   &:first-child {
    border-left: 1px solid $c-tab-border-color;
    border-top-left-radius: 2px;
    margin-left: 10px;
   }
   &:last-child {
    border-top-right-radius: 2px;
    margin-right: 10px;
   }
   &:not(.__is-active):hover {
    color: #409eff;
    .el-icon-close {
     width: 12px;
     margin-right: 0px;
    }
   }
   &.__is-active {
    padding-right: 12px;
    border-bottom: 1px solid #fff;
    color: #409eff;
    .el-icon-close {
     width: 12px;
     margin-right: 0px;
     margin-left: 2px;
    }
   }
   .el-icon-close {
    width: 0px;
    height: 12px;
    overflow: hidden;
    border-radius: 50%;
    font-size: 12px;
    margin-right: 12px;
    transform-origin: 100% 50%;
    transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
    vertical-align: text-top;
    &:hover {
     background-color: #c0c4cc;
     color: #fff;
    }
   }
  }
 }
}
</style>

这个组件它需要两个属性,一个是keepAliveComponentInstance(keep-alive的控件实例对象),blankRouteName(空白路由的名称)

为什么我需要keep-alive的控件实例对象呢,因为这个对象里面有两个属性,一个是cache,一个是keys,存储着keep-alive的缓存的数据,有了这个对象,我就能在页签关闭时手动删除缓存。那这个对象怎么获取呢,如下所示,在keep-alive所在的父页面上的mounted事件上进行获取(如果keep-alive跟多页签组件不在同一个父页面,那可能就得借用vuex来传值了)

<template>
 <div id="app">
  <page-tabs :keep-alive-component-instance="keepAliveComponentInstance" />
  <div ref="keepAliveContainer">
   <keep-alive>
    <router-view :key="$route.fullPath" />
   </keep-alive>
  </div>
 </div>
</template>

<script>
import pageTabs from "./components/pageTabs.vue";
export default {
 name: "App",
 components: {
  pageTabs,
 },
 mounted() {
  if (this.$refs.keepAliveContainer) {
   this.keepAliveComponentInstance = this.$refs.keepAliveContainer.childNodes[0].__vue__;//获取keep-alive的控件实例对象
  }
 },
 data() {
  return {
   keepAliveComponentInstance: null,
  };
 }
};
</script>

而空白路由的名称,是干什么,主要我要实现刷新当前页面的功能,我们知道vue是不允许跳转到当前页面,那么我就想我先跳转到别的页面,再跳转回回来的页面,不就也实现刷新的效果了。(当然我用的是relpace,所以不会产生历史记录)

注:这个空白路由并不是固定定义在根路由上,需根据多页签组件所在位置,假如你有一个根router-view,还有一个布局组件,这个组件里面也有一个子router-view,多页签组件就在这个布局组件里,那么空白路由就需定义在布局组件对应的路由的children里面了

还有这个组件会根据路由对象的meta对象进行不同的配置,如下所示

let router = new Router({
 routes: [
  //这个是空白页面,重新加载当前页面会用到
  {
   name: "blank",
   path: "/blank",
  },
  {
   path: "/a",
   component: A,
   meta: {
    title: "A页面", //页面标题
    canMultipleOpen: true //支持根据参数不同多开不同页签,如果你需要/a跟/a?v=123都分别打开两个页签,请设置为true,否则就只会显示一个页签,后打开的会替换到前打开的页签
   }
  }
}

以上就是Vue实现多页签组件的详细内容,更多关于Vue实现多页签组件的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue2实现组件props双向绑定

    Vue学习笔记-3 前言 Vue 2.x相比较Vue 1.x而言,升级变化除了实现了Virtual-Dom以外,给使用者最大不适就是移除的组件的props的双向绑定功能. 以往在Vue1.x中利用props的twoWay和.sync绑定修饰符就可以实现props的双向绑定功能,但是在Vue2中彻底废弃了此功能,如果需要双向绑定需要自己来实现. Vue2的组件props通信方式 在Vue2中组件的props的数据流动改为了只能单向流动,即只能由组件外(调用组件方)通过组件的DOM属性attribu

  • vue 递归组件的简单使用示例

    前言 递归 相信很多同学已经不陌生了,算法中我们经常用递归来解决问题.比如经典的:从一个全为数字的数组中找出其中相加能等于目标数的组合.思路也不难,循环数组取值,不断递归相加,直到满足目标数条件.递归虽然能解决大部分,但弊处在于,很容易写出死循环的代码,导致爆栈.下面以我实际业务场景讲讲递归在vue组件中的应用. 在vue中使用 完成一个完整的递归,我个人认为最重要的有两点: 确定好进入递归的条件; 确定好跳出递归的条件; 其中最重要的就是确定 什么时候跳出递归.递归组件实际上非常简单,就是 A

  • Vue如何跨组件传递Slot的实现

    在开发过程中遇到这样一个问题,如何跨组件传递插槽.因为在开发类似树组件的过程中,插槽需要通过外部传递到树的根节点,然后通过根节点依次传递到各个叶子节点.那么如何把根节点的Slot如传递给子组件呢? 我们在开发过程中,希望可以这样实现重新定义叶子节点的结构: <data-tree> <template v-slot:node="data"> <div>{{data.title}} - {{data.text}}</div> </tem

  • Vue组件简易模拟实现购物车

    本文实例为大家分享了Vue组件模拟实现购物车的具体代码,供大家参考,具体内容如下 代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <ti

  • Vuejs第九篇之组件作用域及props数据传递实例详解

    本篇资料来于官方文档: http://cn.vuejs.org/guide/components.html#Props 本教程是小编结合官方文档整理的一套更加细致,代码更多更全的教程,特别适合新手阅读. props数据传递 ①组件实例的作用域: 是孤立的,简单的来说,组件和组件之间,即使有同名属性,值也不共享. <div id="app"> <add></add> <del></del> </div> <sc

  • vue之父子组件间通信实例讲解(props、$ref、$emit)

    组件是 vue.js 最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用.那么组件间如何通信,也就成为了vue中重点知识了.这篇文章将会通过props.$ref和 $emit 这几个知识点,来讲解如何实现父子组件间通信. 在说如何实现通信之前,我们先来建两个组件father.vue和child.vue作为示例的基础. //父组件 <template> <div> <h1>我是父组件!</h1> <child>

  • vue mounted组件的使用

    1.钩子函数 钩子函数是Windows消息处理机制的一部分,通过设置"钩子",应用程序可以在系统级对所有消息.事件进行过滤,访问在正常情况下无法访问的消息.钩子的本质是一段用以处理系统消息的程序,通过系统调用,把它挂入系统.(百度百科) 2.相对于前端来讲 对于前端来说,钩子函数就是指再所有函数执行前,我先执行了的函数,即 钩住 我感兴趣的函数,只要它执行,我就先执行. 3.vue中的mounted 在这发起后端请求,拿回数据,配合路由钩子做一些事情 类型:Function 详细: e

  • vue组件是如何解析及渲染的?

    前言 本文将对vue组件如何解析以及渲染做一个讲解. 我们可以通过Vue.component注册全局组件,之后可以在模板中进行使用 <div id="app"> <my-button></my-button> </div> <script> Vue.component("my-button", { template: "<button> 按钮组件</button>"

  • vue如何引用其他组件(css和js)

    1.vuejs组件之间的调用components 注意:报错Do not use built-in or reserved HTML elements as component id: 修改组件的名字,例如不能使用address为组件名字 组件名字不要使用内置的或保留HTML元素为组件id, App.vue是一个入口,vue必须注册才能使用 2.vue引入外部的css,放在和引入vue的位置一样 ./代表当前项目,../代表上一级项目 import '../static/style/reset.

  • Vue实现多页签组件

    直接看效果,增加了右键菜单,分别有重新加载.关闭左边.关闭右边.关闭其他功能. 也可以到我的github上看看代码(如果觉得这个组件有用的话,别忘了顺手给个小星星) 代码:https://github.com/Caijt/VuePageTab 演示:https://caijt.github.io/VuePageTab/ 我这个多页签组件里面的删除缓存的方法不是使用keep-alive组件自带的include.exculde结合的效果,而是使用暴力删除缓存的方法,这个在上个博客中也有提到,用这种方

  • vue实现tagsview多页签导航功能的示例代码

    目录 前言 一.效果图 二.实现思路 1. 新建 tags-view.js 2. 在Vuex里面引入 tags-view.js 3. 新建 tabsView 组件 4. 新建 ScrollPane 组件 5. 引入 tabsView 组件 6. 使用 keep-alive 组件,进行页签的缓存 总结 前言 基本上后台管理系统都需要有多页签的功能,但是因为一些脚手架项目基本都把这个功能给集成好了,导致在学习或者修改的时候不知道该如何下手.今天这篇文章就来聊一聊,vue-element-admin项

  • vue+iview的菜单与页签的联动方式

    vue+iview菜单与页签联动 最近在使用vue+iview开发一个后台管理类的系统,希望做一个点击左侧菜单右侧的页签与内容都能相对应的改变. 但搞了好久的路由也没有实现这个功能. 刚开始使用vue+iview不知道iview-admin可以直接拿来使用,布局之类的开箱即用,可是自己的demo已经写了好久不忍心放弃. 一.使用iview的menu和tab做布局,将这两个组件放到主页面 由于menu与tab的数据相同且样式需要进行关联,因此可以使用vuex进行状态管理,state中写入需要管理的

  • vue使用keep-alive如何实现多页签并支持强制刷新

    目录 使用keep-alive实现多页签并支持强制刷新 需求 思路 已打开菜单组件 Home页面 使用keep-alive以后刷新部分数据如何解决 项目中遇到得问题 使用keep-alive实现多页签并支持强制刷新 需求 我司有一款使用Vue构建的SPA 后台管理系统,此时客户提出一个需求. 1:要求支持打开多页签 2:多页签保留状态,即切换页签的时候不重新刷新内容. 3:关闭某一页签之后,重新打开之后,需要进行刷新 4:在当前页面可以进行强制刷新功能. 如图示意(左侧箭头为多页签,右侧为强制刷

  • Vue.js标签页组件使用方法详解

    本文实例为大家分享了Vue.js标签页组件使用的具体代码,供大家参考,具体内容如下 效果 入口页 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0&q

  • vue自定义翻页组件的方法

    本文实例为大家分享了vue自定义翻页组件的具体代码,供大家参考,具体内容如下 效果图如下: 1.在components建立page.vue文件 <template>     <div class="pagination">         <!-- 上 -->         <button :disabled="pageNo == 1" @click="getPageNo(pageNo - 1)">

  • 基于微前端qiankun的多页签缓存方案实践

    目录 一.多页签是什么? 1.1 单页面应用实现多页签 1.2 使用qiankun进行微前端改造后,多页签缓存有什么不同 二.方案选择 2.1 方案一:多个子应用同时存在 2.2 方案二:同一时间仅加载一个子应用,同时保存其他应用的状态 2.3 最终选择 三.具体实现 3.1 从组件级别的缓存到应用级别的缓存 3.2 移花接木——将vnode重新挂载到一个新实例上 3.3 解决应用级缓存方案的问题 3.3.1 vue-router相关问题 3.3.2 父子组件通信 3.3.3 缓存管理,防止内存

  • 基于Vue.js的表格分页组件

    一.Vue.js简介 1.Vue的主要特点: (1) 简洁 (2) 轻量 (3)快速 (4) 数据驱动 (5) 模块友好 (6) 组件化 (1) 简洁 下面看一段Angular的实现双向绑定的代码 // html <body ng-app="myApp"> <div ng-controller="myCtrl"> <p>{{ note }}</p> <input type="text" ng-

  • jQuery Easyui Tabs扩展根据自定义属性打开页签

    easyui是一个轻量级的后台管理系统框架,各种组件均有,使用简单方便,现在已经有免费版的License了. 1.增加扩展 <script type="text/javascript" > /** * @author {kexb} easyui-tab扩展根据id切换页面 */ $.extend($.fn.tabs.methods, { getTabById: function (jq, id) { var tabs = $.data(jq[0], 'tabs').tabs

  • Vue.js 的移动端组件库mint-ui实现无限滚动加载更多的方法

    通过多次爬坑,发现了这些监听滚动来加载更多的组件的共同点, 因为这些加载更多的方法是绑定在需要加载更多的内容的元素上的, 所以是进入页面则直接触发一次,当监听到滚动事件之后,继续加载更多, 所以对于无限滚动加载不需要写首次载入列表的函数, 代码如下: html: //父组件 <div v-infinite-scroll="loadMore" infinite-scroll-disabled="loading" infinite-scroll-distance=

随机推荐