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

以前都是用别人现成的多页签组件,自己也想尝试下做个Vue3的版本,目前还只有基本功能,慢慢完善。
从零开始Vu3+Element Plus后台管理系统(七)——手写一个简单的多页签组件

主要思路

  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/…

原文链接:https://juejin.cn/post/7229907502900461625 作者:抹茶san

(0)
上一篇 2023年5月7日 上午10:36
下一篇 2023年5月7日 上午10:46

相关推荐

发表回复

登录后才能评论