• 基于Vue实现可以拖拽的树形表格(原创)


     在线DEMO查看

    本博文会分为两部分,第一部分为使用方式,第二部分为实现方式

    安装方式

    npm i drag-tree-table --save-dev

    使用方式

    import dragTreeTable from 'drag-tree-table'
    

     模版写法

    <dragTreeTable :data="treeData" :onDrag="onTreeDataChange"></dragTreeTable>

    data参数示例

    {
      lists: [
      {
        "id":40,
        "parent_id":0,
        "order":0,
        "name":"动物类",
        "open":true,
        "lists":[]
      },{
        "id":5,
        "parent_id":0,
        "order":1,
        "name":"昆虫类",
        "open":true,
        "lists":[
          {
            "id":12,
            "parent_id":5,
            "open":true,
            "order":0,
            "name":"蚂蚁",
            "lists":[]
          }
        ]
      },
      {
        "id":19,
        "parent_id":0,
        "order":2,
        "name":"植物类",
        "open":true,
        "lists":[]
      }
     ],
      columns: [
      {
       type: 'selection',
       title: '名称',
       field: 'name',
        200,
       align: 'center',
       formatter: (item) => {
         return '<a>'+item.name+'</a>'
       }
      },
      {
        title: '操作',
        type: 'action',
         350,
        align: 'center',
        actions: [
          {
            text: '查看角色',
            onclick: this.onDetail,
            formatter: (item) => {
              return '<i>查看角色</i>'
            }
          },
          {
            text: '编辑',
            onclick: this.onEdit,
            formatter: (item) => {
              return '<i>编辑</i>'
            }
          }
        ]
      },
      ]
    }

     onDrag在表格拖拽时触发,返回新的list

    onTreeDataChange(lists) {
        this.treeData.lists = lists
    }

    到这里组件的使用方式已经介绍完毕

    实现

    • 递归生成树性结构(非JSX方式实现)
    • 实现拖拽排序(借助H5的dragable属性)
    • 单元格内容自定义展示

    组件拆分-共分为四个组件

      dragTreeTable.vue是入口组件,定义整体结构

      row是递归组件(核心组件)

      clolmn单元格,内容承载

      space控制缩进

    看一下dragTreeTable的结构

    <template>
        <div class="drag-tree-table">
            <div class="drag-tree-table-header">
              <column
                v-for="(item, index) in data.columns"
                :width="item.width"
                :key="index" >
                {{item.title}}
              </column>
            </div>
            <div class="drag-tree-table-body" @dragover="draging" @dragend="drop">
              <row depth="0" :columns="data.columns"
                :model="item" v-for="(item, index) in data.lists" :key="index">
            </row>
            </div>
        </div>
    </template>

    看起来分原生table很像,dragTreeTable主要定义了tree的框架,并实现拖拽逻辑

    filter函数用来匹配当前鼠标悬浮在哪个行内,并分为三部分,上中下,并对当前匹配的行进行高亮
    resetTreeData当drop触发时调用,该方法会重新生成一个新的排完序的数据,然后返回父组件

    下面是所有实现代码

      1 <script>
      2   import row from './row.vue'
      3   import column from './column.vue'
      4   import space from './space.vue'
      5   document.body.ondrop = function (event) {
      6     event.preventDefault();
      7     event.stopPropagation();
      8   }
      9   export default {
     10     name: "dragTreeTable",
     11     components: {
     12         row,
     13         column,
     14         space
     15     },
     16     props: {
     17       data: Object,
     18       onDrag: Function
     19     },
     20     data() {
     21       return {
     22         treeData: [],
     23         dragX: 0,
     24         dragY: 0,
     25         dragId: '',
     26         targetId: '',
     27         whereInsert: ''
     28       }
     29     },
     30     methods: {
     31       getElementLeft(element) {
     32         var actualLeft = element.offsetLeft;
     33         var current = element.offsetParent;
     34         while (current !== null){
     35           actualLeft += current.offsetLeft;
     36           current = current.offsetParent;
     37         }
     38         return actualLeft
     39       },
     40       getElementTop(element) {
     41         var actualTop = element.offsetTop;
     42         var current = element.offsetParent;
     43         while (current !== null) {
     44           actualTop += current.offsetTop;
     45           current = current.offsetParent;
     46         }
     47         return actualTop
     48       },
     49       draging(e) {
     50         if (e.pageX == this.dragX && e.pageY == this.dragY) return
     51         this.dragX = e.pageX
     52         this.dragY = e.pageY
     53         this.filter(e.pageX, e.pageY)
     54       },
     55       drop(event) {
     56         this.clearHoverStatus()
     57         this.resetTreeData()
     58       },
     59       filter(x,y) {
     60         var rows = document.querySelectorAll('.tree-row')
     61         this.targetId = undefined
     62         for(let i=0; i < rows.length; i++) {
     63           const row = rows[i]
     64           const rx = this.getElementLeft(row);
     65           const ry = this.getElementTop(row);
     66           const rw = row.clientWidth;
     67           const rh = row.clientHeight;
     68           if (x > rx && x < (rx + rw) && y > ry && y < (ry + rh)) {
     69             const diffY = y - ry
     70             const hoverBlock = row.children[row.children.length - 1]
     71             hoverBlock.style.display = 'block'
     72             const targetId = row.getAttribute('tree-id')
     73             if (targetId == window.dragId){
     74               this.targetId = undefined
     75               return
     76             }
     77             this.targetId = targetId
     78             let whereInsert = ''
     79             var rowHeight = document.getElementsByClassName('tree-row')[0].clientHeight
     80             if (diffY/rowHeight > 3/4) {
     81               console.log(111, hoverBlock.children[2].style)
     82               if (hoverBlock.children[2].style.opacity !== '0.5') {
     83                 this.clearHoverStatus()
     84                 hoverBlock.children[2].style.opacity = 0.5
     85               }
     86               whereInsert = 'bottom'
     87             } else if (diffY/rowHeight > 1/4) {
     88               if (hoverBlock.children[1].style.opacity !== '0.5') {
     89                 this.clearHoverStatus()
     90                 hoverBlock.children[1].style.opacity = 0.5
     91               }
     92               whereInsert = 'center'
     93             } else {
     94               if (hoverBlock.children[0].style.opacity !== '0.5') {
     95                 this.clearHoverStatus()
     96                 hoverBlock.children[0].style.opacity = 0.5
     97               }
     98               whereInsert = 'top'
     99             }
    100             this.whereInsert = whereInsert
    101           }
    102         }
    103       },
    104       clearHoverStatus() {
    105         var rows = document.querySelectorAll('.tree-row')
    106         for(let i=0; i < rows.length; i++) {
    107           const row = rows[i]
    108           const hoverBlock = row.children[row.children.length - 1]
    109           hoverBlock.style.display = 'none'
    110           hoverBlock.children[0].style.opacity = 0.1
    111           hoverBlock.children[1].style.opacity = 0.1
    112           hoverBlock.children[2].style.opacity = 0.1
    113         }
    114       },
    115       resetTreeData() {
    116         if (this.targetId === undefined) return 
    117         const newList = []
    118         const curList = this.data.lists
    119         const _this = this
    120         function pushData(curList, needPushList) {
    121           for( let i = 0; i < curList.length; i++) {
    122             const item = curList[i]
    123             var obj = _this.deepClone(item)
    124             obj.lists = []
    125             if (_this.targetId == item.id) {
    126               const curDragItem = _this.getCurDragItem(_this.data.lists, window.dragId)
    127               if (_this.whereInsert === 'top') {
    128                 curDragItem.parent_id = item.parent_id
    129                 needPushList.push(curDragItem)
    130                 needPushList.push(obj)
    131               } else if (_this.whereInsert === 'center'){
    132                 curDragItem.parent_id = item.id
    133                 obj.lists.push(curDragItem)
    134                 needPushList.push(obj)
    135               } else {
    136                 curDragItem.parent_id = item.parent_id
    137                 needPushList.push(obj)
    138                 needPushList.push(curDragItem)
    139               }
    140             } else {
    141               if (window.dragId != item.id)
    142                 needPushList.push(obj)
    143             }
    144             
    145             if (item.lists && item.lists.length) {
    146               pushData(item.lists, obj.lists)
    147             }
    148           }
    149         }
    150         pushData(curList, newList)
    151         this.onDrag(newList)
    152       },
    153       deepClone (aObject) {
    154         if (!aObject) {
    155           return aObject;
    156         }
    157         var bObject, v, k;
    158         bObject = Array.isArray(aObject) ? [] : {};
    159         for (k in aObject) {
    160           v = aObject[k];
    161           bObject[k] = (typeof v === "object") ? this.deepClone(v) : v;
    162         }
    163         return bObject;
    164       },
    165       getCurDragItem(lists, id) {
    166         var curItem = null
    167         var _this = this
    168         function getchild(curList) {
    169           for( let i = 0; i < curList.length; i++) {
    170             var item = curList[i]
    171             if (item.id == id) {
    172               curItem = JSON.parse(JSON.stringify(item))
    173               break
    174             } else if (item.lists && item.lists.length) {
    175               getchild(item.lists)
    176             }
    177           }
    178         }
    179         getchild(lists)
    180         return curItem;
    181       }
    182     }
    183   }
    184 </script>
    View Code

    row组件核心在于递归,并注册拖拽事件,v-html支持传入函数,这样可以实现自定义展示,渲染数据时需要判断是否有子节点,有的画递归调用本身,并传入子节点数据

    结构如下

      1 <template>
      2         <div class="tree-block" draggable="true" @dragstart="dragstart($event)"
      3             @dragend="dragend($event)">
      4             <div class="tree-row" 
      5                 @click="toggle" 
      6                 :tree-id="model.id"
      7                 :tree-p-id="model.parent_id"> 
      8                 <column
      9                     v-for="(subItem, subIndex) in columns"
     10                     v-bind:class="'align-' + subItem.align"
     11                     :field="subItem.field"
     12                     :width="subItem.width"
     13                     :key="subIndex">
     14                     <span v-if="subItem.type === 'selection'">
     15                         <space :depth="depth"/>
     16                         <span v-if = "model.lists && model.lists.length" class="zip-icon" v-bind:class="[model.open ? 'arrow-bottom' : 'arrow-right']">
     17                         </span>
     18                         <span v-else class="zip-icon arrow-transparent">
     19                         </span>
     20                         <span v-if="subItem.formatter" v-html="subItem.formatter(model)"></span>
     21                         <span v-else v-html="model[subItem.field]"></span>
     22 
     23                     </span>
     24                     <span v-else-if="subItem.type === 'action'">
     25                         <a class="action-item"
     26                             v-for="(acItem, acIndex) in subItem.actions"
     27                             :key="acIndex"
     28                             type="text" size="small" 
     29                             @click.stop.prevent="acItem.onclick(model)">
     30                             <i :class="acItem.icon" v-html="acItem.formatter(model)"></i>&nbsp;
     31                         </a>
     32                     </span>
     33                     <span v-else-if="subItem.type === 'icon'">
     34                          {{model[subItem.field]}}
     35                     </span>
     36                     <span v-else>
     37                         {{model[subItem.field]}}
     38                     </span>
     39                 </column>
     40                 <div class="hover-model" style="display: none">
     41                     <div class="hover-block prev-block">
     42                         <i class="el-icon-caret-top"></i>
     43                     </div>
     44                     <div class="hover-block center-block">
     45                         <i class="el-icon-caret-right"></i>
     46                     </div>
     47                     <div class="hover-block next-block">
     48                         <i class="el-icon-caret-bottom"></i>
     49                     </div>
     50                 </div>
     51             </div>
     52             <row 
     53                 v-show="model.open"
     54                 v-for="(item, index) in model.lists" 
     55                 :model="item"
     56                 :columns="columns"
     57                 :key="index" 
     58                 :depth="depth * 1 + 1"
     59                 v-if="isFolder">
     60             </row>
     61         </div>
     62         
     63     </template>
     64     <script>
     65     import column from './column.vue'
     66     import space from './space.vue'
     67     export default {
     68       name: 'row',
     69         props: ['model','depth','columns'],
     70         data() {
     71             return {
     72                 open: false,
     73                 visibility: 'visible'
     74             }
     75         },
     76         components: {
     77           column,
     78           space
     79         },
     80         computed: {
     81             isFolder() {
     82                 return this.model.lists && this.model.lists.length
     83             }
     84         },
     85         methods: {
     86             toggle() {
     87                 if(this.isFolder) {
     88                     this.model.open = !this.model.open
     89                 }
     90             },
     91             dragstart(e) {
     92                 e.dataTransfer.setData('Text', this.id);
     93                 window.dragId = e.target.children[0].getAttribute('tree-id')
     94                 e.target.style.opacity = 0.2
     95             },
     96             dragend(e) {
     97                 e.target.style.opacity = 1;
     98                 
     99             }
    100         }
    101     }
    View Code 

    clolmn和space比较简单,这里就不过多阐述

    上面就是整个实现过程,组件在chrome上运行稳定,因为用H5的dragable,所以兼容会有点问题,后续会修改拖拽的实现方式,手动实现拖拽

    开源不易,如果本文对你有所帮助,请给我个star

    ------

    作者:木法传

  • 相关阅读:
    Beans
    Redis记录-Redis命令
    Redis记录-Redis介绍
    RESTful记录-RESTful服务
    RESTful记录-RESTful内容
    RESTful记录-RESTful介绍
    HTTP记录-HTTP介绍
    Solr记录-solr检索和查询数据
    Solr记录-solr文档xml
    Solr记录-solr内核与索引
  • 原文地址:https://www.cnblogs.com/bfgis/p/9805928.html
Copyright © 2020-2023  润新知