vue3 路由标签页右键菜单

1.创建下拉菜单组件

打开contextmenu.vue编辑

<template>
	<transition name="el-zoom-in-bottom">
		<div class="el-dropdown__popper el-popper is-light is-pure o-contextmenu"
			data-popper-placement="bottom"
			:style="`top: ${state.position.y}px;left: ${state.position.x}px;width: ${width}px;`"
			:key = "comKey"
			v-show="state.isShow"
			ref="contextmenuRef"
		>
			<ul class="el-dropdown-menu" 
			     :style="`width: ${width}px;`">
				<template v-for="(v, k) in state.dropdownList">
					<li class="el-dropdown-menu__item"
						:key="k"
						v-if="v.show(state.name)"
						@click="onItemClick(v)" >
						<span>{{ $t(v.txt) }}</span>
					</li>
				</template>
			</ul>
			<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
		</div>
	</transition>
</template>

<script setup lang="ts">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';

// 定义父组件传过来的值
const props = defineProps({
	list: {//菜单列表数据
		type: Array<Object>,
		default: () => {
			return [];
		},
	},
	handleCommand:{//点击操作项处理
		type: Function,
	},
	comKey:{
		type: String,
		default:"",
	},
	width: {//菜单宽度
		type: Number,
		default:80,
	},
});

// 定义变量内容
const state = reactive({
	isShow: false,
	dropdownList: props.list,
	position:{x:0,y:0},
	name: '',//右键点击标签路由名字
	arrowLeft: 30,
});


// 当前项菜单点击
const onItemClick = (item: Object) => {
	if(props.handleCommand){
		props.handleCommand(state.name, item);
	}
};
// 打开右键菜单
const openContextmenu = (item: Object) => {
	let width = props.cwidth*0.5;
	closeContextmenu();
	state.position.x = item.x;
	state.position.y = item.y;
	state.name = item.name;
	
	//小箭头位置调整
	state.arrowLeft = item.left>=  width ? width : item.left*0.5 -5;
	if(state.arrowLeft<=5){ state.arrowLeft = 5;}
	
	setTimeout(() => {
		state.isShow = true;
	}, 10);
};

// 关闭右键菜单
const closeContextmenu = () => {
	state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
	document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
	document.body.removeEventListener('click', closeContextmenu);
});



// 暴露变量
defineExpose({
	openContextmenu,
	closeContextmenu,
});
</script>

<style scoped lang="scss">
.o-contextmenu {
	transform-origin: center top;
	z-index: 3000;
	position: fixed;
	.el-dropdown-menu__item {
		font-size: 12px !important;
		white-space: nowrap;
		i {
			font-size: 12px !important;
		}
	}
}
</style>

2.tabs中调用contextmenu组件

<template>
  <el-tabs v-model="$route.name" 
		   type="card"   
		   @tab-remove="removeTab" 
		   @tab-click="handleClick" 
		   @contextmenu.prevent= "onContextmenu"
		   >
    <el-tab-pane
      v-for="item in tabs"
      :key="item.name"
      :label="item.title"
      :name="item.name"
	  :closable="item.name!==MENU_DEFAULT_NAME"
	
    >

    </el-tab-pane>
  </el-tabs>
  <Contextmenu  ref="contextmenuRef" 
			   comKey="tab-contextmenu"
			   :width="100"
               :list="dropdownList"  
			   :handleCommand="handleCommand"/>
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref, onBeforeMount, watch } from 'vue'
import { storeToRefs } from 'pinia';
import { useTabStore } from '@/stores/tab'
import {useRouter,useRoute} from 'vue-router'
import { MENU_DEFAULT_NAME } from '@/utils/constant'

// 引入组件
const Contextmenu = defineAsyncComponent(() => import('./contextmenu.vue'));

const router = useRouter()
const route = useRoute()

const useTab = useTabStore()
const { tabs } = storeToRefs(useTab)
const contextmenuRef = ref();

/**
 *
 * 下拉菜单列表数据
 *
**/
const dropdownList=[
	{ id: 0, txt: 'contextmenu.closeCurrent', show: (name)=>{
			return name != MENU_DEFAULT_NAME;
		}
	},
	{ id: 1, txt: 'contextmenu.closeOther', show: (name)=>{
			return useTab.hasOther(name);
		}
	},
	{ id: 2, txt: 'contextmenu.closeLeft', show: (name)=>{
			return useTab.hasLeft(name);
		}
	},
	{ id: 3, txt: 'contextmenu.closeRight', show: (name)=>{
			return useTab.hasRight(name);
		}
	},
	{ id: 4, txt: 'contextmenu.closeAll', show: (name)=>{
			return true;
		}
	},
];



/**
 * 
 * 下拉菜单项点击处理
 *
**/
const handleCommand = async (name: string, item: Object) => {
  let found ;
  let needRefreshRouter = false;
  switch (item.id) {
    case 0:
      // 关闭当前
	  found = useTab.remove(name);
	  needRefreshRouter = router.name == name;
      break;
    case 1:
      // 关闭其它
      found = useTab.removeOther(name)
	  needRefreshRouter = router.name != name;
      break;
    case 2:
      // 关闭左边
	  found = useTab.removeLeft(name)
	  needRefreshRouter = !useTab.has(router.name);
      break;
    case 3:
      // 关闭右边
      found = useTab.removeRight(name)
	  needRefreshRouter = !useTab.has(router.name);
      break;
    case 4:
		 // 关闭全部
	  found = useTab.removeAll();
	  needRefreshRouter = name != MENU_DEFAULT_NAME;
      break;
  }
  if(found){

	if(needRefreshRouter){
		router.push(found.path)
	}
  }
};

/**
 *
 * 右键下拉菜单
 *
**/
const onContextmenu = (e: PointerEvent) =>{
	if(!!e.target.id){
		const v= {name:e.target.id.replace('tab-',''), x:e.clientX, y: e.clientY, left:e.srcElement.clientWidth-e.offsetX};
		contextmenuRef.value.openContextmenu(v);
	}else{
		contextmenuRef.value.closeContextmenu();
	}
}


/**
 * 点击标签处理
 *
**/
const handleClick = (tab:any) => {
	const found	= router.getRoutes().find(v=>v.name== tab.props.name);
	//设置选中标签
	if(found){
		router.push(found.path)
	}
}

/**
 * 移除标签处理
 *
**/
const removeTab = (tab:string) => {
	contextmenuRef.value.closeContextmenu();
	const found = useTab.remove(tab);
	//关闭标签为当前路由时,需要重新设置选中标签
	if(route.name == tab){
		router.push(found.path)
	}
}

onBeforeMount(() => {
  useTab.change(route);
})

watch(
	()=>router.currentRoute.value,
	()=>{
		useTab.change(route);
	},
);

</script>

<style lang="scss" scoped>
.headbar-container {
  z-index: 10;
  height: var(--gl-headbar-height);
  background-color: var(--gl-headbar-background-color);
  box-shadow: var(--el-box-shadow-light);
}
</style>

3.store处理

import {defineStore} from 'pinia';
import { MENU_DEFAULT_NAME } from '@/utils/constant';
/**
 * 
 * @methods tab信息
 */
export const useTabStore = defineStore('tab',{
	persist: true,
  state: (): ArrayState<Meta> => ({
	datas: [],
  }),
  
  getters:{
    tabs(state){
      return this.datas;
    }
	  
  },
  actions: {
	/**
	 * 切换标签
	 * param route 
	 *
	**/
    change(route:any) {
		if (this.datas.every(item => item.name !== route.name)) {
		 this.datas.push(
			{
				id:route.id,
				name:route.name,
				path:route.path,
				title:route.meta.title,
			}
		 );
		}
    },
	/**
	 * 关闭标签
	 * param name 
	 *
	**/
	remove(name:string){
		const length = this.datas.length;
		const removeIndex = this.datas.findIndex(item=>item.name == name);
		this.datas.splice(removeIndex,1);
		return this.datas[removeIndex==length-1? removeIndex-1 : removeIndex];
	},
	/**
	 * 关闭其他标签
	 * param name 
	 *
	**/
	removeOther(name:string){
		this.datas = this.datas.filter(item=>item.name === name || item.name === MENU_DEFAULT_NAME);
		return this.datas.length==2 ? this.datas[1] : this.datas[0];
	},
	/**
	 * 关闭左边标签
	 * param name 
	 *
	**/
	removeLeft(name:string){
		const removeIndex = this.datas.findIndex(item=>item.name == name);
		this.datas.splice(1, removeIndex-1);
		return this.datas[1];
	},
	/**
	 * 关闭右边标签
	 * param name 
	 *
	**/
	removeRight(name:string){
		const removeIndex = this.datas.findIndex(item=>item.name == name);
		this.datas.splice(removeIndex+1);
		return this.datas[this.datas.length-1];
	},
	/**
	 * 关闭全部标签(保留首页)
	 *
	**/
	removeAll(){
		//保留一个
		this.datas.splice(1);
		return this.datas[0];
	},
	/**
	 * 
	 * 指定节点是否存在右边节点
     *
	**/
	hasRight(name: string){
		const index = this.datas.findIndex(item=>item.name == name);
		return index != this.datas.length-1;
	},
    /**
	 * 
	 * 指定节点是否存在左边节点(除了首页)
     *
	**/
	hasLeft(name: string){
		const index = this.datas.findIndex(item=>item.name == name);
		return index >1;
	},
    /**
	 * 
	 * 除了指定节点和首页 是否存在其他节点
     *
	**/
	hasOther(name: string){
		const index = this.datas.findIndex(item=>item.name == name);
		return (index==0 && this.datas.length>1) || (index>0 && this.datas.length>2);
	},
    /**
	 * 
	 * 是否存在指定节点
     *
	**/
	has(name: string){
		const index = this.datas.findIndex(item=>item.name == name);
		return index >-1;
	},
	clear(){
		this.datas = [];
	},

  },

  

});

4.效果

Vue3 中实现标签右键菜单可以通过以下步骤: 1. 创建一个标签组件,使用 `v-for` 循环渲染,为每个标签添加一个唯一的 `id` 值。 2. 在标签组件上监听 `contextmenu` 事件,阻止默认事件,触发自定义事件。 3. 在父组件中监听标签组件的自定义事件,获取当前点击的标签的 `id` 值。 4. 使用 `event.clientX` 和 `event.clientY` 获取鼠标右键点击的位置。 5. 在父组件中渲染一个弹出菜单组件,根据传入的标签 `id` 值和鼠标点击位置,确定弹出菜单的位置和内容。 下面是一个简单的示例代码: ```vue <template> <div> <div v-for="tab in tabs" :key="tab.id" @contextmenu.prevent="showContextMenu(tab.id)"> {{ tab.title }} </div> <ContextMenu v-if="showMenu" :x="menuX" :y="menuY" @close="hideContextMenu" /> </div> </template> <script> import ContextMenu from './ContextMenu.vue' export default { components: { ContextMenu }, data() { return { tabs: [ { id: 1, title: 'Tab 1' }, { id: 2, title: 'Tab 2' }, { id: 3, title: 'Tab 3' }, ], showMenu: false, menuX: 0, menuY: 0, activeTabId: null, } }, methods: { showContextMenu(id) { this.activeTabId = id this.menuX = event.clientX this.menuY = event.clientY this.showMenu = true }, hideContextMenu() { this.showMenu = false }, }, } </script> ``` 在上面的代码中,我们创建了一个标签组件,并在组件上监听了 `contextmenu` 事件,触发了 `showContextMenu` 方法。在父组件中,我们监听了标签组件的自定义事件,并获取了当前点击的标签的 `id` 值和鼠标点击位置。然后根据这些值,渲染了一个弹出菜单组件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Oscar_0208

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值