从零开始Vue3+Element Plus后台管理系统(七)——手写一个简单的多页签组件

news2024/11/30 14:43:51

以前都是用别人现成的多页签组件,自己也想尝试下做个Vue3的版本,目前还只有基本功能,慢慢完善。
image.png

主要思路

  1. 使用 Pinia 记录页签数据、处理操作
  2. 初始状态没有页签数据,使用默认路由数据填充
  3. 右击页签,显示更多关闭操作
  4. 使用el-scrollbar 实现横向滚动

store/tags 处理页签

页签的数据和操作都在store中,

  • list是页签数据
  • nameList保存页签路由的name,用于布局文件的keep-alive
<keep-alive :include="tags.nameList">
   <component :is="Component"></component>
</keep-alive>
  • 对页签的基本操作:增加页签、关闭、关闭其他、关闭全部

引入了持久化插件pinia-plugin-persistedstate,只要设置persist即可在页面刷新时保持页签数据不丢失,具体可以看专栏上篇文章《从零开始Vu3+Element Plus后台管理系统(六)——状态管理Pinia和持久化》

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface ListItem {
  name: string
  path: string
  title: string
}

export const useTagsStore = defineStore(
  'tags',
  () => {
    let list = ref<ListItem[]>([])

    let show = computed(() => {
      return list.value.length > 0
    })
    let nameList = computed(() => {
      return list.value.map((item: ListItem) => item.name)
    })

    function delTagsItem(index: number) {
      list.value.splice(index, 1)
    }
    function setTagsItem(data: ListItem) {
      list.value.push(data)
    }
    function clearTags() {
      list.value = []
    }
    function closeTagsOther(data: ListItem[]) {
      list.value = data
    }

    return { list, show, nameList, delTagsItem, setTagsItem, clearTags, closeTagsOther }
  },
  {
    persist: {
      storage: sessionStorage
    }
  }
)

页签组件页面

页签列表html

<template>
  <div class="shadow mo-tags backdrop-blur-sm bg-white/75 dark:bg-black/75" v-if="tags.show">
    <el-scrollbar>
      <ul v-click-outside="onClickOutside">
        <li
          v-for="(item, index) in tags.list"
          :key="item.path"
          :class="isActive(item.path) ? 'active' : ''"
        >
          <span
            class="cursor-pointer"
            @click="changeTab(item.path)"
            @contextmenu.prevent="openContext($event, index)"
            >{{ item.title }}</span
          >
          <i-ep-close @click="removeTag(item.path)"></i-ep-close>
        </li>
      </ul>
    </el-scrollbar>

    <div
      class="fixed flex flex-col px-4 py-2 text-xs leading-8 text-center bg-white rounded shadow-lg"
      :style="{ left: `${contextmenuPositon.left}px`, top: `${contextmenuPositon.top}px` }"
      v-show="contextmenuShow"
    >
      <div @click="closeOther">
        <el-button :icon="Close" link size="small">关闭其他页签</el-button>
      </div>
      <div class="cursor-default" @click="closeAll">
        <el-button :icon="Minus" link size="small">关闭所有页签</el-button>
      </div>
    </div>
  </div>
</template>

TS

<script setup lang="ts">
import { ref } from 'vue'
import { ClickOutside as vClickOutside } from 'element-plus'
import { useTagsStore } from '~/store/tags'
import { useSidebarStore } from '~/store/sidebar'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import { Close, Minus } from '@element-plus/icons-vue'

const route = useRoute()
const router = useRouter()
const tags = useTagsStore()
const siderbarStore = useSidebarStore()

const isActive = (path: string) => {
  return path === route.fullPath
}

function changeTab(e: string) {
  router.push(e)
}

let contextmenuShow = ref(false)
let contextmenuPositon = ref({ top: 0, left: 0 })
let currentIndex = ref(0)

function openContext(e: Event, index: number) {
  contextmenuShow.value = true
  currentIndex.value = index
  const { top, left } = getParentOffset(e.target)

  contextmenuPositon.value = {
    top: top - 38,
    left: left + e?.target?.clientWidth - (siderbarStore.collapse ? 64 : 200) - 84
  }
}

// 获取父元素的相对位移
function getParentOffset(el: any) {
  let offset = { top: 0, left: 0 }
  offset.top = el.offsetTop
  offset.left = el.offsetLeft

  if (el.offsetParent != null) {
    let offsetParent = getParentOffset(el.offsetParent)
    offset.top += offsetParent.top
    offset.left += offsetParent.left
  }
  return offset
}

const onClickOutside = () => {
  contextmenuShow.value = false
}

function removeTag(e: string) {
  const index = tags.list.findIndex((cur) => cur.path === e)
 
  tags.delTagsItem(index)
  const item = tags.list[index] ? tags.list[index] : tags.list[index - 1]
  if (item) {
    router.push(item.path)
  } else {
    router.push('/')
  }
}

// 设置标签
const setTags = (route: any) => {
  const isExist = tags.list.some((item) => {
    return item.path === route.fullPath
  })

  if (!isExist) {
    tags.setTagsItem({
      name: route.name,
      title: route.meta.title,
      path: route.fullPath
    })
  }
}
setTags(route)
onBeforeRouteUpdate((to) => {
  setTags(to)
})

// 关闭全部标签
const closeAll = () => {
  tags.clearTags()
  router.push('/')
  setTags(route)
}
// 关闭其他标签
const closeOther = () => {
  const curItem = tags.list.filter((item) => {
    return item.path === route.fullPath
  })
  tags.closeTagsOther(curItem)
}
</script>

v-click-outside

Element Plus自带的指令v-click-outside是个好东西,优雅解决了点击元素以外区域关闭元素的问题

<ul v-click-outside="onClickOutside">

const onClickOutside = () => {
  contextmenuShow.value = false
}

样式表

<style lang="scss">
.mo-tags {
  position: fixed;
  top: 60px;
  z-index: 1001;
  left: 200px;
  right: 0;
  height: 30px;
  transition: left 0.3s ease-in-out, width 0.3s ease-in-out;

  &.tag-collapse {
    left: 64px;
  }

  ul {
    display: flex;

    li {
      display: flex;
      align-items: center;
      flex-shrink: 0;
      padding-right: 4px;
      height: 24px;
      margin-top: 3px;
      font-size: 12px;

      margin-right: 2px;
      border: 1px solid var(--el-border-color);
      background: var(--el-fill-color-blank);
      border-radius: 2px;

      > span {
        padding: 0 4px 0 8px;
      }

      &.active {
        color: var(--el-color-primary);
      }

      &:hover {
        background-color: var(--el-bg-color-page);
      }
    }
  }
}
</style>

写完之后觉得页签并不是很复杂,但是也在好几个地方卡住了

  1. 页签太多怎么办?限制页签显示数量,还是让它们滚起来,选择了使用el-scrollbar让它们横向滚动,但是体验感一般。 还有一个缺陷就是——滚动之后再打开新页面或者滚出去的页签,未自动滚回来。
  2. 关闭弹层的位置,一开始取的是鼠标点击的位置,但是这样显示就不是很整齐,所以改了半天找到了元素的位置来定位。偶然发现VSCODE右击文件也是跟随鼠标位置出现浮层(以前真没注意过),现在犹豫要不要改回来
  3. 点击元素之外区域隐藏元素,这是个老问题,以前一直给window增加事件监听来关闭,后来在我研究Element Plus的popover组件时,发现了v-click-outside,很好用! 本来打算用popover做这个关闭的浮层,virtual-ref可以做出来脱离popover的效果,但是碰到了无法解决的问题,作罢,自己写吧。

因为本项目引用了tailwindcss,代码中还有别的引入模块,所以需要看效果可能还需要把代码下载跑起来。聪明如你,应该改一改也可以自己跑起来😄

本项目GIT地址:github.com/lucidity99/…

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

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

相关文章

移动云与启明星辰联合发布移动云|星辰安全品牌

数字中国时代&#xff0c;企业数字化转型不断深化&#xff0c;云安全市场发展持续高增&#xff0c;其安全更需自主可控、全程可信。基于此&#xff0c;移动云和启明星辰共同打造移动云|星辰安全品牌&#xff0c;聚力协行共筑安全云的压舱石&#xff0c;携手共塑中国云安全产业发…

原神服务器服务端多人联机教程

原神服务器服务端多人联机教程 大家好&#xff0c;我是艾西在上一篇文章中我们说了win系统服务器怎么搭建原神服务端&#xff0c;在最后结尾时有带一嘴怎么改为多人联机但不是很详细。哪么这篇文章艾西会给小伙伴们说清楚原神服务端怎么改为多人联机&#xff0c;毕竟玩游戏肯定…

MySQL高级语句(一)

一、SQL高级语句 1、 SELECT 显示表格中一个或数个栏位的所有资料 语法&#xff1a;SELECT "字段" FROM "表名"; select * from test1; select name from test1; select name,sex from test1;2、DISTINCT 不显示重复的内容 语法&#xff1a;SELECT D…

pdf怎么转换成ppt?4种方法1分钟处理

​ pdf怎么转换成ppt&#xff1f;在日常的办公中&#xff0c;经常需要进行PDF文件格式的转换。例如&#xff0c;我们从互联网上下载的许多资料都是以PDF格式保存的。此外&#xff0c;在使用Microsoft Office时&#xff0c;有些用户需要将Word文档转换为PDF格式&#xff0…

MySQL的概念、编译安装,以及自动补全

一.数据库的基本概念 1、数据&#xff08;Data&#xff09; • 描述事物的符号记录 • 包括数字&#xff0c;文字&#xff0c;图形&#xff0c;图像&#xff0c;声音&#xff0c;档案记录等 • 以“记录”形式按统一的格式进行存储 2、表 • 将不同的记录组织在一起 • …

JavaWeb08(MVC应用02[家居商城]连接数据库)

目录 一.绑定分类 1.1 效果预览 1.2 代码实现 ①底层代码 ②前端代码 二.绑定所有商品 2.1 效果预览 2.2.代码实现 ①底层代码 ②前端代码 三.分类查询 3.1效果预览 3.2代码实现 ①底层代码 ②前端代码 四.模糊查询 4.1 效果预览 4.2代码实现 ①底层代码 ②前…

一图看懂 zipp 模块:ZipFile 的一些兼容子类和补充接口,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 zipp 模块&#xff1a;ZipFile 的一些兼容子类和补充接口&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;模块图&#x1f9ca;类关…

直观理解torch.gather函数(带图)

直观理解torch.gather函数 1. gather的作用 因为深度学习里面&#xff0c;像分类或者分割&#xff0c;有时候去进行loss计算或准确度计算的时候&#xff0c;需要挑选某个维度特定的值&#xff0c;所以有了这么个函数。注意不要高估这个函数的能力&#xff0c;这个函数只是在指…

大数据技术之Sqoop

第1章 Sqoop简介 Sqoop是一款开源的工具&#xff0c;主要用于在Hadoop(Hive)与传统的数据库(mysql、postgresql…)间进行数据的传递&#xff0c;可以将一个关系型数据库&#xff08;例如 &#xff1a; MySQL ,Oracle ,Postgres等&#xff09;中的数据导进到Hadoop的HDFS中&…

破案小说中的《人月神话》和女装

DDD领域驱动设计批评文集>> 《软件方法》强化自测题集>> 《软件方法》各章合集>> 在破案小说《谁是凶手》中&#xff0c;《人月神话》、《程序员修炼之道》以及女装作为素材出现了。 成功学&#xff08;鸡汤学&#xff09;书籍《用所有的存在与世界相会》…

如何制定一套有效的期货交易系统策略?

期货交易是一项全球性的金融交易&#xff0c;对于投资者来说&#xff0c;制定有效的期货交易系统策略是至关重要的。在制定期货交易策略时&#xff0c;需要考虑市场趋势、资产种类、交易成本、仓位控制等多个方面。 很多刚进入期货市场的朋友&#xff0c;甚至很多做了很久期货…

JS代码优化——逻辑判断

文章目录 JavaScript 语法篇嵌套层级优化多条件分支的优化处理使用数组新特性简化逻辑判断**多条件判断****判断数组中是否所有项都满足某条件****判断数组中是否有某一项满足条件** **函数默认值**使用默认参数使用解构与默认参数复杂数据解构 策略模式优化分支逻辑处理 JavaS…

Mars3d实现加载gif动图

官网有相关示例参考&#xff1a;功能示例(Vue版) | Mars3D三维可视化平台 | 火星科技 功能示例(Vue版) | Mars3D三维可视化平台 | 火星科技 方式1&#xff1a; // [终点]绘制台风当前位置gif点 const gifGraphic new mars3d.graphic.DivGraphic({ position: [endItem.lon, e…

【人力资源管理】第3集 免费开源ERP: Odoo 16 hr_holidays管理员工休假和缺勤 构建一体化企业人力资源管理

文章目录 前言一、管理员工休假二、批准或者拒绝休假申请三 、简单报表工具四 、使用功能1.管理休假申请2.报告 总结 前言 管理员工休假和缺勤。 一、管理员工休假 跟踪所有员工的假期 跟踪每位员工的休假天数。员工输入请求&#xff0c;经理对请求进行审批和验证&#xff0…

MYSQL-数据库管理.3(用户管理及用户权限)

一、关系型数据库 数据结构 二维表格 库 -> 表 -> 列&#xff08;字段&#xff09;&#xff1a;用来描述对象的一个属性 行&#xff08;记录&#xff09;&#xff1a;用来描述一个对象的信息 二、非关系型数…

能成事的表达笔记

为什么需要好的沟通&#xff1f; 一.让自己舒服 二.让别人乐意 愿意听听得懂听完愿意配合你 共赢 沟通是思维和视角的改变 向上沟通 &#xff08;领导&#xff0c;客户&#xff09; 是最高效的职场成长路径 痛点&#xff1a; 出于恐惧而挖掘不到真实的需求 一味听从权威…

spring项目里的大事务优化

编程型事务更加灵活 声明式事务只需要加在方法头加Transactional注解即可开启事务&#xff0c;但是还是不太灵活&#xff0c;意味着整个方法所进行对数据库操作都要加进事务&#xff0c;当然一次查询也要进入事务&#xff0c;这并不是我们想要的&#xff0c;我们在update、ins…

电容笔和触控笔哪个好用?2023平价好用的电容笔测评

其实&#xff0c;许多产品各有特色&#xff0c;有的注重外观&#xff0c;而有的注重功能。ipad上的那支笔也是如此的。所以&#xff0c;购买电容笔的时候&#xff0c;必须对电容笔有充分的了解。在选购前&#xff0c;必须了解各种类型的电容笔&#xff0c;以决定选购何种电容笔…

ASEMI代理LTC3309AEV#TRMPBF原装ADI车规级LTC3309AEV#TRMPBF

编辑&#xff1a;ll ASEMI代理LTC3309AEV#TRMPBF原装ADI车规级LTC3309AEV#TRMPBF 型号&#xff1a;LTC3309AEV#TRMPBF 品牌&#xff1a;ADI /亚德诺 封装&#xff1a;LQFN-12 批号&#xff1a;2023 安装类型&#xff1a;表面贴装型 引脚数量&#xff1a;12 工作温度:-4…

数字演播厅全新上线,让您的业务展示事半功倍

向客户汇报时还在手忙脚乱找应用&#xff1f; 给领导汇报时还在尴尬的等待应用漫长的加载&#xff1f; 还在因为没有合适的控制设备而施展不开拳脚&#xff1f; 为帮助广大易知微用户提升演示汇报效果&#xff0c;易知微平台上线了「数字演播厅」功能&#xff0c;该功能专注…