因业务需求, 需要一个树形表格, 并且支持拖拽排序, 任意未知插入, GitHub 搜了下, 真不到合适的, 大部分树形表格都没有拖拽功能, 所以决定自己实现一个. 这里分享一下实现过程, 项目源代码请看 https://github.com/ColdDay/vue-drag-tree-table , 插件已打包封装好, 发布到 https://www.npmjs.com/package/drag-tree-table 上
本博文会分为两部分, 第一部分为使用方式, 第二部分为实现方式
安装方式
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',
- width: 200,
- align: 'center',
- formatter: (item) => {
- return '<a>'+item.name+'</a>'
- }
- },
- {
- title: '操作',
- type: 'action',
- width: 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 触发时调用, 该方法会重新生成一个新的排完序的数据, 然后返回父组件
下面是所有实现代码
- <script>
- import row from './row.vue'
- import column from './column.vue'
- import space from './space.vue'
- document.body.ondrop = function (event) {
- event.preventDefault();
- event.stopPropagation();
- }
- export default {
- name: "dragTreeTable",
- components: {
- row,
- column,
- space
- },
- props: {
- data: Object,
- onDrag: Function
- },
- data() {
- return {
- treeData: [],
- dragX: 0,
- dragY: 0,
- dragId: '',
- targetId: '',
- whereInsert: ''
- }
- },
- methods: {
- getElementLeft(element) {
- var actualLeft = element.offsetLeft;
- var current = element.offsetParent;
- while (current !== null){
- actualLeft += current.offsetLeft;
- current = current.offsetParent;
- }
- return actualLeft
- },
- getElementTop(element) {
- var actualTop = element.offsetTop;
- var current = element.offsetParent;
- while (current !== null) {
- actualTop += current.offsetTop;
- current = current.offsetParent;
- }
- return actualTop
- },
- draging(e) {
- if (e.pageX == this.dragX && e.pageY == this.dragY) return
- this.dragX = e.pageX
- this.dragY = e.pageY
- this.filter(e.pageX, e.pageY)
- },
- drop(event) {
- this.clearHoverStatus()
- this.resetTreeData()
- },
- filter(x,y) {
- var rows = document.querySelectorAll('.tree-row')
- this.targetId = undefined
- for(let i=0; i <rows.length; i++) {
- const row = rows[i]
- const rx = this.getElementLeft(row);
- const ry = this.getElementTop(row);
- const rw = row.clientWidth;
- const rh = row.clientHeight;
- if (x> rx && x <(rx + rw) && y> ry && y <(ry + rh)) {
- const diffY = y - ry
- const hoverBlock = row.children[row.children.length - 1]
- hoverBlock.style.display = 'block'
- const targetId = row.getAttribute('tree-id')
- if (targetId == Windows.dragId){
- this.targetId = undefined
- return
- }
- this.targetId = targetId
- let whereInsert = '' var rowHeight = document.getElementsByClassName('tree-row')[0].clientHeight
- if (diffY/rowHeight> 3/4) {
- console.log(111, hoverBlock.children[2].style)
- if (hoverBlock.children[2].style.opacity !== '0.5') {
- this.clearHoverStatus()
- hoverBlock.children[2].style.opacity = 0.5
- }
- whereInsert = 'bottom'
- } else if (diffY/rowHeight> 1/4) {
- if (hoverBlock.children[1].style.opacity !== '0.5') {
- this.clearHoverStatus()
- hoverBlock.children[1].style.opacity = 0.5
- }
- whereInsert = 'center'
- } else {
- if (hoverBlock.children[0].style.opacity !== '0.5') {
- this.clearHoverStatus()
- hoverBlock.children[0].style.opacity = 0.5
- }
- whereInsert = 'top'
- }
- this.whereInsert = whereInsert
- }
- }
- },
- clearHoverStatus() {
- var rows = document.querySelectorAll('.tree-row')
- for(let i=0; i <rows.length; i++) {
- const row = rows[i]
- const hoverBlock = row.children[row.children.length - 1]
- hoverBlock.style.display = 'none'
- hoverBlock.children[0].style.opacity = 0.1
- hoverBlock.children[1].style.opacity = 0.1
- hoverBlock.children[2].style.opacity = 0.1
- }
- },
- resetTreeData() {
- if (this.targetId === undefined) return
- const newList = []
- const curList = this.data.lists
- const _this = this
- function pushData(curList, needPushList) {
- for( let i = 0; i < curList.length; i++) {
- const item = curList[i]
- var obj = _this.deepClone(item)
- obj.lists = []
- if (_this.targetId == item.id) {
- const curDragItem = _this.getCurDragItem(_this.data.lists, Windows.dragId)
- if (_this.whereInsert === 'top') {
- curDragItem.parent_id = item.parent_id
- needPushList.push(curDragItem)
- needPushList.push(obj)
- } else if (_this.whereInsert === 'center'){
- curDragItem.parent_id = item.id
- obj.lists.push(curDragItem)
- needPushList.push(obj)
- } else {
- curDragItem.parent_id = item.parent_id
- needPushList.push(obj)
- needPushList.push(curDragItem)
- }
- } else {
- if (Windows.dragId != item.id)
- needPushList.push(obj)
- }
- if (item.lists && item.lists.length) {
- pushData(item.lists, obj.lists)
- }
- }
- }
- pushData(curList, newList)
- this.onDrag(newList)
- },
- deepClone (aObject) {
- if (!aObject) {
- return aObject;
- }
- var bObject, v, k;
- bObject = Array.isArray(aObject) ? [] : {};
- for (k in aObject) {
- v = aObject[k];
- bObject[k] = (typeof v === "object") ? this.deepClone(v) : v;
- }
- return bObject;
- },
- getCurDragItem(lists, id) {
- var curItem = null
- var _this = this
- function getchild(curList) {
- for( let i = 0; i < curList.length; i++) {
- var item = curList[i]
- if (item.id == id) {
- curItem = JSON.parse(JSON.stringify(item))
- break
- } else if (item.lists && item.lists.length) {
- getchild(item.lists)
- }
- }
- }
- getchild(lists)
- return curItem;
- }
- }
- }
- </script>
- View Code
row 组件核心在于递归, 并注册拖拽事件, v-HTML 支持传入函数, 这样可以实现自定义展示, 渲染数据时需要判断是否有子节点, 有的画递归调用本身, 并传入子节点数据
结构如下
- <template>
- <div class="tree-block" draggable="true" @dragstart="dragstart($event)"
- @dragend="dragend($event)">
- <div class="tree-row"
- @click="toggle"
- :tree-id="model.id"
- :tree-p-id="model.parent_id">
- <column
- v-for="(subItem, subIndex) in columns"
- v-bind:class="'align-' + subItem.align" :field="subItem.field" :width="subItem.width" :key="subIndex">
- <span v-if="subItem.type ==='selection'">
- <space :depth="depth"/>
- <span v-if = "model.lists && model.lists.length" class="zip-icon" v-bind:class="[model.open ?'arrow-bottom':'arrow-right']">
- </span>
- <span v-else class="zip-icon arrow-transparent">
- </span>
- <span v-if="subItem.formatter" v-HTML="subItem.formatter(model)"></span>
- <span v-else v-HTML="model[subItem.field]"></span>
- </span>
- <span v-else-if="subItem.type ==='action'">
- <a class="action-item"
- v-for="(acItem, acIndex) in subItem.actions"
- :key="acIndex"
- type="text" size="small"
- @click.stop.prevent="acItem.onclick(model)">
- <i :class="acItem.icon" v-HTML="acItem.formatter(model)"></i>
- </a>
- </span>
- <span v-else-if="subItem.type ==='icon'">
- {{model[subItem.field]}}
- </span>
- <span v-else>
- {{model[subItem.field]}}
- </span>
- </column>
- <div class="hover-model" style="display: none">
- <div class="hover-block prev-block">
- <i class="el-icon-caret-top"></i>
- </div>
- <div class="hover-block center-block">
- <i class="el-icon-caret-right"></i>
- </div>
- <div class="hover-block next-block">
- <i class="el-icon-caret-bottom"></i>
- </div>
- </div>
- </div>
- <row
- v-show="model.open"
- v-for="(item, index) in model.lists"
- :model="item"
- :columns="columns"
- :key="index"
- :depth="depth * 1 + 1"
- v-if="isFolder">
- </row>
- </div>
- </template>
- <script>
- import column from './column.vue'
- import space from './space.vue'
- export default {
- name: 'row',
- props: ['model','depth','columns'],
- data() {
- return {
- open: false,
- visibility: 'visible'
- }
- },
- components: {
- column,
- space
- },
- computed: {
- isFolder() {
- return this.model.lists && this.model.lists.length
- }
- },
- methods: {
- toggle() {
- if(this.isFolder) {
- this.model.open = !this.model.open
- }
- },
- dragstart(e) {
- e.dataTransfer.setData('Text', this.id);
- Windows.dragId = e.target.children[0].getAttribute('tree-id')
- e.target.style.opacity = 0.2
- },
- dragend(e) {
- e.target.style.opacity = 1;
- }
- }
- }
- View Code
clolmn 和 space 比较简单, 这里就不过多阐述
上面就是整个实现过程, 组件在 Chrome 上运行稳定, 因为用 H5 的 dragable, 所以兼容会有点问题, 后续会修改拖拽的实现方式, 手动实现拖拽
开源不易, 如果本文对你有所帮助, 请给我个 star
来源: https://www.cnblogs.com/bfgis/p/9805928.html