vue3实现系统tab标签页面切换

功能:

  • 支持刷新当前、关闭其他、关闭全部、关闭当前
  • 支持打开多个相同path不同路由参数的页面,将fullPath作为路由页面唯一值

UI组件:

使用的是element-plus中的el-tab组件,结构目录如下

代码实现:

下面是 TagsView中的 index.vue

<template>
  <div class="m-tags-view" ref="containerDom">
    <div class="tags-view">
      <el-tabs
        v-model="activeTabsValue"
        @contextmenu.prevent.stop="openMenu($event)"
        type="card"
        @tab-click="tabClick"
        @tab-remove="removeTab"
      >
        <!-- && item.meta.affix -->
        <el-tab-pane
          v-for="item in routerTabList"
          :key="item.fullPath"
          :path="item.fullPath"
          :label="item.title"
          :name="item.fullPath"
          :closable="!(item.meta && routerTabList.length == 1)"
        >
          <template #label>
            {{ item.meta.title }}
          </template>
        </el-tab-pane>
      </el-tabs>
    </div>
    <div class="right-btn">
      <MoreButton ref="moreBtnRef" />
    </div>
  </div>
</template>
<script lang="ts" setup>
import { computed, watch, ref, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { TabsPaneContext } from "element-plus";
import MoreButton from "./components/MoreButton.vue";
import { useRouterTab } from "@/store/routeTab";
const route = useRoute();
const router = useRouter();
const routerTabList: any = computed(() => useRouterTab().routerTabList);

const addTags = () => {
  const { name } = route;
  if (name === "login") {
    return;
  }
  if (name) {
    useRouterTab().addView(route);
  }
  return false;
};
const moreBtnRef = ref();
const containerDom = ref();
const openMenu = (e: any) => {
  let tabFullPath;
  if (e.srcElement.id) {
    tabFullPath = e.srcElement.id.split("-")[1];
  }
  moreBtnRef.value.open(tabFullPath);
};
let affixTags = ref([]);
function filterAffixTags(routes: any, basePath = "/") {
  let tags: any = [];
  routes.forEach((route: any) => {
    if (route.meta && route.meta.affix) {
      tags.push({
        fullPath: basePath + route.fullPath,
        path: basePath + route.path,
        name: route.name,
        meta: { ...route.meta },
      });
    }
    if (route.children) {
      const tempTags = filterAffixTags(route.children, route.path);
      if (tempTags.length >= 1) {
        tags = [...tags, ...tempTags];
      }
    }
  });
  return tags;
}
const initTags = () => {
  let routesNew = routerTabList.value;
  let affixTag = (affixTags.value = filterAffixTags(routesNew));
  for (const tag of affixTag) {
    if (tag.name) {
      useRouterTab().addRouterList(tag);
    }
  }
};
onMounted(() => {
  initTags();
  addTags();
});
watch(route, () => {
  addTags();
});
const activeTabsValue = computed({
  get: () => {
    return useRouterTab().activeTabsValue;
  },
  set: (val) => {
    useRouterTab().setTabsMenuValue(val);
  },
});
const tabClick = (tabItem: TabsPaneContext) => {
  let path = tabItem.props.name as string;
  router.push(path);
};

const isActive = (path: any) => {
  return path === route.fullPath;
};
const removeTab = async (activeTabPath: string) => {
  await useRouterTab().delView(activeTabPath, isActive(activeTabPath));
};
</script>
<style lang="scss" scoped>
:deep(.el-tabs__item) {
  min-width: 100px !important;
}
.m-tags-view {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-left: 10px;
  padding-right: 10px;
  background: white;
  .right-btn {
    height: 100%;
    flex-shrink: 0;
  }
}
.tags-view {
  flex: 1;
  overflow: hidden;
  box-sizing: border-box;
}

.tags-view {
  .el-tabs--card :deep(.el-tabs__header) {
    box-sizing: border-box;
    height: 40px;
    padding: 0 10px;
    margin: 0;
  }
  :deep(.el-tabs) {
    .el-tabs__nav {
      border: none;
    }
    .el-tabs__header .el-tabs__item {
      border: none;
      color: #cccccc;
    }
    .el-tabs__header .el-tabs__item.is-active {
      color: blue;
      border-bottom: 2px solid blue;
    }
  }
}
</style>

右键显示功能菜单,写成了一个组件,在上面文件中进行引用

moreButton.vue

<template>
  <transition name="el-zoom-in-top">
    <ul v-show="visible" :style="getContextMenuStyle" class="contextmenu">
      <li @click="refresh">
        <el-icon :size="14"><Refresh /></el-icon> <span>刷新当页</span>
      </li>
      <li @click="closeCurrentTab">
        <el-icon :size="14"><FolderRemove /></el-icon> <span>关闭当前</span>
      </li>
      <li @click="closeOtherTab">
        <el-icon :size="14"><Close /></el-icon> <span>关闭其他</span>
      </li>
      <!-- <li @click="closeAllTab">
        <el-icon :size="14"><FolderDelete /></el-icon> <span>关闭所有</span>
      </li> -->
    </ul>
  </transition>
</template>
<script lang="ts" setup>
import { computed, type CSSProperties } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useRouterTab } from "@/store/routeTab";
const router = useRouter();
const route = useRoute();
const visitedViews: any = computed(() => useRouterTab().visitedViews);
const { x, y } = useMouse();
let visible = ref(false);
const left = ref(0);
const top = ref(0);
const currentPath = ref("");
const open = (fullPath: string) => {
  visible.value = true;
  currentPath.value = fullPath;
  left.value = x.value;
  top.value = y.value;
};
const closeMenu = () => {
  visible.value = false;
};

const getContextMenuStyle = computed((): CSSProperties => {
  return { left: left.value + 20 + "px", top: top.value + 10 + "px" };
});
watch(visible, (value) => {
  if (value) {
    document.body.addEventListener("click", closeMenu);
  } else {
    document.body.removeEventListener("click", closeMenu);
  }
});
defineExpose({ open });
// 关闭当前
const closeCurrentTab = (event: any) => {
  useRouterTab().toLastView(currentPath.value);
  useRouterTab().delView(currentPath.value);
};
// 关闭其他
const closeOtherTab = async () => {
  useRouterTab().delOtherViews(currentPath.value, route.fullPath);
};
// 刷新当前
const refresh = () => {
  useRouterTab().setReload(currentPath.value);
};
// 关闭所有 去模型管理
const closeAllTab = async () => {
  await useRouterTab().delAllViews();
  useRouterTab().goHome();
};
</script>
<style lang="scss" scoped>
.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;
    display: flex;
    align-items: center;
    span {
      padding-left: 4px;
    }
    &:hover {
      background: #eee;
      color: #4248f4 !important;
    }
  }
}
.more {
  background-color: blue;
  color: white;
  .tags-view-item {
    display: flex;
    align-items: center;
  }
}
</style>
@/store/tagsView

用到的store里面的ts

routeTab.ts

import router from "@/router";
import { defineStore } from "pinia";
import { useRequest } from "@/hooks/use-request";
import { messageError, messageConfirm } from "@/utils/element-utils/notification-common";
import { isBtnPermission } from "@/utils/permission";
import { ElMessageBox } from "element-plus";

export const useRouterTab = defineStore("routerList", {
  state: () => ({
    routerTabList: [] as any,
    isReload: true,
    activeTabsValue: "/",
  }),
  getters: {
    showTabList(state: any) {
      return state.routerTabList.filter((e: any) => !e.hidden);
    },
  },
  actions: {
    setTabsMenuValue(val: any) {
      this.activeTabsValue = val;
    },
    addView(view: any) {
      this.addRouterList(view);
    },
    addRouterList(view: any) {
      this.setTabsMenuValue(view.fullPath);
      if (this.routerTabList.some((v: any) => v.fullPath === view.fullPath)) return; // 不重复添加
      if (JSON.stringify(view.meta) != "{}") {
        this.routerTabList.push(Object.assign({}, view));
      }
    },
    // 删除当前,过滤掉本身的标签
    delView(activeTabPath: any, flag?: boolean) {
      return new Promise((resolve) => {
        this.routerTabList = this.routerTabList.filter((v: any) => {
          return v.fullPath !== activeTabPath || v.meta.affix;
        });
        this.toLastView(activeTabPath)
        resolve({
          routerTabList: [...this.routerTabList],
        });
      });
    },
    // 关闭标签后,跳转标签
    toLastView(activeTabPath: any) {
      const index = this.routerTabList.findIndex((item: any) => item.fullPath === activeTabPath);
      const nextTab: any = this.routerTabList[index + 1] || this.routerTabList[index - 1];
      if (!nextTab) return;
      router.push(nextTab.fullPath);
      this.addRouterList(nextTab);
    },
    // 刷新
    setReload(fullPath: any) {
      const index = this.routerTabList.findIndex((item: any) => item.fullPath === fullPath);
      this.routerTabList[index].code = Date.now(); //用于改变keep-alive中的key值实现刷新
    },
    clearVisitedView() {
      this.delAllViews();
    },
    // 删除全部
    delAllViews() {
      return new Promise((resolve) => {
        this.routerTabList = this.routerTabList.filter((v: any) => v.meta.affix);
        resolve([...this.routerTabList]);
      });
    },
    // 删除其他
    delOtherViews(fullPath: any, currentPath: any) {
      // affix是用来设置路由表中的meta,表示该路由是否是固定路由,固定则不删
      const op = () => {
        this.routerTabList = this.routerTabList.filter((item: any) => {
          return item.fullPath === fullPath || item.meta.affix;
        });
        router.push(fullPath);
      };
      this.isDelViews("other", op);
    },
    // 判断是否可以关闭全部和关闭其他
    isDelViews(type: string, callback: () => void) {
      if (useRouterTab().SaveRoutes?.length != 0) {
        messageConfirm(
          `存在未保存的模型建模,无法${type == "all" ? "全部关闭" : "关闭其他"},是否继续?`,
          {
            confirmButtonText: `${type == "all" ? "全部关闭" : "关闭其他"}`,
            cancelButtonText: `取消`,
          },
          () => {
            callback();
          }
        );
      } else {
        callback();
      }
    },
    goHome() {
      this.activeTabsValue = "/";
      router.push({ path: "/" }); //semantic/manage/model
    },
  },
  // persist: {
  //   enabled: true,
  //   strategies: [
  //     {
  //       key: "routerTabList1",
  //       storage: sessionStorage,
  //       paths: ["routerTabList"],
  //     },
  //   ],
  // },
});

注意:

由于本系统的keepAlive实现没有用页面的name,而是用v-if条件判断哪些页面缓存就套个keepalive的壳,否则正常展示,因此,上面刷新的逻辑原理,需要配合下面的key值设置

如果,你们是通过设置页面name,结合include实现的缓存,就像下面这种形式,那么可以用v-if实现刷新

刷新的逻辑就是,在store里面存一个isReload的变量。通过设置true false来实现刷新。

 // 刷新
    setReload() {
      this.isReload = false
      setTimeout(() => {
        this.isReload = true
      }, 50)
    },

有问题评论区留言或私信哦~

相关推荐
GISer_Jing3 分钟前
Vue前端进阶面试题目(二)
前端·vue.js·面试
乐闻x22 分钟前
Pinia 实战教程:构建高效的 Vue 3 状态管理系统
前端·javascript·vue.js
程序员学姐31 分钟前
基于SpringBoot+Vue的高校社团管理系统
java·开发语言·vue.js·spring boot·后端·mysql·spring
weixin_4314496836 分钟前
web组态软件
前端·物联网·低代码·编辑器·组态
橘子味小白菜41 分钟前
el-table的树形结构后端返回的id没有唯一键怎么办
前端·vue.js
前端Hardy1 小时前
HTML&CSS:比赛记分卡
前端·javascript·css·3d·html
疯狂的沙粒2 小时前
Vue项目开发 element-UI 前端实现 1到10排列选择的按钮
前端·vue.js·ui
刺客-Andy2 小时前
React第六节 组件属性prop的propTypes类型使用介绍
前端·javascript·react.js·typescript
Mr.Liu62 小时前
小程序24-滚动效果:scroll-view组件详解
前端·微信小程序·小程序
是萝卜干呀2 小时前
Frontend - 防止多次请求,避免重复请求
javascript·ajax·jquery·防抖·节流·disabled属性