实现效果
需求说明
- 鼠标按下左边菜单项暂存被点击的数据
- 拖动到右边松开则插入新数据(tree2)
- 如果未有父级,则直接插入tree2
- 如果有指定父级(在想放入的位置悬浮鼠标),则插入指定位置(前提是此菜单为父级菜单)
主文件
<template>
<div class="dis-flex">
<div class="left">
<Menu @move="moveChange" :tree="tree" />
</div>
<div class="right" @mouseup="moveConfirm">
<menu-new @handleClick="handleClick" @handleMouseEnter="handleMouseEnter" :tree="tree2" />
</div>
</div>
<!-- 跟随鼠标效果,展示被选中要移动的菜单项 -->
<div class="mouse-p"></div>
</template>
<script>
import Menu from './menu.vue'
import MenuNew from './menu-new.vue'
export default {
components: {
Menu,
MenuNew
},
data() {
return {
tree: [
{
path: '/home',
name: '首页'
}, {
path: '/treeRouter',
name: '菜单树'
}, {
path: '/tableDemo',
name: '表格示例'
}, {
path: '/formDemo',
name: '表单示例'
}, {
path: '/mine',
name: '个人中心',
childrenShow: true,
children: [
{
path: '/mine/mine1',
name: '菜单1',
childrenShow: true,
children: [
{
path: '/mine/mine1/mine3',
name: '菜单2'
},
{
path: '/mine/mine1/mine4',
name: '菜单3',
childrenShow: true,
children: [
{
path: '/mine/mine1/mine5',
name: '菜单4'
},
{
path: '/mine/mine1/mine6',
name: '菜单5'
}
]
}
]
},
{
path: '/mine2',
name: '菜单6'
}
]
}
],
tree2: [],
nowItem: null, // 当前被选中的项
nowTree: null, // 要移动或删除项所属的树
el: undefined, // 选中虚拟元素跟随鼠标
enterTree: null, // 选中的当前树
enterIndex: null // 选中的项索引
}
},
mounted() {
this.el = document.querySelector('.mouse-p')
window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', this.mouseEvent)
this.el.style.display = 'none'
})
},
methods: {
handleMouseEnter(tree,item, index) { // 获取选中的数据要插入的位置
this.enterTree = tree,
this.enterItem = item,
this.enterIndex = index
},
handleClick(tree, item, type) { // 获取操作项所在的树,被操作项,操作类型
this.nowTree = tree
this.operate(item, type)
},
operate(item, type) { // 对生成的菜单执行操作
for(let i=0; i<this.nowTree.length;i++) {
if (this.nowTree[i].path === item.path) {
if (type === 'top') { // 上移一位
this.nowTree[i] = this.nowTree[i - 1]
this.nowTree[i - 1] = item
return false;
} else if (type === 'bottom') { // 下移一位
this.nowTree[i] = this.nowTree[i + 1]
this.nowTree[i + 1] = item
return false;
} else if (type === 'delete') { // 删除操作
this.nowTree.splice(i, 1)
return false;
}
}
}
},
mouseEvent(e) {
if (this.nowItem) {
this.el.innerText = this.nowItem.name
this.el.style.display = 'block'
this.el.style.left = e.clientX - 300 + 'px'
this.el.style.top = e.clientY - 40 + 'px'
}
},
moveChange(item) {
this.nowItem = item
window.addEventListener('mousemove', this.mouseEvent)
},
moveConfirm() {
if (this.nowItem) { // 有选择菜单
let arr = (this.enterTree || this.tree2).filter(item => item.path === this.nowItem.path)
// 指定级别没有相同菜单则执行插入操作
if (arr.length === 0) {
let obj = this.deepCopy(this.nowItem)
// 如果有指定父级菜单
if (this.enterTree) {
if (this.enterItem.children) {
this.enterItem.children.push(obj)
} else {
this.enterTree.splice(this.enterIndex+1, 0, obj)
}
} else {
// 未获取到菜单,放入主菜单
this.tree2.push(obj)
}
}
// 请空暂存数据
this.enterIndex = null
this.enterTree = null
this.nowItem = null
}
},
deepCopy(instance) {
// 递归开始,判断参数是数组还是对象
const result = Array.isArray(instance) ? [] : instance instanceof Object ? {} : null
if (result === null) {
throw new Error(`需要传入Array或Object类型的参数,但现在是${typeof instance}类型的参数`)
}
// 如果参数是数组,开启for循环遍历数组
if (Array.isArray(instance)) {
for (let i = 0; i < instance.length; i++) {
const inst = instance[i]
// 如果数组中某个索引对应的是对象,就对其进行递归并赋值,否则直接赋值
result[i] = inst instanceof Object ? this.deepCopy(inst) : inst
}
// 如果参数是对象,开启for...in循环遍历对象
} else if (instance instanceof Object) {
if (instance.children) {
instance.childrenShow = true
}
for (const key in instance) {
// 如果对象中某个key对应的是一个数组或者对象,那么就对其递归并赋值给当前值,否则直接赋值
const condition = Array.isArray(instance[key]) || instance[key] instanceof Object
result[key] = condition ? this.deepCopy(instance[key]) : instance[key]
}
}
return result
}
}
}
</script>
<style scoped lang="css">
.dis-flex {
display: flex;
justify-content: space-between;
-webkit-user-select: none;
}
.left, .right {
width: 40%;
background: #000;
color: #fff;
padding: 30px;
height: 100vh;
}
.flex-column {
flex-direction: column;
}
.mouse-p {
background: rgb(65 135 255 / 30%);
height: 40px;
line-height: 40px;
position: fixed;
left: 0;
top: 0;
width: 300px;
display: none;
color: #fff;
}
</style>
主菜单
<template>
<ul class="menu">
<li v-for="(item, index) in tree" :key="index + item.path">
<div class="title"
@mousedown="mousedownClick(item)">
<div class="title-left">
<span class="icon" @click="changeItemShow(item)">
<svg v-if="item.children && item.childrenShow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"></path></svg>
<svg v-if="item.children && !item.childrenShow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg>
</span>
{{item.name}}
</div>
</div>
<template v-if="item.children && item.childrenShow">
<Menu @move="moveChange" :tree="item.children" />
</template>
</li>
</ul>
</template>
<script>
import Menu from './menu.vue'
export default {
props: {
tree: {
type: Array,
default: () => {
return []
}
}
},
components: {
Menu
},
methods: {
mousedownClick(item) {
this.emitParent(item)
},
emitParent(item) {
this.$emit('move', item)
},
moveChange(item) {
this.$emit('move', item)
},
changeItemShow(item) {
item.childrenShow = !item.childrenShow
}
}
}
</script>
<style scoped>
.menu {
text-align: left;
}
.menu li {
padding-left: 20px;
}
.menu li .title {
cursor: pointer;
font-size: 18px;
padding: 0 10px;
}
.menu li .title:hover {
background: rgb(135, 160, 180, 0.5);
}
.title .title-left {
display: flex;
justify-content: flex-start;
align-items: center;
}
.title .title-left .icon {
width: 24px;
}
.title .title-left .icon svg {
width: 16px;
height: 16px;
}
</style>
新菜单
<template>
<ul class="menu">
<li v-for="(item, index) in tree" :key="index" :index="index">
<div class="title"
@mouseenter="handleMouseEnter(tree, item, index)"
@mouseleave="handleMouseEnter(null, null, null)">
<div class="title-left">
<span class="icon" @click="changeItemShow(item)">
<svg v-if="item.children && item.children.length > 0 && item.childrenShow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"></path></svg>
<svg v-if="item.children && item.children.length > 0 && !item.childrenShow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg>
</span>
{{item.name}}
</div>
<div class="icon-box">
<span class="icon" v-if="index !== 0" @click="handleClick(tree, item, 'top')">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="M572.235 205.282v600.365a30.118 30.118 0 1 1-60.235 0V205.282L292.382 438.633a28.913 28.913 0 0 1-42.646 0 33.43 33.43 0 0 1 0-45.236l271.058-288.045a28.913 28.913 0 0 1 42.647 0L834.5 393.397a33.43 33.43 0 0 1 0 45.176 28.913 28.913 0 0 1-42.647 0l-219.618-233.23z"></path></svg>
</span>
<span class="icon" v-if="index !== tree.length - 1" @click="handleClick(tree, item, 'bottom')">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="M544 805.888V168a32 32 0 1 0-64 0v637.888L246.656 557.952a30.72 30.72 0 0 0-45.312 0 35.52 35.52 0 0 0 0 48.064l288 306.048a30.72 30.72 0 0 0 45.312 0l288-306.048a35.52 35.52 0 0 0 0-48 30.72 30.72 0 0 0-45.312 0L544 805.824z"></path></svg>
</span>
<span class="icon" @click="handleClick(tree, item, 'delete')">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa=""><path fill="currentColor" d="M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V256zm448-64v-64H416v64h192zM224 896h576V256H224v640zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32zm192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32z"></path></svg>
</span>
</div>
</div>
<template v-if="item.children && item.childrenShow">
<menu-new @handleClick="handleClick" @handleMouseEnter="handleMouseEnter" :tree="item.children" />
</template>
</li>
</ul>
</template>
<script>
import MenuNew from './menu-new.vue'
export default {
name: 'MenuNew',
props: {
tree: {
type: Array,
default: () => {
return []
}
}
},
components: {
MenuNew
},
methods: {
handleClick(tree, item, type) {
this.$emit('handleClick', tree, item, type)
},
handleMouseEnter(tree, item, index) {
this.$emit('handleMouseEnter', tree, item, index)
},
changeItemShow(item) {
item.childrenShow = !item.childrenShow
}
}
}
</script>
<style scoped>
.menu {
text-align: left;
}
.menu li {
padding-left: 20px;
}
.menu li .title {
cursor: pointer;
font-size: 18px;
line-height: 24px;
padding: 0 10px;
display: flex;
justify-content: space-between;
}
.title .title-left {
display: flex;
justify-content: flex-start;
align-items: center;
}
.title .title-left .icon {
width: 24px;
}
.title .title-left .icon svg {
width: 16px;
height: 16px;
}
.icon-box {
display: none;
}
.icon-box svg {
width: 16px;
height: 16px;
margin: 0 5px;
}
.menu li .title:hover {
background: rgb(135, 160, 180, 0.5);
}
.menu li .title:hover .icon-box {
display: block;
}
</style>