vue点击菜单实现多标签页tab,打开关闭多个页面(右击关闭功能)

1.页面显示效果
在这里插入图片描述
2.App.vue代码

<template>
  <div id="app">
    <router-view />
  </div>
</template>
<script>
export default {
  name: "",
  data() {
    return {};
  },
  created() {
    // 防止页面刷新,vuex里面的菜单数据消失
    if (sessionStorage.getItem("visitedMenus")) {
      this.$store.commit(
        "setVisitedMenus",
        JSON.parse(sessionStorage.getItem("visitedMenus"))
      );
      this.$store.commit(
        "setVisitedMenusName",
        JSON.parse(sessionStorage.getItem("menuNames"))
      );
    }

    window.addEventListener("beforeunload", () => {
      sessionStorage.setItem(
        "visitedMenus",
        JSON.stringify(this.$store.state.visitedMenus)
      );
      sessionStorage.setItem(
        "menuNames",
        JSON.stringify(this.$store.state.menuNames)
      );
    });
  },
  watch: {
    $route() {
      this.addRouteMenu();
    },
  },
  methods: {
    addRouteMenu() {
      const route = this.$route;
      this.$store.commit({
        type: "addRouteMenus",
        route,
      });
    },
  },
};
</script>

3.store关于vuex的js文件代码

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import storage from "@/api/storage.js"
import { changeMeta } from "@/router/common.js";
const store = new Vuex.Store({

    state: {
        visitedMenus: [],
        menuNames: []
    },
    mutations: {
        //退出登录状态时
        logout(state) {
            storage.removeStorage()
            state.visitedMenus = []
            state.menuNames = []
        },
        setVisitedMenus(state, value) {
            state.visitedMenus = value
        },
        setVisitedMenusName(state, value) {
            state.menuNames = value
        },
        addRouteMenus(state, routeMenu) {
            if (routeMenu.route.path != '/login' && routeMenu.route.path != '/register') {
                let hasMenu = state.visitedMenus.some(item => item.path == routeMenu.route.path)//判断此路由是否在已存储的路由数据中
                if (!hasMenu) {
                    // 如果存储的路由数据中,没有此路由就添加
                    state.visitedMenus.push(Object.assign({}, {
                        path: routeMenu.route.path,
                        titleText: routeMenu.route.meta.titleText,
                        query: routeMenu.route.query,
                    }))
                    state.menuNames.push(routeMenu.route.name)
                    changeMeta(routeMenu.route.path, "saveKeep"); //保持数据缓存应该在路由导航前设置,否则还会保持原来的数据。好像可以删除没啥用
                }
            }
        },
    },
    actions: {
        closeMenu({ commit, state }, routeMenu) {
            let deletekey;
            for (const [key, item] of state.visitedMenus.entries()) {
                if (item.path == routeMenu.page.path) {
                    deletekey = key;
                    state.visitedMenus.splice(key, 1);
                    state.menuNames.splice(key, 1);
                    break;
                }
            }
            return Promise.resolve({
                visitedMenus: state.visitedMenus,
                deletekey: deletekey,
                deletPath: routeMenu.page.path,
            })
        },
        closeAllMenu({ commit, state },) {
            // 删除所有,只保留第一个
            let firstPage = state.visitedMenus.slice(0, 1)
            state.visitedMenus.splice(1);
            state.menuNames.splice(1);
            return Promise.resolve(firstPage[0])
        },
        closeRightMenu({ commit, state }, page) {
            // 删除所有,只保留第一个
            let index = state.visitedMenus.findIndex(v => v.path == page.clikedMenu.path) + 1
            if (index > 0 && index < state.visitedMenus.length) {
                state.visitedMenus.splice(index);
                state.menuNames.splice(index);
            }
            return Promise.resolve({})
        }
    }

})
export default store

4.router文件下的common.js代码

import router from "@/router/index.js";
export const changeMeta = (nowPath, fromType) => {
    let firstModule = nowPath.split("/")[1];
    let routes = router.options.routes;
    let firstPathIndex = routes.findIndex((v) => v.path.indexOf(firstModule) != -1);
    // 获取路由里面的二级path
    let secondModule = nowPath.split("/")[2];//路由只能有一个/代表的内容,否则失效
    let secondPathIndex = routes[firstPathIndex].children.findIndex((v) => v.path == secondModule);
    if (fromType == 'cancelKeep') {
        // 点击左侧栏或者最顶部headr
        router.options.routes[firstPathIndex].children[secondPathIndex].meta.keepAlive = false
    } else if (fromType == 'saveKeep') {
        router.options.routes[firstPathIndex].children[secondPathIndex].meta.keepAlive = true
    }
}

5.router文件下的index.js是路由文件。部分代码示例如下

{
    // 系统管理模块
    path: '/system',
    name: 'S',
    component: SystemManagement,
    children: [{
        path: 'user',
        name: 'S01',
        component: User,
        meta: { titleText: '用户列表', keepAlive: true }
    }
    ]
}

6.顶部菜单代码。@/api/storage.js该文件主要是本地存存储的方法调用,可自行写

<template>
  <div>
    <div class="header">
      <el-menu
        :default-active="curr_menu"
        class="el-menu-demo"
        mode="horizontal"
        text-color="#8896C5"
        active-text-color="#1761fd"
        :router="true"
      >
        <el-menu-item index="/system">系统管理</el-menu-item>
      </el-menu>
      <div class="headerRight">
        <p class="userName">{{ userInfo.nickName }}</p>
        <el-avatar :size="50">{{ userInfo.username }}</el-avatar>
        <p class="logout" @click="logout">退出</p>
      </div>
    </div>
    <div>
      <div class="m-tab-container">
        <div>
          <router-link
            class="m-tab-item"
            :to="item"
            :key="item.path"
            :class="isActive(item) ? 'active' : ''"
            v-for="item in Array.from(visitedMenus)"
            @click.middle.native="closeSelectedMenu(item)"
            @contextmenu.prevent.native="openMenu(item, $event)"
          >
            {{ item.titleText }}
            <span
              class="m-icon-close el-icon-close"
              @click.prevent.stop="closeSelectedMenu(item)"
            ></span>
          </router-link>
        </div>
         <ul
          v-show="visible"
          :style="{ left: left + 'px', top: top + 'px' }"
          class="contextmenu"
        >
          <!-- <li @click="refreshSelectedTag(selectedMenu)">刷新</li> -->
          <li @click="closeSelectedMenu(selectedMenu)">关闭</li>
          <!-- <li @click="closeRightMenus()">关闭右边</li> -->
          <li @click="closeAllMenus(selectedMenu)">全部关闭</li>
        </ul>
      </div>
    </div>
  </div>
</template>
<script>
import storage from "@/api/storage.js";
import { changeMeta } from "@/router/common.js";
export default {
  name: "Header",
  data() {
    return {
      curr_menu: "",
      userInfo: {},
      visitedMenus: [],
      visible: false,
      top: 0,
      left: 0,
      selectedMenu: {},
    };
  },
  created() {
    this.visitedMenus = this.$store.state.visitedMenus;
    this.matchRoute();
    this.userInfo = storage.getUser();
  },
  watch: {
    $route() {
      this.matchRoute();
    },
    visible(value) {
      if (value) {
        document.body.addEventListener("click", this.closeMenu);
      } else {
        document.body.removeEventListener("click", this.closeMenu);
      }
    },
  },
  methods: {
    //把当前地址栏路由跟侧导航栏匹配
    matchRoute() {
      const path = this.$route.matched[0].path;
      switch (path) {
        case "/system":
          this.curr_menu = "/system";
          break;
      }
    },
    logout() {
      this.$store.commit("logout");
      this.$router.push("/login");
    },
    isActive(route) {
      // 判断当前地址栏地址,并激活样式
      return route.path == this.$route.path;
    },
    closeSelectedMenu(page) {
      if (this.visitedMenus.length <= 1) {
        this.$message.warning("请至少保留一个!");
        return false;
      }
      this.$store.dispatch({ type: "closeMenu", page }).then((pages) => {
        let deletekey = pages.deletekey;
        let visitedMenus = pages.visitedMenus;
        let deletPath = pages.deletPath;
        if (deletPath) {
          changeMeta(deletPath, "cancelKeep"); //取消数据缓存好像可以删除没啥用
        }
        if (this.isActive(page)) {
          // slice() 方法可从已有的数组中返回选定的元素。
          // slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
          // 注意: slice() 方法不会改变原始数组。
          let latestPage;
          if (visitedMenus.length != 0) {
            // 删除最后一个,默认前一个。不是最后一个,默认后一个
            if (deletekey == visitedMenus.length) {
              latestPage = visitedMenus.slice(-1)[0];
            } else {
              latestPage = visitedMenus.slice(deletekey, deletekey + 1)[0];
            }
            this.$router.push(latestPage);
          } else {
            // 已删除完,默认登陆成功后的页面。由于动态设置的接口返回导航数据,没有中文名称,不做限制
            this.$message.warning("请至少保留一个!");
          }
        }
      });
    },
    openMenu(tag, e) {
      const menuMinWidth = 320;
      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 + 230; // 15: margin right
      if (left > maxLeft) {
        this.left = maxLeft;
      } else {
        this.left = left;
      }
      this.top = e.clientY;
      this.visible = true;
      this.selectedMenu = tag;
    },
    refreshSelectedTag(page) {
      // this.$nextTick(() => {
      //   this.$router.replace({
      //     path: page.path,
      // path: '/redirect' + fullPath //?/redirect
      //   });
      // });
    },
    closeRightMenus(clikedMenu) {
      let selectedMenu = this.selectedMenu;
      this.$store
        .dispatch({ type: "closeRightMenu", clikedMenu })
        .then((page) => {
          // let deletekey = pages.deletekey;
          // let visitedMenus = pages.visitedMenus;
          // let deletPath = pages.deletPath;
          // if (deletPath) {
          //   changeMeta(deletPath, "cancelKeep"); //取消数据缓存好像可以删除没啥用
          // }
          let visitedMenus = this.$store.state.visitedMenus;
          let index = visitedMenus.findIndex(v.path == selectedMenu.path);
          if (index < 0) {
            this.$router.push(visitedMenus.slice(-1)[0]);
          } else {
          }
          this.visitedMenus = visitedMenus;
        });
    },
    closeAllMenus(view) {
      this.$store.dispatch({ type: "closeAllMenu" }).then((page) => {
        changeAllMeta(page.path, "cancelKeep"); //取消数据缓存好像可以删除没啥用
        this.visitedMenus = this.$store.state.visitedMenus;
        if (!this.isActive(page)) {
          this.$router.push(page);
        }
      });
    },
    closeMenu() {
      this.visible = false;
    },
  },
};
</script>

<style rel="stylesheet/scss" lang="scss">
.m-tab-container {
  background: #fff;
  height: 45px;
  border-top: 1px solid #d8dce5;
  border-bottom: 1px solid #d8dce5;
  overflow: hidden;
  white-space: nowrap;
  & > div {
    overflow: auto;
    overflow-y: hidden;
  }
  a.m-tab-item {
    text-decoration: none;
  }
  .m-tab-item {
    display: inline-block;
    position: relative;
    height: 32px;
    line-height: 32px;
    border: 1px solid #d8dce5;
    color: #495060;
    background: #fff;
    padding: 0 8px;
    font-size: 14px;
    margin-left: 5px;
    margin-top: 6px;
    &:first-of-type {
      margin-left: 18px;
    }
    &.active {
      background-color: #1c5ffd;
      color: #fff;
      border-color: #1c5ffd;
      &::before {
        content: "";
        background: #fff;
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        position: relative;
        margin-right: 2px;
      }
    }
    .m-icon-close {
      width: 20px;
      height: 20px;
      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 {
        display: inline-block;
        vertical-align: -3px;
      }
      &:hover {
        background-color: #b4bccc;
        color: #fff;
      }
    }
  }
    .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>

7.布局文件重要代码

<el-main>
          <keep-alive :include="menuNames">
            <router-view
              v-if="$route.meta.keepAlive"
              :key="$route.fullPath"
            ></router-view>
          </keep-alive>
          <router-view v-if="!$route.meta.keepAlive"></router-view>
        </el-main>
export default {
  name: "",
  components: {
  },
  data() {
    return { menuNames: this.$store.state.menuNames };
  },
  created() {},
  computed: {},
};
</script>

8.警告js路由文件中的name值要和vue文件中的name值一样

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js实现同一个页面多个页面标签(即多个 tab)的实现,可以使用 Vue.js 的路由功能和一个标签组件。 首先,在 Vue.js 中配置路由,将多个页面的路由地址和组件对应起来。例如,可以在 `router/index.js` 中定义如下路由: ```js import Vue from 'vue'; import Router from 'vue-router'; import Home from '@/views/Home'; import About from '@/views/About'; import Contact from '@/views/Contact'; Vue.use(Router); export default new Router({ routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About }, { path: '/contact', name: 'contact', component: Contact } ] }); ``` 接着,在需要显示多个页面标签的组件中,引入一个标签组件,并在其模板中使用路由链接和路由视图。例如,可以在 `views/Tabs.vue` 组件中使用 `vue-tabs-component` 插件实现多个标签: ```html <template> <div> <tabs> <tab v-for="tab in tabs" :key="tab.name" :label="tab.label">{{ tab.content }}</tab> </tabs> <router-view></router-view> </div> </template> <script> import { Tabs, Tab } from 'vue-tabs-component'; export default { components: { Tabs, Tab }, data() { return { tabs: [ { name: 'home', label: 'Home', content: 'Homepage content' }, { name: 'about', label: 'About', content: 'About page content' }, { name: 'contact', label: 'Contact', content: 'Contact page content' } ] }; } }; </script> ``` 在这个例子中,使用了 `vue-tabs-component` 插件,将 `tabs` 数组中的每一个对象转换为一个标签,每个标签都有一个 `name` 属性对应路由的名称,一个 `label` 属性对应标签的文本,以及一个 `content` 属性对应标签的内容。 最后,在需要显示标签的组件中,引入 `Tabs.vue` 组件,并在路由配置文件中将其对应到一个路由地址。例如,可以在 `App.vue` 组件中引入 `Tabs.vue` 组件,并将其对应到根路径: ```html <template> <div id="app"> <Tabs /> </div> </template> <script> import Tabs from '@/views/Tabs'; export default { components: { Tabs } }; </script> ``` ```js import Vue from 'vue'; import Router from 'vue-router'; import Tabs from '@/views/Tabs'; Vue.use(Router); export default new Router({ routes: [ { path: '/', name: 'tabs', component: Tabs, children: [ { path: '', redirect: '/home' }, { path: 'home', name: 'home', component: Home }, { path: 'about', name: 'about', component: About }, { path: 'contact', name: 'contact', component: Contact } ] } ] }); ``` 这样,就可以在同一个页面中显示多个页面标签,并切换不同的路由视图了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值