實現Vue的多頁籤組件

在以前的博客中  關於vue的多頁面標籤功能,對於嵌套router-view緩存的最終無奈解決方法  有寫過vue的多頁籤功能的解決方案css

能夠看到我當時那個多頁籤的組件仍是比較簡單 的,只有打開跟關閉功能,後面有很多網友找我,能不能實現刷新當前頁,關閉其它頁面,關閉左邊頁面,關閉右邊頁面的功能。html

 

這幾天項目上線後有點時間,把這個多頁籤組件給完善一下。vue

直接看效果,增長了右鍵菜單,分別有從新加載、關閉左邊、關閉右邊、關閉其餘功能。git

 

也能夠到個人github上看看代碼(若是以爲這個組件有用的話,別忘了順手給個小星星)github

代碼:https://github.com/Caijt/VuePageTabvuex

演示:https://caijt.github.io/VuePageTab/element-ui

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

我這個多頁籤組件裏面的刪除緩存的方法不是使用keep-alive組件自帶的include、exculde結合的效果,而是使用暴力刪除緩存的方法,這個在上個博客中也有提到,用這種方法的話,能夠實現更完整的多頁籤功能,例如同個路由能夠根據參數的不一樣同時打開不一樣的頁籤,也能不用去寫那些路由的name值。app

先直接看組件代碼(裏面用了一些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,不然就只會顯示一個頁籤,後打開的會替換到前打開的頁籤
      }
    }
}
相關文章
相關標籤/搜索