zoukankan      html  css  js  c++  java
  • 实现Vue的多页签组件

    在之前的博客中  关于vue的多页面标签功能,对于嵌套router-view缓存的最终无奈解决方法  有写过vue的多页签功能的解决方案

    可以看到我当时那个多页签的组件还是比较简单 的,只有打开跟关闭功能,后面有不少网友找我,能不能实现刷新当前页,关闭其它页面,关闭左边页面,关闭右边页面的功能。

    这几天项目上线后有点时间,把这个多页签组件给完善一下。

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

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

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

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

    演示:http://test.caijt.com:9001

    我这个多页签组件里面的删除缓存的方法不是使用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 ? '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,否则就只会显示一个页签,后打开的会替换到前打开的页签
          }
        }
    }
  • 相关阅读:
    [视频]K8飞刀--WinRAR远程代码执行漏洞利用视频
    [视频]K8飞刀 SQL注入点脱库演示教程
    [视频]K8飞刀 mysql注入点拿shell & UDF提权教程
    [视频]K8飞刀无代码编程之生成EXP
    [视频]K8飞刀 exploit管理功能 演示教程
    [视频]K8飞刀 Discuz! X 系列(1.5 2.5 3.x)产品升级漏洞GetShell教程
    [视频]K8飞刀 WordPress XSS添加管理员 & GetShell 教程
    [视频]K8飞刀 HackerIE自动检测网站注入教程
    [视频]K8飞刀 shellcode loader演示教程
    [视频]K8飞刀 ms15022 office漏洞演示动画
  • 原文地址:https://www.cnblogs.com/caijt/p/14256911.html
Copyright © 2011-2022 走看看