在vue中實現tab標籤切換頁面功能

頁面效果



需求分析

在導航條上,默認打開一個頁面(列表頁面),當點擊列表項,打開一個詳情頁面,需添加到標籤導航中,同時顯示當前標籤的頁面內容,繼續點擊列表項,添加標籤到導航中,依次類推,效果如上圖。

具體功能:前端

  • 標籤可關閉;
  • 標籤可切換,同時內容切換;
  • 標籤大於可視範圍,需添加到選項卡列表中,可點擊切換;
  • 當點擊選項卡列表中的標籤,須要在可視區中與最後一個標籤進行切換;
  • 標籤的顯示個數自適應;


思考

在一些後臺系統中,見過相似的功能,是利用 iframe標籤添加連接的方式,實現這個功能。那麼在vue中怎麼去實現?vue

首先,這是一個單頁面網站應用,使用了vue-router進行前端路由,那麼咱們可使用 vue-router 的命名視圖,這種方式去代替iframe。
webpack

vue-router命名視圖官方代碼:git

<router-view class="view one"></router-view><router-view class="view two" name="a"></router-view><router-view class="view three" name="b"></router-view>複製代碼

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: Foo,
        a: Bar,
        b: Baz
      }
    }
  ]
})複製代碼

項目結構github


項目目錄大同小異,就不過多解釋了~~


路由文件-命名視圖

router代碼:web

import Vue from "vue";import Router from "vue-router";import store from '../store';Vue.use(Router);// layout頁面const Layout = () =>  import(/* webpackChunkName: "layout" */ "@/views/layout/Layout.vue");// 產品相關頁面const Product = () =>  import(/* webpackChunkName: "product" */ "@/views/product/Index.vue");const ProductDetail = () =>  import(/* webpackChunkName: "productDetail" */ "@/views/product/Detail.vue");// 新聞相關頁面const News = () =>  import(/* webpackChunkName: "news" */ "@/views/news/Index.vue");const NewsDetail = () =>  import(/* webpackChunkName: "newsDetail" */ "@/views/news/Detail.vue");const rotuer = new Router({  mode: "history",  base: process.env.BASE_URL,  routes: [    {      path: "/",      name: "layout",      component: Layout,      redirect: "/product",      children: [        {          path: "/product",          name: "product",          meta: {            title: "產品",            tabName: "產品列表"          },          components: {            default: Product,            detail: ProductDetail          }        },        {          path: "/news",          name: "news",          meta: {            title: "新聞",            tabName: "新聞列表"          },          components: {            default: News,            detail: NewsDetail          }        }      ]    }  ]});rotuer.afterEach(to => {  // 初始化navTag標籤  if (to.meta && to.meta.tabName) {    store.dispatch('navTab/addTab', {      tabName: to.meta.tabName,      initStatus: true    })  }});export default rotuer;複製代碼

根據上面的功能分析,路由使用了命名視圖的方式,每一個模塊的列表頁面有個name屬性,同時components定義了一個默認的頁面,和一個詳情頁面,若是還有其餘頁面能夠繼續添加到commontents中,如:

components: {
    default: Product,
    detail: ProductDetail,
    create: ProductCreate
}複製代碼


佈局文件

在layou.vue的文件中,對 navTabs(vuex中定義的標籤數組) 進行循環生成 <router-view :name=""></router-view> 標籤,這個 name 對應的是路由裏 components 屬性定義的key值,如:"detail"。

layou.vue的部分代碼:vue-router

<template>    <el-row class="container">    <el-col :span="24">      <Header @collapse="collapseHandler" :isCollapse="isCollapse"></Header>    </el-col>    <el-col :span="24" class="main">      <Menu :isCollapse="isCollapse"></Menu>      <el-main class="content-container">        <nav-tab :navTabs="navTabs"></nav-tab>        <div class="main-content">          <router-view v-if="!navTabs.length"></router-view>          <div class="navTab-content" v-for="item in navTabs" :key="item.tabId" v-show="item.active">            <keep-alive>              <router-view :name="item.vName"></router-view>            </keep-alive>          </div>        </div>      </el-main>    </el-col>  </el-row>    </template>複製代碼

主要使用<router-view :name="item.vName"></router-view>,顯示命名的組件。vuex


使用vuex

咱們來看看navTabs是什麼?將tab標籤數據放在了vuex的state裏,能夠全局對標籤集合繼續操做控制,後面會貼出完整代碼。

如下是對navTab.js中的部分代碼說明:數組

一、state定義bash

const tabIdStr = "tab_"; // 標籤的id前綴

const state = {
  tabs: [],
  tabMaxNum: 0
};複製代碼

tabIdStr變量是在新增標籤的時,防止與其餘命名衝突
state對象:
  • tabs屬性:保存標籤的數據
  • tabMaxNum:最大可顯示的標籤數量(導航可視區)

二、新增標籤

/**
   * 添加標籤
   * @param {Any} tabId tab的惟一id標識
   * @param {String} tabName tab的標題
   * @param {String} vName 對應router命名視圖的名稱
   * @param {Object} pParams 參數傳遞
   * @param {Boolean} initStatus 初始化狀態
   */
  addTab({ commit, state }, { tabId, tabName, vName, pParams, initStatus = false }) {
    // 設置標籤id
    let newTabId = tabIdStr + (tabId || 0); // 默認 0
    let opts = {
      tabName: "",
      vName: "default",
      pParams: {},
      active: true,
      ...{
        tabId: newTabId,
        tabName,
        vName,
        pParams
      }
    };

    // 初始化時,重置標籤
    if (initStatus) {
      commit("resetTabs");
    }

    // 判斷函數
    let hasTabId = item => {
      return item.tabId === newTabId;
    };
    // 判斷新增標籤是否已存在,若是存在直接激活,不然新增
    if (state.tabs.some(hasTabId)) {
      // 激活標籤
      commit("activeTab", newTabId);
      return false;
    }

    // 添加標籤
    commit("addTab", opts);
  },複製代碼

這裏代碼主要注意的是:

// 初始化時,重置標籤
if (initStatus) {
  commit("resetTabs");
}複製代碼

例如在產品模塊中,切換到新聞模塊時,需重置tabs的數據,能夠在router的afterEach方法中使用,路由部分代碼:

rotuer.afterEach(to => {
  // 初始化navTag標籤
  if (to.meta && to.meta.tabName) {
    store.dispatch('navTab/addTab', {
      tabName: to.meta.tabName,
      initStatus: true
    })
  }
});複製代碼

navTab.js完整代碼:

const tabIdStr = "tab_"; // 標籤的id前綴

const state = {
  tabs: [],
  tabMaxNum: 0
};

const getters = {
  getNavTabs: state => state.tabs
};

const actions = {
  /**
   * 添加標籤
   * @param {Any} tabId tab的惟一id標識
   * @param {String} tabName tab的標題
   * @param {String} vName 對應router命名視圖的名稱
   * @param {Object} pParams 參數傳遞
   * @param {Boolean} initStatus 初始化狀態
   */
  addTab({ commit, state }, { tabId, tabName, vName, pParams, initStatus = false }) {
    // 設置標籤id
    let newTabId = tabIdStr + (tabId || 0); // 默認 0
    let opts = {
      tabName: "",
      vName: "default",
      pParams: {},
      active: true,
      ...{
        tabId: newTabId,
        tabName,
        vName,
        pParams
      }
    };

    // 初始化時,重置標籤
    if (initStatus) {
      commit("resetTabs");
    }

    // 判斷函數
    let hasTabId = item => {
      return item.tabId === newTabId;
    };
    // 判斷新增標籤是否已存在,若是存在直接激活,不然新增
    if (state.tabs.some(hasTabId)) {
      // 激活標籤
      commit("activeTab", newTabId);
      return false;
    }

    // 添加標籤
    commit("addTab", opts);
  },
  /**
   * 切換標籤
   * @param {String} tabId tab的惟一id標識
   */
  changeTab({ commit }, { tabId }) {
    // 激活標籤
    commit('activeTab', tabId);
  },
  /**
   * 更多標籤處理
   * @param {Number} index tabs數組下標
   */
  handleMoreTab({ commit }, { index }) {
    commit('handleMoreTab', index);
  },
  /**
   * 刪除標籤
   * @param {Number} index tabs數組下標
   */
  deleteTab({ commit }, { index }) {
    commit('deleteTab', index);
  },
  /**
   * 刪除其餘標籤
   */
  deleteOtherTab({ commit, state }) {
    // 保存第一個標籤
    let firstTab = state.tabs[0];
    // 若是第一個當前標籤是第一個,則直接刪除所有
    if(firstTab.active) {
      commit('deleteAllTab');
    } else {
      commit('deleteOtherTab');
    }
  },
  /**
   * 刪除所有標籤
   */
  deleteAllTab({ commit }) {
    commit('deleteAllTab');
  }
};

const mutations = {
  /**
   * 添加標籤
   */
  addTab(state, opts) {
    // 隱藏其餘標籤狀態
    state.tabs.forEach(item => {
      item.active = false;
    });
    // 當tabs數量大於或等於標籤的最大顯示數,新添加的標籤放在可顯示的最後一位
    if(state.tabs.length >= state.tabMaxNum) {
      state.tabs.splice(state.tabMaxNum - 1, 0, opts);
    } else {
      state.tabs.push(opts);
    }
  },
  /**
   * 激活標籤
   */
  activeTab(state, tabId) {
    state.tabs.forEach(item => {
      item.active = false;
      if (item.tabId === tabId) {
        item.active = true;
      }
    });
  },
  /**
   * 更多標籤處理
   */
  handleMoreTab(state, index) {
    let tabs = state.tabs;
    let _index = state.tabMaxNum + index;
    // 激活點擊標籤
    tabs[_index].active = true;
    // 拷貝點擊標籤
    let copyTab = [tabs[_index]];
    // 刪除點擊標籤
    tabs.splice(_index, 1);
    // 隱藏其餘標籤
    tabs.forEach(item => {
      item.active = false;
    });
    // 插入到可顯示的標籤最後一個位置
    tabs.splice([state.tabMaxNum - 1], 0, ...copyTab);
  },
  /**
   * 刪除標籤
   */
  deleteTab(state, index) {
    let tabs = state.tabs;
    // 判斷刪除的是當前標籤,需激活上一個標籤
    if(tabs[index].active && tabs.length > 0) {
      tabs[index -1].active = true;
    }
    tabs.splice(index, 1);
  },
  /**
   * 刪除其餘標籤
   */
  deleteOtherTab(state) {
    // 解構第一個標籤,其餘標籤
    let [firstTab, ...otherTabs] = state.tabs;
    // 獲取當前標籤
    let curTab = otherTabs.filter(item => item.active);
    state.tabs = [firstTab, ...curTab];
  },
  /**
   * 刪除所有標籤
   */
  deleteAllTab(state) {
    let tabs = state.tabs;
    // 除了第一個標籤其餘的都刪除
    let firstTab = tabs[0];
    firstTab.active = true;
    state.tabs = [firstTab];
  },
  /**
   * 重置標籤
   */
  resetTabs(state) {
    state.tabs = [];
  },
  /**
   * 設置顯示標籤最大值
   */
  setMaxTabVal(state, val) {
    state.tabMaxNum = parseInt(val);
  },
};

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
};複製代碼

tps:navTab.js實現了標籤的新增、點擊切換、更多點擊切換、刪除等功能


NavTab組件

標籤的導航組件,對於標籤進行操做,使用navTab.js裏面的方法,對tabs數據進行增、刪、改、查~~

部分代碼:

<template>  <div class="nav-wrap">    <div class="nav-title">      <strong>{{$route.meta.title}}</strong>    </div>    <div class="nav-tabs" ref="tabsNav">      <div class="tabs-item" :class="{ 'acitve': item.active }" @click="handleClickTab(item)" v-for="(item, index) in navTabs.slice(0, this.tabMaxNum)" :key="item.tabId">        {{item.tabName}}        <i class="el-icon-close icon-close" @click.stop="handleCloseTab(index)" v-if="index"></i>      </div>      <div class="more">        <div class="more-btn" @click="handleClickMore">          <i class="icon el-icon-arrow-down"></i>        </div>        <ul class="more-dropdown-menu" v-show="moreStatus">          <li @click.stop="handleClickMoreTab(index)" v-for="(item, index) in navTabs.slice(this.tabMaxNum)" :key="item.tabId">            <span>{{item.tabName}}</span>            <i class="el-icon-close icon-close" @click.stop="handleCloseTab(item, index)"></i>          </li>          <li @click.stop="handleClickDelAll">            <span>關閉所有</span>          </li>          <li @click.stop="handleClickDelOther">            <span>關閉其餘</span>          </li>        </ul>      </div>    </div>  </div></template><script>import { mapGetters } from 'vuex';export default {  data() {    return {      tabMaxNum: 1,      moreStatus: false    }  },  computed: {    ...mapGetters({      navTabs: 'navTab/getNavTabs'    })  },  mounted() {    // 初始化    this.init();    window.addEventListener('resize', this.init, false);  },  deactivated() {    window.removeEventListener('resize', this.init, false);  },  methods: {    /**     * 初始化     */    init() {      // 計算標籤最大顯示個數      this.calcTabMaxNum();    },    /**     * 計算標籤最大顯示個數     */    calcTabMaxNum() {      if (!this.$refs.tabsNav) {        return false;      }      let tabsNav = this.$refs.tabsNav;      let tabsItem = tabsNav.querySelectorAll('.tabs-item');      let moreW = tabsNav.querySelector('.more').getBoundingClientRect().width;      let navW = tabsNav.getBoundingClientRect().width - moreW;      let itemW = tabsItem[0].getBoundingClientRect().width;      // 設置最大值      this.tabMaxNum = Math.floor(navW / itemW);      this.$store.commit('navTab/setMaxTabVal', this.tabMaxNum);    },    /**     * 點擊標籤     */    handleClickTab(item) {      let { tabId, acitve } = item;      if(acitve) return;      this.hideMore();      this.$store.dispatch('navTab/changeTab', { tabId });    },    /**     * 點擊更多     */    handleClickMore() {      this.moreStatus = !this.moreStatus;    },    /**     * 更多標籤點擊     */    handleClickMoreTab(index) {      this.hideMore();      this.$store.dispatch('navTab/handleMoreTab', { index });    },    /**     * 關閉標籤     */    handleCloseTab(index) {      this.$store.dispatch('navTab/deleteTab', { index });    },    /**     * 關閉所有     */    handleClickDelAll() {      if(this.navTabs.length === 1) return;      this.hideMore();      this.$store.dispatch('navTab/deleteAllTab');    },    /**     * 關閉其餘     */    handleClickDelOther() {      if(this.navTabs.length === 1) return;      this.hideMore();      this.$store.dispatch('navTab/deleteOtherTab');    },    /**     * 隱藏更多列表     */    hideMore() {      this.moreStatus = false;    }  }}</script>複製代碼

這裏初始化的時候進行了,最大可視標籤數量的計算。
calcTabMaxNum方法,根據標籤導航的寬度和標籤的寬度進行計算,獲得的值賦值給 state 裏的 tabMaxNum。


建立一個標籤頁面

handleRowClick(row) {
  let { id, title, intro } = row;
  this.$store.dispatch('navTab/addTab', {
    tabId: 'detail_' + id,
    tabName: title,
    vName: 'detail',
    pParams: {
      title,
      intro
    }
  })
}複製代碼

調用標籤的 addTab 方法。
  • tabId: 標籤的惟一Id標識
  • tabName: 標籤導航的title值
  • vName: 'detail' 就是指向路由裏面 components 定義的key值
  • pParams: 須要傳遞到其餘頁面的參數


怎麼獲取到傳遞過來的參數(pParams值)

在utils/index.js中定義的方法:

/**
 * 獲取當前標籤的傳遞參數
 * @param {Array} tabs 標籤數據
 */
export const getCurTabParams = (tabs) => {
    if(!tabs || !Array.isArray(tabs)) return {};
    // 查找當前標籤
    let curTab = tabs.filter(item => {
        return item.active;
    });
    return curTab.length > 0 ? curTab[0].pParams : {};
}複製代碼

Detail.vue中的部分代碼,getCurTabParams方法的使用:

computed: {
    ...mapGetters({
        navTabs: 'navTab/getNavTabs'
    }),
    tabParams() {
        return getCurTabParams(this.navTabs) ? getCurTabParams(this.navTabs) : {};
    }
},複製代碼

基本功能實現完成,到這裏就結束了,第一次寫分析,思路有點亂,若是有錯誤的地方歡迎指正~~

附上項目地址:github.com/GuJiBao/vue…

相關文章
相關標籤/搜索