Vue3 & TypeScript ---- tagsView
2023-05-03 本文已影响0人
牛会骑自行车
效果图: contextMenu tags较多时有滚动条
点:
- 数据(tags(标签列表)、activeTag(当前标签)、以及更改这俩数据的方法、关闭一个或多个标签的方法)都放在了pinia(仓库)里,因为很多东西需要和pinia中的permission联动并且在不同地方都会使用,全部放仓库里管理起来更加方便~
- activeTag的更新:初始时赋值一次;刷新页面时在Router.beforeEach中用to.name或其他路由唯一值与之相匹配;
- tags的数据来源与左侧经过筛选后的路由保持一致,所以从pinia的permission中获取;
- tags的滚动:写一个可以获取到滚动容器的scrollLeft及可以获取到tagView视图区宽度及每个tagItem的offsetWidth与offsetLeft的布局即可。尽量自己写!不要使用el-scrollbar!!除非你想效仿mall-admin-master~
- contextMenu(右键小菜单儿)
- 我选择将它设定在tagItem区域以鼠标落点为参照的固定定位。不要放在每一个tagItem(标签单项)中相对定位或者使用el-dropdown(组件库中下拉菜单)!!
- 根据tagItem的位置和是否为固定标签来判断contextMenu有哪几项操作;
-
需要一丢丢计算的是点击靠左或靠右tagItem时需要将它之前或之后的tag也全部露出来,代码中的方法为autoScroll
思考时的图图.jpg
不好意思。。昨晚因为工作的烦心事基本就没怎么睡觉。我知道这张图乱得一屁。。。。。不管你信不信哈哈哈哈我真的是通过画嚓这张图想明白的~
还有刚刚着重强调的两点!我使用了感叹号儿的地儿!!一定不要尝试哇。。因为之前用Vue2时没用element-ui,都是自己写的。这次就想说,要不用一下子人家的组件吧?结果我了个妈耶~(除非你打算借鉴mall-admin https://gitee.com/youlaiorg/mall-admin 那当我没说嘿嘿)
使用el-scrollbar没法子获取滚动容器(一长条儿的那个)的宽度;使用el-dropdown没法子获取tagItem的offsetLeft哈哈哈哈哈哈哈我谆的栓Q儿了我
代码:(还存有报错的地儿可能是没粘全仓库里的数据,你可以先自己写个假的试试~
TagView.vue 👇
<script lang="ts" setup>
import { RouteLocationNormalized } from 'vue-router';
import Router from '@/router'
import type { RouteType, TagType } from '@/store/types'
import useStore from '@/store'
import commonStyle from '@/assets/css/variables.module.scss'
const { permission, tag, app } = useStore();
// #region ---- tagsData
///////////////////////////////// fake data
// const tagList = ref<TagType[]>([]);
// for (let i = 0; i < 20; i++) {
// const item = {
// name: '哈哈哈' + i + '哈哈哈',
// meta: { title: i % 2 === 0 ? '哈哈哈 ' + (i + 1) + ' 哈哈哈' : '哈 ' + (i + 1), url: '哈哈哈' + i + '哈哈哈' }
// }
// tagList.value.push(item);
// }
const tags = computed(() => {
return tag.state.tags;
})
// ---- 右键菜单对应tag
const currentTag = ref<TagType>({
name: "",
meta: {}
});
// ---- 当前路由对应tag
const activeTag = computed(() => {
return tag.state.activeTag;
})
// #endregion
// #region ---- tagsFunction
const handleTag = (item: TagType, index: number) => {
// 路由跳转、设置当前高亮
Router.push(item.meta.url);
app.state.activeRoute = item.meta.url;
// 自动滚动
autoScroll(item, index);
}
// ---- 滚动
// #region ---- autoScroll
const { proxy } = getCurrentInstance() as any;
const refScrollWrap = ref();
const refScrollCon = ref();
const viewScrollLeft = ref(0);
const handleScroll = (e: any) => {
viewScrollLeft.value = e.target.scrollLeft;
}
const autoScroll = (item: TagType, index: number) => {
// 容器
const containerEl = refScrollWrap.value;
// 可视区域
const viewEl = refScrollWrap.value;
// 当前DOM
const currentEl = proxy.$refs[item.name][0];
// 前一个元素DOM = index > 0 ? 前一个元素 : 当前元素
const prevEl = index > 0 ? proxy.$refs[tags.value[index - 1].name][0] : currentEl;
// 后一个元素DOM = index < tag列表最后一个元素index - 1 ? 后一个元素 : 当前元素
const nextEl = index < tags.value.length - 1 ? proxy.$refs[tags.value[index + 1].name][0] : currentEl;
// 缝儿 = firstTag || lastTag ? 14 : 5; 此处俩值根据公共样式中的缝儿宽决定 (commonStyle中的sideTagSeam和tagItemSeam与后面style中的tag-item中的样式保持一致昂。。)
const seamWidth = index === 0 || index === tags.value.length - 1 ? parseInt(commonStyle.sideTagSeam) : parseInt(commonStyle.tagItemSeam);
if (viewScrollLeft.value > prevEl.offsetLeft + seamWidth) {
viewScrollLeft.value = prevEl.offsetLeft - seamWidth;
containerEl.scrollLeft = viewScrollLeft.value;
}
if ((nextEl.offsetLeft + nextEl.offsetWidth + seamWidth) > (viewScrollLeft.value + containerEl.offsetWidth)) {
viewScrollLeft.value = nextEl.offsetLeft + nextEl.offsetWidth + seamWidth - viewEl.offsetWidth;
containerEl.scrollLeft = viewScrollLeft.value;
}
}
// #endregion
// #region ---- contextMemu
const menu = reactive({
left: "0",
top: "0",
show: false,
})
// ---- 打开右键菜单
const handleContextMenu = ({ x, y }: any, item: TagType) => {
menu.left = x + 'px';
menu.top = y + 'px';
menu.show = true;
currentTag.value = item;
// 点击页面其他地方 菜单消失
window.addEventListener('click', menuDissapear);
window.addEventListener('contextmenu', menuDissapear);
}
// ---- 菜单消失
const menuDissapear = () => {
if (menu.show) {
menu.show = false;
window.removeEventListener('click', menuDissapear);
window.removeEventListener('contextmenu', menuDissapear);
}
}
// ---- 菜单功能 👇
// ---- 公共方法:当前Tag和当前路由是否匹配
const isMatch = (tag: TagType | RouteType | RouteLocationNormalized) => {
const currentRoute = Router.currentRoute.value;
return currentRoute.name === tag.name;
}
// 右击小菜单儿
// ---- 刷新
const handleRefresh = () => {
menuDissapear();
Router.push({
path: '/404',
query: {
redirect: currentTag.value.meta.url
}
})
}
// ---- 关闭
const closeTag = (item: RouteType | RouteLocationNormalized | TagType) => {
// 判断是否为当前项:是 - 路由跳转前一项;不是 - 只splice当前tag,路由无处理
const index = tag.state.tags.findIndex((i: any) => i.name === item.name);
if (isMatch(item)) {
const beforeTag = tag.state.tags[index - 1];
Router.push(beforeTag.meta.url);
}
tag.closeTag(item);
menuDissapear();
}
// ---- 关闭右侧
const showMenuItemCloseRight = computed(() => {
const currentIndex = tags.value.findIndex((item: TagType) => item.name === currentTag.value.name);
return currentIndex < tags.value.length - 1;
})
const handleCloseRight = () => {
// 当前Tag索引
const currentIndex = tags.value.findIndex((item: TagType) => item.name === currentTag.value.name);
tags.value.map((item: TagType, index: number) => {
if (index > currentIndex) tag.closeTag(item);
if (!isMatch(currentTag.value)) Router.push(currentTag.value.meta.url);
})
menuDissapear();
}
// ---- 关闭其他:tags超过一个!fixedTag时显示
const showMenuItemCloseOthers = computed(() => {
const notFixedList = tags.value.filter((item: TagType) => !item.meta.fixedTag);
return notFixedList.length > 1;
})
const handleCloseOthers = () => {
tags.value.map((item: TagType) => {
if (item.name === currentTag.value.name || item.meta.fixedTag) return;
tag.closeTag(item);
if (!isMatch(currentTag.value)) {
Router.push(currentTag.value.meta.url);
}
})
menuDissapear();
}
// ---- 关闭所有
const showMenuItemCloseAll = computed(() => {
const hasNotConstance = tags.value.findIndex((item: TagType) => !item.meta.fixedTag) !== -1;
return hasNotConstance;
})
const handleCloseAll = () => {
tag.closeAllTags();
Router.push(tags.value[0].meta.url);
}
// #endregion
// #region ---- 生命周期
onMounted(() => {
initTags();
})
// 初始化tagList数据
const initTags = () => {
// tags
const tags = permission.state.constanceSideBarRoutes.filter((item: any) => item.meta.fixedTag);
// currentTag
const activeTag = Router.currentRoute.value;
const isActiveTag = tags.findIndex((item: any) => item.name === activeTag.name) !== -1;
if (!isActiveTag) tags.push(Router.currentRoute.value);
tag.state.tags = tags;
tag.state.activeTag = activeTag;
}
// #endregion
</script>
<template>
<div class="tag-wrap" @scroll="handleScroll" ref="refScrollWrap">
<div class="tag-container" ref="refScrollCon">
<div v-for="(item, index) in tags" :key="item.name" class="tag-item" :class="{
fixed: item.meta.fixedTag,
active: activeTag.name === item.name
}" :style="{ color: item.color }" @mouseenter="item.color = commonStyle.$primary"
@mouseleave="item.color = '#222'" @click.native="handleTag(item, index)" :ref="item.name"
@contextmenu.stop.prevent="handleContextMenu($event, item)">
<span>{{ item.meta.title }}</span>
<span v-if="!item.meta.fixedTag" class="tag-close" @click.stop="closeTag(item)">×</span>
</div>
</div>
<div class="contextmenu" :style="{ left: menu.left, top: menu.top }" v-show="menu.show">
<div class="menu-item" @click="handleRefresh" @contextmenu.prevent="handleRefresh">刷新</div>
<div class="menu-item" v-if="!currentTag.meta.fixedTag" @click="closeTag(currentTag)"
@contextmenu.prevent="closeTag(currentTag)">关闭</div>
<div class="menu-item" v-if="showMenuItemCloseRight" @click="handleCloseRight"
@contextmenu.prevent="handleCloseRight">关闭右侧</div>
<div class="menu-item" v-if="showMenuItemCloseOthers" @click="handleCloseOthers"
@contextmenu.prevent="handleCloseOthers">关闭其他</div>
<div class="menu-item" v-if="showMenuItemCloseAll" @click="handleCloseAll"
@contextmenu.prevent="handleCloseAll">关闭所有</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.tag-wrap {
overflow-x: scroll;
border-bottom: 1px solid #d8dce5;
}
::-webkit-scrollbar {
height: 4px;
}
::-webkit-scrollbar-thumb {
background: $shadow_color;
}
.tag-container {
display: inline-block;
height: 36px;
padding-top: 8px;
user-select: none;
white-space: nowrap;
position: relative; // for take item's offsetLeft value
}
.contextmenu {
position: fixed;
left: 0;
top: 0;
padding: 4px 0;
border-radius: 4px;
background: #fff;
z-index: 4;
box-shadow: 0 1px 3px 0 $shadow_color, 0 0 3px 0 $shadow_color;
.menu-item {
padding: 4px 16px;
text-align: center;
font-size: 12px;
color: #222;
cursor: pointer;
&:hover {
background: $shadow_color;
}
}
}
.tag-item {
display: inline-block;
border: 1px solid $primary_border_color;
height: 26px;
line-height: 24px;
font-size: 12px;
color: #222;
padding: 0 24px 0 6px;
margin-right: $tagItemSeam;
border-radius: 4px;
position: relative;
cursor: pointer;
&:hover {
color: $primary;
}
&:nth-child(1) {
margin-left: 14px;
}
&:nth-last-child(1) {
margin-right: 14px;
}
.tag-close {
position: absolute;
top: 50%;
right: 6px;
transform: translateY(-50%);
cursor: pointer;
width: 15px;
height: 15px;
text-align: center;
line-height: 13px;
border-radius: 50%;
font-size: 15px;
&:hover {
color: $primary;
background: $shadow_color;
}
}
}
.tag-item.fixed {
padding-right: 6px;
}
.tag-item.active {
border-color: $primary;
background: $primary;
color: #fff !important;
.tag-close {
&:hover {
background: #fff;
}
}
}
</style>
pinia中的tags.ts代码 👇
import { defineStore } from 'pinia'
import type { RouteLocationNormalized } from 'vue-router'
import type { TagStateType, RouteType, TagType } from '../types'
import { defaultRoute } from '@/utils/dict'
const useTagsStore = defineStore('tag', () => {
const state = reactive<TagStateType>({
tags: [],
activeTag: defaultRoute,
})
function setActiveTag(tag: any) {
state.activeTag = tag;
}
function tagSetting(tag: RouteLocationNormalized) {
const already = state.tags.findIndex((item: any) => item.name === tag.name) !== -1;
// 列表中暂无该项 && 在左侧菜单中该项不隐藏 && 推
!already && !tag.meta.hidden && state.tags.push(tag);
setActiveTag(tag);
}
function closeTag(tag: RouteType | RouteLocationNormalized | TagType) {
const index = state.tags.findIndex((item: any) => item.name === tag.name);
state.tags.splice(index, 1);
}
function closeAllTags() {
state.tags = state.tags.filter((tag: TagType) => {
return !!tag.meta.fixedTag;
})
}
return {
state,
setActiveTag,
tagSetting,
closeTag,
closeAllTags,
}
})
export default useTagsStore;