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
}]

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

相关推荐
im_AMBER14 小时前
Web 开发 27
前端·javascript·笔记·后端·学习·web
蓝胖子的多啦A梦14 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码14 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo15 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.15 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
一壶浊酒..15 小时前
ajax局部更新
前端·ajax·okhttp
DoraBigHead16 小时前
React 架构重生记:从递归地狱到时间切片
前端·javascript·react.js
彩旗工作室17 小时前
WordPress 本地开发环境完全指南:从零开始理解 Local by Flywhee
前端·wordpress·网站
iuuia17 小时前
02--CSS基础
前端·css
kyle~17 小时前
Qt---setAttribute设置控件或窗口的内部属性
服务器·前端·c++·qt