vue3实现页签

效果
在这里插入图片描述

注意点

  • useStore涉及的部分是pina的缓存,需要改成自己的;userStore.tabStore是获取缓存里的页签,
  • userStore.$patch(state => {
    state.tabStore = tabStoreList.value
    }) 是存储改变的页签
  • 注意我的页签是根据路由path来判断的,不是根据fullPath;可能有的需求是要使用fullPath
<template><div><div>{{isAtLeft}} -{{isAtLeft ? '滚动条头在最左侧' : ""}}</div><div>{{isAtRight}} -{{isAtRight ? '滚动条尾在最右侧' : ""}}</div><div>{{tabsMenuIndex}}</div></div><div class="box-all flex-row" v-if="tabStoreList.length"><div class="btn_box" v-if="!_.isEmpty(tabStoreList) && hasScroll && !isAtLeft" @click="scrollLeft"><el-icon class="btn_icon" size="16"><DArrowLeft /></el-icon></div><div class="tab_box" ref="tab_box"><!-- <pre>{{tabStoreList}}</pre> --><!-- {{tabStoreList.length}} --><template v-for="(item,index) in tabStoreList" :key="index"><el-popover ref="popoverRef" :visible="showPopoverPath === item.path" placement="bottom" :width="'fit-content'"><template #reference><div :id="'_id_'+item.path" class="tab_item" :class="tabsMenuValue === item.path ? 'active_bgi' : ''"@click="clickItem(item,index)" @contextmenu.prevent="tabRightClick(item,index)">{{item.tabName}}<el-icon class="close_icon" size="16" style="position:relative;top:3px;"@click.stop="deleteTab(item,index)"><Close /></el-icon></div></template><div class="popover_content"><div class="popover_btn" @click="closeRight(1,item,index)">关闭全部</div><div class="popover_btn mt" @click="closeRight(2,item,index)">关闭其他</div><div class="popover_btn mt" @click="closeRight(3,item,index)">关闭左侧</div><div class="popover_btn mt" @click="closeRight(4,item,index)">关闭右侧</div></div></el-popover></template></div><div class="btn_box" v-if="!_.isEmpty(tabStoreList) && hasScroll && !isAtRight" @click="scrollRight"><el-icon class="btn_icon" size="16"><DArrowRight /></el-icon></div></div>
</template><script setup>
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
import useStore from "@/store/index.js"
import { useRouter, useRoute } from "vue-router"
import _ from "lodash"
import { Close, DArrowLeft, DArrowRight } from "@element-plus/icons-vue"const route = useRoute()
const router = useRouter()
const { userStore } = useStore()let tabsMenuValue = ref('') // 当前选择的tab标签页
let tabsMenuIndex = ref('') // 当前选择的tab标签页对应下标
let tabStoreList = ref(userStore.tabStore || []) // 标签页集合 取自缓存 同时会更新缓存数据// 点击切换
const clickItem = (item) => {router.push({ path: item.path })
}
// 删除
const deleteTab = (item, index) => {tabStoreList.value = tabStoreList.value.filter((it, i) => i !== index)userStore.$patch(state => {state.tabStore = tabStoreList.value})// 判断是否是点击删除当前高亮标签 高亮删除需跳新页面if (item.path === tabsMenuValue.value) {let nextIndex = index - 1if (nextIndex === -1) { // 说明删第一个tabif (tabStoreList.value.length === 0) { // 说明删了后面没有tab了// 跳转首页或不处理} else {let nextPath = tabStoreList.value[0].pathrouter.push({ path: nextPath })}} else {let nextPath = tabStoreList.value[nextIndex].pathrouter.push({ path: nextPath })}}
}let popoverRef = ref(null)
let showPopoverPath = ref(false)
// 标签右键事件
const tabRightClick = (item) => {// console.log(item, index);showPopoverPath.value = item.path
}
// 弹出框右键事件
const closeRight = (type, item, index) => {// console.log(type, item, index);// 全部 其他 左侧 右侧if (type === 1) {tabStoreList.value = []} else if (type === 2) {tabStoreList.value = tabStoreList.value.filter((it, i) => i === index)let nextPath = tabStoreList.value[0].pathrouter.push({ path: nextPath })} else if (type === 3) {if (tabsMenuIndex.value < index) {let nextPath = tabStoreList.value[index].pathrouter.push({ path: nextPath })}tabStoreList.value = tabStoreList.value.filter((it, i) => i >= index)} else if (type === 4) {if (tabsMenuIndex.value > index) {let nextPath = tabStoreList.value[index].pathrouter.push({ path: nextPath })}tabStoreList.value = tabStoreList.value.filter((it, i) => i <= index)}userStore.$patch(state => {state.tabStore = tabStoreList.value})
}const tab_box = ref(null) // 滚动条元素
const hasScroll = ref(false) // 是否存在滚动条
const isAtLeft = ref(true); // 滚动条头在最左侧
const isAtRight = ref(false); // 滚动条尾在最右侧// 左右控制滚动条移动
const scrollLeft = () => {tab_box.value.scrollLeft = tab_box.value.scrollLeft - 800
}
const scrollRight = () => {tab_box.value.scrollLeft = tab_box.value.scrollLeft + 800
}
// 处理滚动事件
const handleScroll = () => {if (tab_box.value) {// 判断是否滚动到最左侧isAtLeft.value = tab_box.value.scrollLeft === 0;if (hasScroll.value) {// 判断是否滚动到最右侧isAtRight.value = tab_box.value.scrollLeft + tab_box.value.clientWidth >= tab_box.value.scrollWidth;} else {isAtRight.value = false}}
};
// 处理鼠标滚轮事件
const handleWheel = (event) => {if (tab_box.value) {event.preventDefault(); // 防止默认的垂直滚动// 根据鼠标滚轮的方向调整 scrollLefttab_box.value.scrollLeft += event.deltaY; // deltaY 表示垂直滚动的距离handleScroll(); // 更新滚动位置}
};watch(route, val => {const { path, matched } = valif (!matched.length) returnconst flatPermissionList = userStore.flatPermissionListconsole.log('页签监听', path, val, matched,)console.log('权限全路径', flatPermissionList);let matchedTitle = matched[matched.length - 1]?.meta?.title || ''let tabName = flatPermissionList.find(it => it.permissionUrl === path)?.permissionName || matchedTitleconsole.log('tabName', tabName);// let keepAliveName = matched[matched.length - 1].name// console.log('keepAliveName', keepAliveName);// 现在是根据路由的path来查找tabStore中是否存在相同的path,如果存在则不添加,如果不存在则添加(可能要改成fullPath判断 解决编辑新增的特殊页)tabsMenuValue.value = pathlet hasTabName = userStore.tabStore.findIndex(it => it.path === path) != -1// console.log(userStore.tabStore.findIndex(it => it.path === path), hasTabName);if (!hasTabName) {userStore.$patch(state => {state.tabStore.push({// ...val,tabName: tabName,// keepAliveName: keepAliveName,path: val.path,// name: val.name,// params: val.params,// query: val.query,// hash: val.hash,// fullPath: val.fullPath,// meta: val.meta,// close: true, // 是否支持关闭})})}tabStoreList.value = userStore.tabStore// 找到当前路由下标tabsMenuIndex.value = tabStoreList.value.findIndex(it => it.path === path)if (tab_box.value) {// 手动滑动到当前路由下标console.log('手动滑动到当前路由下标', tabsMenuIndex.value * 180, hasScroll.value);tab_box.value.scrollLeft = tabsMenuIndex.value * 180}
}, { immediate: true, deep: true })watch(() => tabStoreList.value, () => {nextTick(() => {if (tab_box.value) {hasScroll.value = tab_box.value.scrollWidth > tab_box.value.clientWidth}// 当没有滚动条时候,一定是在最左侧if (!hasScroll.value) {isAtLeft.value = trueisAtRight.value = false}// // 同时页签变化时候 时刻计算滚动条位置// handleScroll()})
}, { immediate: true, deep: true })const handleClickOutside = () => {showPopoverPath.value = null
};// 在组件挂载时添加全局点击事件监听器
onMounted(() => {window.addEventListener('click', handleClickOutside);if (tab_box.value) {tab_box.value.addEventListener('scroll', handleScroll);tab_box.value.addEventListener('wheel', handleWheel); // 添加鼠标滚轮事件监听}
});// 在组件卸载时移除全局点击事件监听器
onBeforeUnmount(() => {window.removeEventListener('click', handleClickOutside);if (tab_box.value) {tab_box.value.removeEventListener('scroll', handleScroll);tab_box.value.removeEventListener('wheel', handleWheel); // 移除鼠标滚轮事件监听}
});
</script><style lang="scss" scoped>
.box-all {width: 100%;max-width: 100%;.btn_box {display: inline-block;width: 46px;// height: 38px;background: #f9f9f9;border-radius: 0px 0px 0px 0px;cursor: pointer;.btn_icon {position: relative;top: 11px;left: 12px;}.btn_icon:hover {color: #00706b;}}.btn_box:first-child {box-shadow: 15px 0px 17px 1px rgba(0, 0, 0, 0.1);z-index: 0;}.btn_box:last-child {box-shadow: -15px 0px 17px 1px rgba(0, 0, 0, 0.1);}
}
.tab_box {// padding: 0 30px; // 可以看到到头的空白width: 100%;max-width: 100%;// height: 40px !important;// min-height: 40px !important;// max-height: 40px !important;white-space: nowrap;overflow-x: auto;// overflow-y: none;background-color: transparent;// transition: transform 0.5s ease;transition: scroll-left 0.3s ease; /* 添加过渡效果 */scroll-behavior: smooth; /* 添加平滑滚动效果 */.tab_item {line-height: 39px;cursor: pointer;display: inline-block;width: 180px;text-align: center;background: #f6f6f6;border-radius: 0px 0px 0px 0px;border-right: 1px solid #edecec;.close_icon:hover {color: red;}}.tab_item:hover {color: #00706b;font-weight: 700;// background-color: #edf6f6;border-radius: 8px 8px 0 0;}.active_bgi {color: #00706b;font-weight: 700;background-color: #fff;border-radius: 8px 8px 0 0;}
}/* 滚动条整体样式 */
.tab_box::-webkit-scrollbar {width: 1px; /* 竖直滚动条宽度 */height: 1px; /* 水平滚动条高度 */
}
/* 滚动条滑块 */
.tab_box::-webkit-scrollbar-thumb {background: transparent; /* 设置滑块为完全透明 */border-radius: 4px; /* 圆角滑块 */
}
/* 滚动条滑块在悬停时的样式 */
.tab_box::-webkit-scrollbar-thumb:hover {background: transparent; /* 在悬停时略微可见 */
}/* 滚动条轨道(背景) */
.tab_box::-webkit-scrollbar-track {background: transparent; /* 设置轨道为完全透明 */
}
::-webkit-scrollbar-track-piece {background-color: #fff; /* 设置轨道为完全透明 不设置不生效 */
}.popover_content {text-align: center;.popover_btn {cursor: pointer;}.popover_btn:hover {color: #00706b;font-weight: 700;}
}
</style>

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

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

相关文章

dfs算法搜索(详细)

目录 算法简介&#xff1a; 枚举方式&#xff1a; 1.每一个数都有两种状态&#xff0c;也就是选或不选&#xff0c;时间复杂度也就是2^n&#xff0c;每一个数都有选和不选两种状态。 2.生成给定集合所有可能排列的方法&#xff0c;与之不同的是同样是1 2 3三个数字&#xff0…

【机器学习】解构概率,重构世界:贝叶斯定理与智能世界的暗语

文章目录 条件概率与贝叶斯定理&#xff1a;深入理解机器学习中的概率关系前言一、条件概率与贝叶斯定理1.1 条件概率的定义与公式1.1.1 条件概率的定义1.1.2 条件概率的实例讲解 1.2 条件概率的性质与法则1.2.1 链式法则1.2.2 全概率公式1.2.3 贝叶斯定理的推导 1.3 贝叶斯定理…

利用开源Stable Diffusion模型实现图像压缩比竞争方法用更低的比特率生成更逼真的图像

概述 论文地址&#xff1a;https://studios.disneyresearch.com/app/uploads/2024/09/Lossy-Image-Compression-with-Foundation-Diffusion-Models-Paper.pdf 迪士尼的研究部门正在提供一种新的图像压缩方法&#xff0c;利用开源Stable Diffusion V1.2 模型&#xff0c;以比竞…

【Flask+OpenAI】利用Flask+OpenAI Key实现GPT4-智能AI对话接口demo - 从0到1手把手全教程(附源码)

文章目录 前言环境准备安装必要的库 生成OpenAI API代码实现详解导入必要的模块创建Flask应用实例配置OpenAI API完整代码如下&#xff08;demo源码&#xff09;代码解析 利用Postman调用接口 了解更多AI内容结尾 前言 Flask作为一个轻量级的Python Web框架&#xff0c;凭借其…

SpringBoot【十三(实战篇)】集成在线接口文档Swagger2

一、前言&#x1f525; 环境说明&#xff1a;Windows10 Idea2021.3.2 Jdk1.8 SpringBoot 2.3.1.RELEASE 二、如何生成Swagger文档 上一期我们已经能正常访问swagger在线文档&#xff0c;但是文档空空如也&#xff0c;对不对&#xff0c;接下来我就教大家怎么把相关的接口都给…

Qt之自定义动态调控是否显示日志

创作灵感 最近在芯驰x9hp上开发仪表应用。由于需要仪表警告音&#xff0c;所以在该平台上折腾并且调试仪表声音的时候&#xff0c;无意间发现使用&#xff1a; export QT_DEBUG_PLUGINS1 可以打印更详细的调试信息。于是想着自己开发的应用也可以这样搞&#xff0c;这样更方便…

Linux网络 UDP socket

背景知识 我们知道&#xff0c; IP 地址用来标识互联网中唯一的一台主机&#xff0c; port 用来标识该主机上唯一的一个网络进程&#xff0c;IPPort 就能表示互联网中唯一的一个进程。所以通信的时候&#xff0c;本质是两个互联网进程代表人来进行通信&#xff0c;{srcIp&…

数据链路层(Java)(MAC与IP的区别)

以太网协议&#xff1a; "以太⽹" 不是⼀种具体的⽹络, ⽽是⼀种技术标准; 既包含了数据链路层的内容, 也包含了⼀些物理 层的内容. 例如: 规定了⽹络拓扑结构, 访问控制⽅式, 传输速率等; 例如以太⽹中的⽹线必须使⽤双绞线; 传输速率有10M, 100M, 1000M等; 以太…

Apache APISIX快速入门

本文将介绍Apache APISIX&#xff0c;这是一个开源API网关&#xff0c;可以处理速率限制选项&#xff0c;并且可以轻松地完全控制外部流量对内部后端API服务的访问。我们将看看是什么使它从其他网关服务中脱颖而出。我们还将详细讨论如何开始使用Apache APISIX网关。 在深入讨…

项目15:简易扫雷--- 《跟着小王学Python·新手》

项目15&#xff1a;简易扫雷 — 《跟着小王学Python新手》 《跟着小王学Python》 是一套精心设计的Python学习教程&#xff0c;适合各个层次的学习者。本教程从基础语法入手&#xff0c;逐步深入到高级应用&#xff0c;以实例驱动的方式&#xff0c;帮助学习者逐步掌握Python的…

HTML+CSS+Vue3的静态网页,免费开源,可当作作业使用

拿走请吱一声&#xff0c;点个关注吧&#xff0c;代码如下&#xff0c;网页有移动端适配 HTML <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width…

Python的3D可视化库【vedo】2-1 (plotter模块) 绘制器的使用

文章目录 1 相关用语及其关系2 Plotter类的基本使用3 Plotter类具体的初始化设置3.1 全部初始化参数3.2 使用不同的axes vedo是Python实现的一个用于辅助科学研究的3D可视化库。 vedo的plotter模块封装了绘制器类Plotter。 Plotter实例可以用于显示3D图形对象、控制渲染器行为、…

职业院校人工智能实验室解决方案

随着人工智能技术的迅猛发展&#xff0c;企事业单位对具备高素质技术应用能力的人才需求愈发迫切&#xff0c;目前人工智能已经逐步从感知理解阶段转变为生成创造阶段&#xff0c;可以为各行各业提供多维的智能化应用服务。2024年的《政府工作报告》中首次提出了“人工智能”行…

steel-browser - 专为AI应用构建的开源浏览器自动化 API

Steel是一个开源浏览器 API&#xff0c;可以轻松构建与 Web 交互的 AI 应用程序和代理。您无需从头开始构建自动化基础设施&#xff0c;而是可以专注于 AI 应用程序&#xff0c;而 Steel 会处理复杂性。 2300 Stars 99 Forks 4 Issues 5 贡献者 Apache-2.0 License TypeScript …

ElasticSearch - 使用 Composite Aggregation 实现桶的分页查询

文章目录 官方文档概述Composite Aggregation 概述示例&#xff1a;基本分页查询分页&#xff1a;获取下一页结果使用场景注意事项 官方文档 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html#_pagin…

基于Python+Sqlite3实现的搜索和推荐系统

基于Python实现的搜索和推荐系统 一、引言 伴随着科技的不断进步&#xff0c;互联网&#xff0c;万维网的不断发展。我们越来越热爱万维网&#xff0c;也欣赏他的发展方式。20世纪90年代初&#xff0c;万维网还只是一个将文档联系起来的简单网络。如今&#xff0c;他已经成为…

Oracle:VARCHAR2(100)与VARCHAR2(100 CHAR)的差异导致的报错

目录 >> 问题背景&#xff1a;>> 阴差阳错&#xff1a;>> 问题出现&#xff1a;>> 问题排查&#xff1a;>> 知识点&#xff1a;>> 问题复盘&#xff1a;>> 问题拓展&#xff1a; >> 问题背景&#xff1a; Oracle下&#xff1…

右玉200MW光伏电站项目 微气象、安全警卫、视频监控系统

一、项目名称 山西右玉200MW光伏电站项目 微气象、安全警卫、视频监控系统 二、项目背景&#xff1a; 山西右玉光伏发电项目位于右玉县境内&#xff0c;总装机容量为200MW&#xff0c;即太阳能电池阵列共由200个1MW多晶硅电池阵列子方阵组成&#xff0c;每个子方阵包含太阳能…

最短路----Dijkstra算法详解

简介 迪杰斯特拉&#xff08;Dijkstra&#xff09;算法是一种用于在加权图中找到单个源点到所有其他顶点的最短路径的算法。它是由荷兰计算机科学家艾兹格迪科斯彻&#xff08;Edsger Dijkstra&#xff09;在1956年提出的。Dijkstra算法适用于处理带有非负权重的图。迪杰斯特拉…

从零开始学docker(五)-可用的docker镜像

最近docker镜像都不能访问&#xff0c;目前亲测可用的docker镜像可用&#xff0c;并拉取mysql测试完成。 [缺点] docker search 查不到镜像的索引列表&#xff0c;只能手动查询索引目录&#xff08;解决方案在最后&#xff09;。 linux服务器vim打开镜像文件daemon.json vim /e…