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

news2024/9/28 17:26:03

功能:

  • 支持刷新当前、关闭其他、关闭全部、关闭当前
  • 支持打开多个相同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)
    },

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

 

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2066864.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MATLAB水果分级系统

课题介绍 现在商业行为中&#xff0c;在水果出厂前都需要进行质量检测&#xff0c;需要将不同等级的水果进行分级包装&#xff0c;以保证商业利益最大化。可是传统方法都是依靠人工进行检测&#xff0c;效率低下&#xff0c;主观成分大&#xff0c;并不能很好客观地评价出货质…

适用于 Windows 10/11 的 2 大文件恢复软件

我很遗憾我在 Windows 10 中删除了 PC 中的数据并再次移动了它们。当我检查时&#xff0c;什么都没有。是否有任何Windows数据恢复软件&#xff0c;或者是否可以想象&#xff1f;我都会看到任何援助的价值。 文档、图像、音频等数据文件可能会因不良或危险行为而丢失&#xff…

Ollama 部署大模型

由于每次调用 OpenAI 等大模型都会产生费用&#xff0c;这个成本问题可以在开发环节可以通过私有化部署 LLM 的方式来避免。 Ollama 简介 Ollama 是一个开源的大型语言模型服务工具&#xff0c;专注于在本地运行大型语言模型。用户可以通过简单的安装指令在本地运行开源大型语…

关于Br的bean

笔者高烧了5天没有更新&#xff0c;今天终于感到热了&#xff0c;来继续更新。 JSON to Dart使用生成模型&#xff0c;首先要继承Br BR点进去把重复的内容删掉 然后去List里rename一下就好了。 然后再去

TypeError: Cannot read properties of undefined (reading ‘ciphertext‘)

ciphertext 是密文的意思&#xff0c;可能是使用插件进行解密的时候&#xff0c;密文的内容是 null 空的&#xff0c;假如密文是 null 时我们可以把密文改成空字符串就好了 例如 使用了 CryptoJS 进行加解密&#xff0c;关于 CryptoJS 的介绍可以看这篇文章 【CryptoJS】使…

基于STM32开发的智能水族箱控制系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 系统初始化水温监测与调节水质监控与自动换水照明控制与状态指示Wi-Fi通信与远程控制应用场景 家庭水族箱的智能管理公共水族馆的水质监控常见问题及解决方案 常见问题解决方案结论 1. 引言 …

mp4转m4v怎么转?5种方法快速完成转换

在这个多媒体内容爆炸的时代&#xff0c;视频格式转换成为了我们日常生活中不可或缺的一部分。尤其是从MP4转换为M4V&#xff0c;这种转换不仅关乎视频播放的兼容性&#xff0c;还影响着视频质量。下面就来给大家分享5种高效转换方法&#xff0c;一起来看看吧。 方法一&#xf…

开学季数码好物分享!推荐适合学生党好用又实惠的平替电容笔!

​开学季总是伴随着满满的期待与新鲜感&#xff0c;好多小伙伴都会在这个时候规划自己的学习新篇章&#xff0c;寻找那些能够助力学习、提升效率的好帮手。在数字化时代&#xff0c;电容笔作为无纸化学习的重要工具之一&#xff0c;其重要性不言而喻。它不仅能让学习笔记更加便…

MinIO在Windows中部署,并注册服务

文章目录 一、下载二、安装1. 打开命令提示符或PowerShell(需用命令提示符窗口运行)&#xff1a;2. 切换到 D:\MinIO 目录&#xff1a; 使用 cd 命令导航到 D:\MinIO 目录3. 运行 minio.exe&#xff1a; 输入以下命令并按 Enter&#xff1a;.\minio.exe4. 退出命令行&#xff1…

⼆⼿⻋交易系统架构分析

二手车交易系统架构分析涉及多个层面&#xff0c;包括技术选型、系统模块、数据库设计、用户界面及安全性等。以下是对二手车交易系统架构的综合分析&#xff1a; 技术选型&#xff1a;系统通常采用B/S架构模式&#xff0c;前后端分离&#xff0c;前端使用微信小程序开发工具&…

⼆⼿⻋交易系统小程序功能分析

二手车交易系统小程序的功能分析主要聚焦于如何利用移动互联网技术提升用户体验和交易效率。以下是一些关键功能的分析&#xff1a; 用户注册与登录&#xff1a;提供用户注册和登录功能&#xff0c;确保用户信息安全&#xff0c;可能包括手机号验证、邮箱验证或第三方平台&…

ROG NUC 助力金猴 冲破天命!

ROG NUC -畅玩黑神话性能指南来了&#xff01; 黑神话悟空已经发布两天了&#xff0c;三百万大圣齐齐讨贼&#xff0c;感觉大头怪都快不够用了&#xff01;而ROG NUC作为目前最强的桌面性能独显主机&#xff0c;到底作为英特尔基于INTEL 4的7nm工艺开发的最新一代CPU酷睿Ultra…

【深度生成模型】Diffusion model-公式推导

&#xff08;前提&#xff1a;数学原理很多&#xff09; 第一件事&#xff0c;前向过程&#xff1a;不断往输入数据中加噪声&#xff0c;变成纯噪声 每一步加入的噪声是不一样的&#xff0c;希望加噪的过程不断越来越多&#xff0c;理解为噪声的权重越来越大。 任意时刻的xt的…

Python实用库大全:解锁编程无限可能

前言 Python&#xff0c;作为一门广泛应用于数据科学、机器学习、网络爬虫、自动化测试等多个领域的编程语言&#xff0c;其强大的功能离不开丰富多样的库支持。这些库不仅简化了复杂的编程任务&#xff0c;还极大地提高了开发效率。本文将为您介绍一些Python中的实用库&#x…

version `GLIBCXX_3.4.30‘ not found解决

报错信息&#xff1a; 解决方法&#xff1a; 检查是否存在&#xff1a; strings /usr/lib/x86_64-linux-gnu/libstdc.so.6 | grep GLIBCXX 结果如下&#xff1a;(我这里有3.4.30) 建立软连接 # 逐行进行 cd /home/su2204/miniconda3/envs/FaceVerse/bin/../lib mv libstd…

Linux - 模拟实现 shell 命令行解释器

目录 简介 shell 的重要性 解释为什么学习 shell 的工作原理很重要 模拟实现一个简单的 shell 循环过程 1. 获取命令行 2. 解析命令行 3. 建立一个子进程&#xff08;fork&#xff09; 4. 替换子进程&#xff08;execvp&#xff09; 5. 父进程等待子进程退出&#xff08;wai…

服务器数据总是被恶意删除,日常该如何做好安全防范?

随着互联网技术的飞速发展&#xff0c;服务器数据安全成为企业运营中不可忽视的重要环节。服务器数据频繁遭遇恶意删除&#xff0c;不仅影响业务连续性&#xff0c;还可能带来重大的经济损失和声誉损害。因此&#xff0c;采取有效措施加强服务器数据安全防范至关重要。以下是一…

从0到1构建视频汇聚生态:EasyCVR视频汇聚平台流媒体协议支持的前瞻性布局

TSINGSEE青犀EasyCVR视频汇聚平台是一款基于云-边-端一体化架构的视频融合AI智能分析平台&#xff0c;广泛应用于工地、仓储、工厂、社区、校园、楼宇等多个领域。平台凭借其强大的数据接入、处理、转码及分发能力&#xff0c;在视频监控领域展现出显著的技术优势和应用前景。本…

共享文件夹

右键要共享的文件夹&#xff0c;Sharing和Security都要设置&#xff0c;都要设置成Everyone可以读写 Full Control

活跃窃密木马TriStealer加密通信分析

1 概述 观成安全研究团队近期在现网监测到多起TriStealer窃密木马攻击事件&#xff0c;TriStealer窃密木马从2024年4月开始活跃&#xff0c;通过Bunny CDN进行载荷下发。TriStealer会收集系统信息、屏幕截图、浏览器中存储的账号密码以及设备中所有的“txt”后缀文件、桌面文件…