vue3实现TabsView(包含鼠标滚动横向滚动条以及鼠标右击菜单)

TagsView.vue

<template>
	<div id="tags-view-container" class="tags-view-container">
		<div class="tags-view-wrapper scroll-pane" id="scroll">
			<router-link
				v-for="tag in visitedViews"
				:key="tag.path"
				:data-path="tag.path"
				:class="isActive(tag) ? 'active' : ''"
				:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
				class="tags-view-item"
				:style="activeStyle(tag)"
				@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
				@contextmenu.prevent="openMenu(tag, $event)"
			>
				{
   {
    tag.title }}
				<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
					<t-icon name="close" style="width: 1em; height: 1em; vertical-align: middle" />
				</span>
			</router-link>
		</div>

		<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
			<!-- <li @click="refreshSelectedTag(selectedTag)">刷新页面</li> -->
			<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭当前</li>
			<li @click="closeOthersTags(selectedTag)">关闭其他</li>
			<li @click="closeAllTags(selectedTag)">全部关闭</li>
		</ul>
	</div>
</template>

<script setup lang="ts">
import {
    usePermissionStore, useTagsViewStore } from '@/store';
import path from 'path';

import {
    color } from 'echarts';
defineOptions({
   
	name: 'TagsView',
});

/**
 * @desc: Types
 */

/**
 * @desc: Ref
 */

/**
 * @desc: Hooks
 */
const {
    proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();

/**
 * @desc: Data
 */
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({
   });
const affixTags = ref([]);
const tagsViewStore = useTagsViewStore();

/**
 * @desc: Watch
 */
watch(route, () => {
   
	addTags();
	moveToCurrentTag();
});

watch(visible, (value) => {
   
	if (value) {
   
		document.body.addEventListener('click', closeMenu);
	} else {
   
		document.body.removeEventListener('click', closeMenu);
	}
});

/**
 * @desc: Computed
 */
//  const key = computed(() => {
   
// 	return route.path;
// });
const visitedViews = computed(() => tagsViewStore.visitedViews);

const routes = computed(() => usePermissionStore().routes);

/**
 * @desc: 方法
 */
// 初始化与绑定监听事件方法
const scrollInit = () => {
   
	// 获取要绑定事件的元素
	const nav = document.getElementById('tags-view-container');
	const scrollDiv = document.getElementById('scroll');
	// document.addEventListener('DOMMouseScroll', handler, false)
	// 添加滚轮滚动监听事件,一般是用下面的方法,上面的是火狐的写法
	nav.addEventListener('mousewheel', handler, false);
	// 滚动事件的出来函数
	function handler(event) {
   
		// 获取滚动方向
		const detail = event.wheelDelta || event.detail;
		// 定义滚动方向,其实也可以在赋值的时候写
		const moveForwardStep = 1;
		const moveBackStep = -1;
		// 定义滚动距离
		let step = 0;
		// 判断滚动方向,这里的100可以改,代表滚动幅度,也就是说滚动幅度是自定义的
		if (detail < 0) {
   
			step = moveForwardStep * 100;
		} else {
   
			step = moveBackStep * 100;
		}
		// 对需要滚动的元素进行滚动操作
		scrollDiv.scrollLeft += step;
	}
};

function openMenu(tag, e) {
   
	const menuMinWidth = 105;
	const offsetLeft = proxy.$el.getBoundingClientRect().left; // container margin left
	const offsetWidth = proxy.$el.offsetWidth; // container width
	const maxLeft = offsetWidth - menuMinWidth; // left boundary
	const l = e.clientX - offsetLeft + 15; // 15: margin right

	if (l > maxLeft) {
   
		left.value = maxLeft;
	} else {
   
		left.value = l;
	}

	top.value = e.clientY - 64 - 8; // 64: header 8 margin
	visible.value = true;
	selectedTag.value = tag;
}

function closeMenu() {
   
	visible.value = false;
}

function isAffix(tag) {
   
	return tag.meta && tag.meta.affix;
}

function addTags() {
   
	const {
    name } = route;
	if (name) {
   
		tagsViewStore.addVisitedView(route);
	}
	return false;
}

function moveToCurrentTag() {
   
	nextTick(() => {
   
		for (const r of visitedViews.value) {
   
			if (r.path === route.path) {
   
				// scrollPaneRef.value.moveToTarget(r);
				// when query is different then update
				if (r.fullPath !== route.fullPath) {
   
					tagsViewStore.updateVisitedView(route);
				}
			}
		}
	});
}

function toLastView(visitedViews, view) {
   
	const latestView = visitedViews.slice(-1)[0];
	if (latestView) {
   
		router.push(latestView.fullPath);
	} else {
   
		// now the default is to redirect to the home page if there is no tags-view,
		// you can adjust it according to your needs.
		if (view.name === 'Dashboard') {
   
			// to reload home page
			router.replace({
    path: '/redirect' + view.fullPath });
		} else {
   
			router.push('/');
		}
	}
}

const closeSelectedTag = (view) => {
   
	tagsViewStore.delView(view).then(({
     visitedViews }) => {
   
		if (isActive(view)) {
   
			toLastView(visitedViews, view);
		}
	});
};

const closeOthersTags = (selectedTag) => {
   
	router.push(selectedTag);
	tagsViewStore.delOthersVisitedViews(selectedTag);
	moveToCurrentTag();
};

const closeAllTags = (selectedTag) => {
   
	tagsViewStore.delAllVisitedViews().then(({
     visitedViews }) => {
   
		if (affixTags.value.some((tag) => tag.path === view.path)) {
   
			return;
		}
		toLastView(visitedViews, selectedTag);
	});
};

// function refreshSelectedTag(selectedTag) {
   
// 	const { fullPath } = selectedTag;
// 	console.log(fullPath);

// 	// location.reload();
// 	router.replace({
   
// 		path: fullPath,
// 	});
// }

function isActive(r) {
   
	return r.path === route.path;
}

function activeStyle(tag) {
   
	if (!isActive(tag)) return {
   };
	return {
   
		'background-color': '#F2F3FF',
		color: '#194BFB',
	};
}

function filterAffixTags(routes, basePath = '/') {
   
	let tags: any = [];
	routes.forEach((route) => {
   
		if (route.meta && route.meta.affix) {
   
			// const tagPath = path.resolve(basePath, route.path);
			const tagPath = route.path;
			tags.push({
   
				fullPath: tagPath,
				path: tagPath,
				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;
}

function initTags() {
   
	const affixTags = filterAffixTags(routes.value);

	for (const tag of affixTags) {
   
		// Must have tag name
		if (tag.name) {
   
			// this.$store.dispatch('tagsView/addVisitedView', tag);
			useTagsViewStore().addVisitedView(tag);
		}
	}
}

/**
 * @desc: 生命周期
 */

onMounted(() => {
   
	initTags();
	addTags();
	scrollInit();
});
</script>

<style lang="scss" scoped>
.tags-view-container {
   
	position: relative;
	height: 48px;
	width: 100%;
	background: #fff;
	box-shadow:
		0 1px 3px 0 rgba(0, 0, 0, 0.12),
		0 0 3px 0 rgba(0, 0, 0, 0.04);
	// overflow-x: scroll;
	.tags-view-wrapper {
   
		.tags-view-item {
   
			border-radius: 4px;
			display: inline-block;
			position: relative;
			cursor: pointer;
			height: 32px;
			line-height: 32px;
			color: #000;
			background: #f3f3f3;
			padding: 0 12px;
			font-size: 12px;
			margin-left: 8px;
			margin-top: 8px;
			text-decoration: none !important;
			/* 超出滚动的关键,没有它元素会自动缩小,不会滚动 */
			flex-shrink: 0;
			&:first-of-type {
   
				margin-left: 15px;
			}
			&:last-of-type {
   
				margin-right: 15px;
			}
		}
		a {
   
			text-decoration: none;
		}

		.router-link-active {
   
			text-decoration: none;
		}
	}

	.scroll-pane {
   
		display: flex;
		/* 设置超出滚动 */
		overflow-x: auto;
	}

	::-webkit-scrollbar {
   
		/* 隐藏滚动条 */
		display: none;
	}

	.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;
			&:hover {
   
				background: #eee;
			}
		}
	}
}
</style>

pinna实现全局状态管理
@/store/modules/tagsView.ts

import {
    defineStore } from 'pinia';
import type {
    RouteRecordRaw } from 'vue-router';

export const useTagsViewStore = defineStore('tags-view', {
   
	state: () => ({
   
		visitedViews: [] as RouteRecordRaw[],
	}),

	actions: {
   
		addVisitedView(view: RouteRecordRaw) {
   
			console.log(this);
			if (this.visitedViews.some((v) => v.path === view.path)) return;
			this.visitedViews.push(
				Object.assign({
   }, view, {
   
					title: view.meta.title || 'no-name',
				}),
			);
		},

		delVisitedView(view: RouteRecordRaw) {
   
			for (const [i, v] of this.visitedViews.entries()) {
   
				if (v.path === view.path) {
   
					this.visitedViews.splice(i, 1);
					break;
				}
			}
		},

		delView(view: RouteRecordRaw) {
   
			return new Promise((resolve) => {
   
				this.delVisitedView(view);
				resolve({
   
					visitedViews: [...this.visitedViews],
				});
			});
		},

		delOthersVisitedViews(view: RouteRecordRaw) {
   
			this.visitedViews = this.visitedViews.filter((v) => {
   
				return v.meta.affix || v.path === view.path;
			});
		},

		delAllVisitedViews() {
   
			return new Promise((resolve) => {
   
				// keep affix tags
				const affixTags = this.visitedViews.filter((tag) => tag.meta.affix);
				this.visitedViews = affixTags;
				resolve({
   
					visitedViews: [...this.visitedViews],
				});
			});
		},

		updateVisitedView(view: RouteRecordRaw) {
   
			for (let v of this.visitedViews) {
   
				if (v.path === view.path) {
   
					v = Object.assign(v, view);
					break;
				}
			}
		},
	},
});

参考文章:链接

最近更新

  1. TCP协议是安全的吗?

    2023-12-06 01:06:34       17 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-06 01:06:34       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-06 01:06:34       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-06 01:06:34       18 阅读

热门阅读

  1. 第三方UI组件库的样式修改

    2023-12-06 01:06:34       43 阅读
  2. Diary14-Word样式设计

    2023-12-06 01:06:34       36 阅读
  3. 【mybatis <sql>,<include>标签】

    2023-12-06 01:06:34       34 阅读
  4. 音乐一拍到底多长

    2023-12-06 01:06:34       38 阅读
  5. 2023大厂高频面试题之Vue篇(3)

    2023-12-06 01:06:34       43 阅读
  6. SQL Server对象类型(7)——4.7.触发器(Trigger)

    2023-12-06 01:06:34       36 阅读
  7. vue el-cascader 省市区封装及使用

    2023-12-06 01:06:34       39 阅读
  8. Go函数和方法之间有什么区别

    2023-12-06 01:06:34       38 阅读