Vue + Element Plus 实现一个简单的拖动生成菜单

460 阅读1分钟

实现效果

image.png

image.png

需求说明

  • 鼠标按下左边菜单项暂存被点击的数据
  • 拖动到右边松开则插入新数据(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>