自定义vue通用左侧菜单组件(未完善版本)

使用到的技术:

vue3、pinia、view-ui-plus

拖拽功能参考技术:HTMLElement:drag 事件 – Web API 接口参考 | MDN

实现的功能:

传入一个菜单数组数据,自动生成一个左侧菜单栏。菜单栏可以添加、删除、展开、重命名,拖动插入位置等。

效果预览:

自定义vue通用左侧菜单组件(未完善版本)自定义vue通用左侧菜单组件(未完善版本)自定义vue通用左侧菜单组件(未完善版本)自定义vue通用左侧菜单组件(未完善版本)

代码:

c-menu-wrap.vue

	
		
			
				
			

			
				
			
		

		
			
		

		
			
  • 新建文件夹
  • 新建子文件夹
  • 新建文档
  • <!--
  • 上方新建模块
  • 下方新建模块
  • -->
  • 重命名
  • 删除
import { storeToRefs } from 'pinia' import { useMenuStore } from '@/stores/menu' import { defineProps, defineEmits, withDefaults, ref, nextTick, onMounted, onBeforeUnmount, provide } from 'vue' import { Modal, Message } from 'view-ui-plus' import cMenu from './c-menu.vue' const menuStore = useMenuStore() const { modal, modalX, modalY, menuList, isShowPopper, editItem } = storeToRefs(menuStore) const { expandAll, showPopper, hidePopper, createNew, openRename } = menuStore const emit = defineEmits(['doAction']) onMounted(() => { // document.addEventListener('click', hidePopper) }) onBeforeUnmount(() => { // document.removeEventListener('click', hidePopper) }) // 删除文档 function delDoc() { Modal.confirm({ title: '提示', content: '确定要删除该文档吗?', onOk: () => { }, onCancel: () => { Message.info('取消操作~'); } }); } .main-container { display: flex; flex-direction: column; height: 100%; .nav-top { flex-shrink: 0; box-sizing: border-box; padding: 0 24px; height: 48px; display: flex; justify-content: flex-end; align-items: center; font-size: 25px; // position: absolute; // top: 0; // left: 0; // width: 100%; border-bottom: 1px solid rgba(37, 55, 92, 0.10); i { cursor: pointer; margin-left: 15px; &:hover { color: #05F; } } :deep(.ivu-icon-ios-code) { font-size: 20px; transform: rotate(90deg); transform-origin: center; margin: 0; } } .nav-list { flex: auto; margin-top: 10px; overflow: auto; } .menu-modal { z-index: 10; position: fixed; top: 0; left: 0; &:after { z-index: 20; content: ''; position: fixed; top: 0; left: 0; right: 0; bottom: 0; } ul { z-index: 21; position: relative; background: white; border: 1px solid rgba(37, 55, 92, 0.10); li { cursor: pointer; padding: 10px 20px; display: flex; align-items: center; &:hover { i, span { color: #05F; } } i { font-size: 15px; } span { margin-left: 10px; font-size: 14px; } } } } }

c-menu.vue


	
		

			
				
				
			
				

			
				
					{{ item.val }}
					
				
				
				
				
			

			
				

			
	
		
	




	import { storeToRefs } from 'pinia'
	import { useMenuStore } from '@/stores/menu.ts'
	import { defineProps, defineEmits, withDefaults, ref, onMounted, inject } from 'vue';

	interface Props {
		list: Array,
		index?: number
	}

	const props = withDefaults(defineProps(), {
		list: [],
		index: 0
	})

	const menuStore = useMenuStore()
	const { 
		currentMenuId, 
		editItem,
		isEditRename,
		editInput,
		dragItem,
		dropPosition
	} = storeToRefs(menuStore)
	const { 
		doMenuAction, 
		editTitle, 
		showPopper,
		hidePopper
	} = menuStore

	
	const draggable = ref(false)


	onMounted(() => {
	})


	function changeDraggable(_draggable: boolean) {
		draggable.value = _draggable
		console.log(_draggable)
	}

	function drop($event: any, item: string) {
		$event.preventDefault()
		$event.stopPropagation()

		let data = $event.dataTransfer.getData("item");
	
		if(data) {
			let mitem = JSON.parse(data)
			console.log('拖动放置:', mitem)

			if(mitem.id == item.id) {
				console.log('同一个元素')
			} else {
				console.log('放置位置', dropPosition.value)
			}
		}

		$event.target.classList.remove('over')
		$event.target.classList.remove('over-top')
		$event.target.classList.remove('over-bottom')
		dragItem.value = null
		dropPosition.value = 0
	}

	// 拖动时触发
	function dragStart($event: any, item: any) {
		console.log("开始拖动:", item);

		$event.stopPropagation()

		$event.dataTransfer.setData(
			"item",
			JSON.stringify(item)
		)

		dragItem.value = JSON.parse(JSON.stringify(item))
	}

	function dragEnd($event: any) {
		$event.preventDefault()

		$event.stopPropagation()
		draggable.value = false
	}

	function dragOver($event: any) {
		$event.preventDefault()
		$event.stopPropagation()

		let t = $event.target
		let e = '.title-box'

		for (var i = t.matches || t.webkitMatchesSelector || t.mozMatchesSelector || t.msMatchesSelector; t && !i.call(t, e); ) {
			t = t.parentElement;
		}
		// 判断是否是同一个元素
		if(t.className.indexOf('title-box')!== -1 && t.getAttribute('node-id') != dragItem.value?.id) {
			t.classList.add('over')
			let dom = t.getBoundingClientRect()

			if($event.clientY  dom.bottom - 5) {
				// console.log('下')
				t.classList.add('over-bottom')
				t.classList.remove('over-top')
				dropPosition.value = 2
			} else {
				// console.log('中')
				t.classList.remove('over-bottom')
				t.classList.remove('over-top')
				dropPosition.value = 0
			}
		}
	}

	function dragEnter($event: any) {
		console.log('dragEnter')
		$event.preventDefault()
		$event.stopPropagation()
	}

	function dragLeave($event: any) {
		$event.stopPropagation();
		$event.target.classList.remove('over')
		$event.target.classList.remove('over-top')
		$event.target.classList.remove('over-bottom')
	}





	.menu-box {
		.li {
			cursor: pointer;
			

			.edit-title {
				position: relative;
				.edit-input {
					padding: 0 24px;
					:deep(.ivu-input) {
						padding-right: 25px;
					}
				}
				.icon {
					position: absolute;
					top: 50%;
					right: 30px;
					transform: translate(0, -50%);
					font-size: 18px;
					cursor: pointer;
					&:hover {
						color: #0055FF;
					}
				}
			}

			.title-box {
				position: relative;
				padding: 0 24px;
				border: 1px solid transparent;
				&.drag {
					background: rgb(203, 218, 245, 0.5);
				}

				&::before {
					background: transparent;
					content: "";
					height: 2px;
					top: 0;
					left: 0;
					position: absolute;
					width: 100%;
				}

				&::after {
					background: transparent;
					content: "";
					height: 2px;
					bottom: 0;
					left: 0;
					position: absolute;
					width: 100%;
				}

				&.over {
					border: 1px dashed #0055FF;
				}

				&.over-top {
					&::before {
						background: #0055FF;
					}
				}

				&.over-bottom {
					&::after {
						background: #0055FF;
					}
				}

				.md-reorder {
					display: none;
					position: absolute;
					left: 5px;
					top: 50%;
					transform: translate(0, -50%);
					font-size: 15px;
					cursor: move;

					&:hover {
						color: #0055FF;
					}
				}

				.md-more {
					display: none;
					position: absolute;
					right: 5px;
					top: 50%;
					transform: translate(0, -50%);	
					font-size: 15px;
				
					&:hover {
						color: #0055FF;
					}			
				}
				
				&>div {
					padding: 8px 12px;
					display: flex;
					align-items: center;
					justify-content: space-between;
					.txt {
						display: block;
						color:  #81838C;
						font-size: 14px;
						font-style: normal;
						font-weight: 500;
						overflow: hidden;
						text-overflow: ellipsis;
						white-space: nowrap;
					}
					.icon {
						width: 8px;
						height: 8px;
						background: url(../../assets/ic8_draw-down_normal.png) no-repeat;
						background-size: 100%;
						&.active {
							transform: rotate(180deg)
						}
					}
				}
				
				&:hover {
					&>div{
						.txt {
							color: #0055FF;
						}
					}
					.md-reorder, .md-more {
						display: block;
					}
				}
			}

			.sub-box {
				overflow: hidden;
			}
		

			&.active {
				.title-box {
					&>div {
						border-radius: 8px;
						background: #EBF2FF;
						.txt {
							color: #0055FF;
						}
						.icon {
							background-image: url(../../assets/ic8_draw-down_normal_hover.png);
							transition: all 0.5s;
							transform: rotate(180deg);
						}
					}
					
				}
				
			}
			
		}
	}

menu.ts

import { defineStore } from 'pinia'
import { ref, nextTick } from 'vue'
import { randomString } from '@/utils/index.js'

export const useMenuStore = defineStore('menu', () => {

	const menuList = ref([
		{
			id: 1,
			val: '标题1',
			type: 'folder'
		},
		{
			id: 2,
			val: '标题1',
			type: 'folder',
			children: [{
				id: 21,
				val: '标题2',
				type: 'folder',
				children: [
					{
						id: 211,
						val: '标题3',
						type: 'file',
						content: '123'
					}, 
					{
						id: 212,
						val: '标题3',
						type: 'file',
						content: '345'
					}
				]
			}]
		}
	])
	const preMenuList = ref([])

	const currentMenuId = ref('')
	const isShowPopper = ref(false)
	const modal = ref(null)
	const modalX = ref(0)
	const modalY = ref(0)

	const editItem = ref(null) // 当前标题编辑对象(文件夹或文件)
	const editInput = ref(null)
	const selectItem = ref(null) // 当前选中的文件对象
	const preSelectItem = ref(null)

	const isEdit = ref(false) // 文档是否开启编辑状态
	const isNew = ref(false) // 是否新建
	const isEditRename = ref(false) // 是否重命名

	const dragItem = ref(null) // 当前拖动元素
	const dropPosition = ref(0) // 拖动元素插入的位置:0 中,1 上,2 下

	// 点击菜单
	function doMenuAction(e:any, item: any) {

		hidePopper()
	
		if(item.type === 'folder') {
			item.showSub = !item.showSub
		} 
		// 选中的文档
		else if(item.type === 'file') {
			if(item.id !== currentMenuId.value) {
				isEdit.value = false
				currentMenuId.value = item.id.toString()
				selectItem.value = JSON.parse(JSON.stringify(item))
				preSelectItem.value = JSON.parse(JSON.stringify(selectItem.value))
			}
		}
	}

	// 显示更多菜单
	function showPopper(param: any) {
		hidePopper()

		isShowPopper.value = true

		let e = param.e
		let item = param.item

		editItem.value = item

		nextTick(() => {
			
			let _w = modal.value?.offsetWidth || 0
			let _h = modal.value?.offsetHeight || 0
			
			modalX.value = e.clientX + _w > window.innerWidth ? window.innerWidth - _w : e.clientX+2
			modalY.value = e.clientY + _h > window.innerHeight ? window.innerHeight - _h : e.clientY+2
			
		})
	}

	// 隐藏更多菜单
	function hidePopper() {
		console.log('隐藏更多菜单')
		

		if(isNew.value || (!isNew.value && isEditRename.value)) {	
			menuList.value = JSON.parse(JSON.stringify(preMenuList.value))
		}

		isNew.value = false
		isEditRename.value = false
		editItem.value = null
		isShowPopper.value = false
	}

	

	// 确定修改文档标题
	function editTitle() {
		// 新建
		if(isNew.value) {
			console.log('确定新建标题', editItem.value)
			if(editItem.value && editItem.value.type === 'file') {
				currentMenuId.value = editItem.value.id
				isEdit.value = true
				selectItem.value = JSON.parse(JSON.stringify(editItem.value))
				preSelectItem.value = JSON.parse(JSON.stringify(selectItem.value ))
			}
		} 
		// 修改
		else {
			console.log('确定修改标题', editItem.value)
			if(editItem.value && editItem.value.type === 'file' && currentMenuId.value === editItem.value.id) {
				
				selectItem.value = JSON.parse(JSON.stringify(editItem.value))
				preSelectItem.value = JSON.parse(JSON.stringify(selectItem.value ))
			}
		}

		isNew.value = false
		isEditRename.value = false
		editItem.value = null
	}

	function callBack(name: string) {
		console.log('callBack: ', name)
	}

	// 开启重命名
	function openRename() {
		preMenuList.value = JSON.parse(JSON.stringify(menuList.value))

		isShowPopper.value = false
		isEditRename.value = true

		nextTick(() => {
			editInput.value && editInput.value[0] && editInput.value[0].focus()
		})
	}

	// 全部展开
	function expandAll(_list: Array = []) {
	
		if(!_list || _list.length <= 0 || !(_list instanceof Array)) {
			_list = menuList.value
		}
		for(let i = 0; i  0) {
				_list[i].showSub = true
				expandAll(_list[i].children)
			}
		}
	}

	// 创建文件夹或文档
	function createNew(type: number) {
		isNew.value = true

		isShowPopper.value = false
		isEditRename.value = true

		preMenuList.value = JSON.parse(JSON.stringify(menuList.value))

		// 文件夹
		if(type === 1) {
			// 新建文件夹
			if(!editItem.value) {
				console.log('新建文件夹')
				editItem.value = JSON.parse(JSON.stringify({
					id: randomString(32),
					val: '',
					type: 'folder',
				}))

				menuList.value.push(editItem.value)
			} 
			// 新建子文件夹
			else {
				console.log('新建子文件夹')
				findParentMenu(menuList.value, type)
			}
		} 
		// 文档
		else if(type === 2) {
			// 新建文档
			if(!editItem.value) {
				console.log('新建文档')
				editItem.value = JSON.parse(JSON.stringify({
					id: randomString(32),
					val: '',
					type: 'file',
				}))

				menuList.value.push(editItem.value)
			} 
			// 新建子文档
			else {
				console.log('新建子文档')
				findParentMenu(menuList.value, type)
			}
		}

		nextTick(() => {
			editInput.value && editInput.value[0] && editInput.value[0].focus()
		})
		
	}

	function findParentMenu(list: Array, type: number) {
		console.log(type)
		
		for(let i = 0; i  0) {
					findParentMenu(list[i].children, type)
				}
			}
		}
	}

	return {
		editInput,
		isEdit,
		isNew,
		isEditRename,
		isShowPopper,
		modal,
		modalX,
		modalY,
		currentMenuId,
		editItem,
		menuList,
		preMenuList,
		selectItem,
		preSelectItem,
		dragItem,
		dropPosition,

		doMenuAction,
		expandAll,
		createNew,
		openRename,
		editTitle,
		showPopper,
		hidePopper,
		callBack
	}

})

使用

import cMenuWrap from '@/components/menu/c-menu-wrap.vue'

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/3dba44bdf5.html