ElementUI-tree拖拽功能与节点自定义

前言

在管理端会遇到多分类时,要求有层次展示出来,并且每个分类有额外的操作。例如:添加分类、编辑分类、删除、拖到分类等。

下面将会记录这样的一个需求实习过程。

了解需求

  1. 分类展示按层级展示
  2. 分类根据特定的参数展示可以操作的按钮,分类的操作有增、删、改
  3. 分类还支持拖拽功能,并不是所有的分类都支持拖拽
  4. 点分类去执行别的操作。例如:刷新数据(不实现)
  5. 增加分类之后刷新分类数据,当前选择的分类为增加的分类
  6. 删除分类后回到上一级分类
  7. 右击分类和点击操作按钮均可以弹出操作弹窗
  8. 点击分类前的箭头可展开和折叠分类

效果图

  • 分类展示
  • 分类操作的弹窗

组件库

采用ElementUI 中的 Tree树形控件、Dropdown下拉菜单

开始编码

搭建tree 组件

  • html 部分:
html 复制代码
<el-tree :data="classifyData" node-key="id" draggable ref="tree" :accordion="false" 
      auto-expand-parent :default-expanded-keys="[checkedId]" :props="defaultProps"
      :allow-drop="allowDrop" :allow-drag="allowDrag"
      @node-drag-start="handleDragStart" @node-drop="handleDrop"
      @node-click="nodeClick" @node-contextmenu="rightClick"
      :show-checkbox="false" :check-strictly="true"  >
        <div class="custom-tree-node" slot-scope="{ node, data }">
          <span>{{ data.name }}</span> 
          <span>
            <el-dropdown type="primary" trigger="click" :ref="'messageDrop'+data.id" @visible-change="controlCheckedKeys">
              <span class="el-dropdown-link" @click.stop="setSeletKey(data.id)">  
                <img src="~@/../more-active.png" v-if="checkedKeys == data.id"  class="myicon-opt" /> 
                <img src="~@/../more.png" v-else class="myicon-opt" />
              </span>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item v-if="data.is_add_classify">
                  <div @click="openClassify(data.id,'新增子分类')">
                    <img src="~@/../add.png" class="myicon-opt"/> 
                    新增子分类
                  </div>
                </el-dropdown-item>
                <el-dropdown-item v-if="data.is_edit_sort">
                  <div @click="editClassify(data)"> 
                    <img src="~@/../edit.png" class="myicon-opt" /> 
                    修改
                  </div>
                </el-dropdown-item>
                <el-dropdown-item v-if="data.is_edit_sort">
                  <div @click="delBefore(data.id,data.parent_id)">
                    <img src="~@/../del.png" class="myicon-opt" />
                    删除
                  </div>
                </el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </span>
        </div>
      </el-tree>
  • css
css 复制代码
<style lang="stylus" scoped>
.active{
  background: #F2F6F9;
  color: #409EFF;
}
.classify{
  padding : 0 16px;
  height: 40px;
  font-family: PingFangSC-Medium;
  font-weight: 500;
  font-size: 15px; 
  line-height:40px;
}
 
.el-tree ::v-deep {
  .el-tree-node__content{ 
    @extend .classify;
    &:hover{
      @extend .active; 
    }
    .el-tree-node__expand-icon.is-leaf{
      // display:none
      margin-left:-12px
    }
  }
  .is-checked > .el-tree-node__content{
    @extend .active;
  } 
} 
.custom-tree-node{
  display: flex;
  justify-content: space-between;
  width: 100%;
}
.myicon-opt{
  vertical-align: middle;
  width: 16px;
  height: 16px;
} 
</style>
  • js
javascript 复制代码
<script>
  export default {
    props:{
      activeId:{
        type:[String,Number],
        default:''
      },
      classifyData:{
        type:Array,
        default:[]
      }
    },
    watch:{
      activeId: {
        handler(v,o){
          // v 值为0时, 0 == '' 值为true
          if (typeof v == 'number') {
            this.checkedId = v 
            this.$nextTick(()=>{
              this.$refs.tree.setCheckedKeys([v])
            }) 
          }
        },
        immediate:true,
        deep:true 
      },
    },
    data() {
      return {
        checkedId:'', 
        checkedKeys:'', 
        defaultProps: {
          children: 'child',
          label: 'name'
        },
        classifyCofig:{
          flag:false,
          Id: '',
          title:'',
          value:''
        }, 
      }
    },
    methods: { 
      // 点击分类名称
      nodeClick(data,node){ 
        this.checkedId = data.id  
        this.$refs.tree.setCheckedKeys([data.id]) 
        node.expanded = true
        this.$emit('selectId',data.id)
        // console.log('node',data.id,node.parent)
        let addId  = [ data.id]
        if(node.parent.parent != null)  this.selectNode(addId,node.parent)
        // console.log('addId',addId)
        this.$emit('selectaddId', addId)
      },
      // 获取多层级的父类id加入到数组下标为0的位置
      selectNode(id,node){ 
        id.unshift(node.data.id)
        if(node.parent.parent != null){
          this.selectNode(id,node.parent)
        } 
      },
      // 右击分类
      rightClick(event,data, Node, element){ 
        setTimeout(()=>{
          this.checkedKeys = data.id 
          this.$refs['messageDrop'+data.id].show() 
        })
      },
      // 点击操作按钮
      setSeletKey(k){ 
        setTimeout(()=>{
          this.checkedKeys = k
        })
      },
      // 下拉菜单的异步监听,打开(true)还是隐藏(flase)
      controlCheckedKeys(flag){  
        if(!flag){
          this.checkedKeys = ''
        }
      },
      // 节点开始拖拽时触发的事件
      handleDragStart(node) {
        if(!node.data.is_edit_sort){
          return false
        } 
      }, 
      // 拖拽成功完成时触发的事件
      handleDrop(draggingNode, dropNode, dropType) {
        if(dropType == 'none') return 
        // 准备排序参数可自行更改
        let params = {
          pk1: draggingNode.data.id,
          pk2: dropNode.data.id,
          direction:dropType == 'before' ? -1 : 1
        }
        this.orderClassify(params)
      }, 
      /** 
       *  拖拽时判定目标节点能否被放置。
       * @param {*} draggingNode 
       * @param {*} dropNode 
       * @param {*} type 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后
       */
      allowDrop(draggingNode, dropNode, type) {
        if (draggingNode.level === dropNode.level) { 
          if (draggingNode.data.parent_id === dropNode.data.parent_id && dropNode.data.is_edit_sort) {
            // 向上拖拽 || 向下拖拽
            return type === "prev" || type === "next"
          }
        } else {
          // 不同级进行处理
          return false
        }
      },
      //判断节点能否被拖拽
      allowDrag(draggingNode) {
        if(!draggingNode.data.is_edit_sort){
          return false
        }
        return true 
      }, 
      async orderClassify(params){
        // 发送排序请求
      }, 
      setClassCofig(flag,id,title,value){
        this.classifyCofig['flag'] = flag
        this.classifyCofig['Id'] = id
        this.classifyCofig['title'] = title
        this.classifyCofig['value'] = value
      },
      openClassify(pid,txt){
        this.setClassCofig(true,pid, txt ? txt : '新增分类','')   
      },
      editClassify(row){
        this.setClassCofig(true,row.id, '修改分类', row.name) 
      }, 
      closeAdd(){
        this.setClassCofig(false,'', '', '')
      },
      // 新增/修改分类
      async sureClassify(params){ 
        let {value,Id} = this.classifyCofig
        // 通过value的值判断当前是新增还是修改
        // 刷新分类,cid 新分类的id
        let refresh = { }
        if(value){ 
          refresh.flag = false
        }else{ 
          refresh.flag = true
        }  
        // 准备参数,发送请求
        // 请求成功后执行
        this.setClassCofig(false,'', '', '')
        refresh.cid = value? this.checkedId : res.data.data.id
        this.$emit('refreshClass',refresh)
      },
      //判断分类是否可以删除
      async delBefore(id,pid){
       //1.自定义判断是否可以删除,
       
       //2.可以删去执行删除操作,
       this.sureDelete(id,pid)
        
      },
      //删除分类,删除后回到上一级
      async sureDelete(id,pid){
        //1.准备删除的接口使用数据
        //2.发起请求,请求成功后执行下面代码
        this.setClassCofig(false,'', '', '')
          let refresh = {
            flag: true,
            cid: pid
          }
          this.$emit('refreshClass',refresh)
      },
    }
  };
</script>

使用tree组件

  • html
javascript 复制代码
 <PersonalTree :activeId="currentClassfiyId" :classifyData="classifyData"
        @selectId="changeSelectId" @selectaddId="setAddId" @refreshClass="refreshClass"/>
  • js
javascript 复制代码
<script>
// 在此处引入tree组件命名为customTree
  export default{
    components:{customTree},
      data(){
        return{
          currentClassfiyId:'',
          addClassifyId:[],
          classifyData:[], 
        }
      },
    mounted(){
      this.getClassList(true) 
    },
    methods:{
        async getClassList(flagScene,cid){
          // console.log(flagScene,cid)
          // 发送请求,获取全部分类
          this.classifyData = res.data.data.classify 
          this.currentClassfiyId = cid || this.classifyData?.[0].id
          if(flagScene){ 
              // 可以去获取内容
            } 
          }
        },
        refreshClass({flag,cid}){
         // 去刷新分类列表
           this.getClassList(flag,cid)
        },
        setAddId(val){
          this.addClassifyId = val
        },
        changeSelectId(id){
          this.currentClassfiyId = id
          // 可以去获取内容
        },
    }
   }
</script>   
 

classifyData的数据:

javascript 复制代码
[{
  "id": 1033,
  "name": "一级分类",
  "parent_id": 0, 
  "level": 1,
  "child": [
    {
      "id": 1036,
      "name": "aaaaaaaaa",
      "parent_id": 1033, 
      "level": 2,
      "child": [],
      "is_edit_sort": true,
      "is_add_classify": true,
      "is_add_scene": true
    },
    {
      "id": 1035,
      "name": "aaaaa",
      "parent_id": 1033,  
      "level": 2,
      "child": [
        {
          "id": 1037,
          "name": "a-1",
          "parent_id": 1035, 
          "level": 3,
          "child": [
            {
              "id": 1040,
              "name": "a-1-3",
              "parent_id": 1037, 
              "level": 4,
              "child": [],
              "is_edit_sort": true,
              "is_add_classify": false,
              "is_add_scene": true
            },
            {
              "id": 1038,
              "name": "a-1-1",
              "parent_id": 1037, 
              "level": 4,
              "child": [],
              "is_edit_sort": true,
              "is_add_classify": false,
              "is_add_scene": true
            }
          ],
          "is_edit_sort": true,
          "is_add_classify": true,
          "is_add_scene": true
        }
      ],
      "is_edit_sort": true,
      "is_add_classify": true,
      "is_add_scene": true
    }
  ],
  "is_edit_sort": true,
  "is_add_classify": true,
  "is_add_scene": true
},{
  "id": 1032,
  "name": "测试分类b",
  "parent_id": 0, 
  "level": 1,
  "child": [],
  "is_edit_sort": true,
  "is_add_classify": true,
  "is_add_scene": true
},{
  "id": 1015,
  "name": "无操作区",
  "parent_id": 0,
  "level": 1,
  "child": [],
  "is_edit_sort": false,
  "is_add_classify": false,
  "is_add_scene": false
}]

如有帮到您,请收藏+关注哦!!!

相关推荐
y先森4 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy4 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189114 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿6 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡6 小时前
commitlint校验git提交信息
前端
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇7 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒7 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员7 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐7 小时前
前端图像处理(一)
前端