基于vxe-table+Sortable的树形表格行拖动

10/20/2023 Vue

# 背景

目前在公司负责的项目是低代码平台,有个需求是需要在工作流中配置工作表的字段权限并且可以拖动排序。在工作表编辑中拖拽配置控件使用的 sortablejs,表格渲染涉及到单元格编辑使用的 vxe-table,所以毫无疑问就是通过这两个去实现啦。

# 普通表格行拖动排序

拖动结束事件中有oldIndexnewIndex,所以就是两个位置上的元素互换一下就好了。 点击查看 vxe-table 官网 demo (opens new window)

# 那树形表格如何实现呢

官网也有 demo,但不符合我们的需求。只能在同一层级才允许相互拖动排序。所以想的就是在拖动事件中判断是否在同一层级,不在同一层级就不允许拖动。然后把树形数据转为列表再进行排序。

代码实现

const createTableSort = () => {
  const el = document.querySelector('.body--wrapper>.vxe-table--body tbody') as HTMLElement
  if (!el) return
  if (sortTable.value) return
  sortTable.value = Sortable.create(el, {
    disabled: false, // 是否开启拖拽
    animation: 150, // 拖拽延时,效果更好看
    handle: '.icon-move',
    sort: true,
    onMove: (evt: any) => {
      const { dragged, related } = evt

      const dragElClassList: string[] = []
      dragged.classList.forEach((element: string) => {
        if (element.includes('row--level')) {
          dragElClassList.push(element)
        }
      })

      const relatelClassList: string[] = []
      related.classList.forEach((element: string) => {
        if (element.includes('row--level')) {
          relatelClassList.push(element)
        }
      })
      // 不同层级的不能互相拖动
      // 这里采用比较笨的方法实现,大佬们指点一下有什么更好的实现
      // isEqual采用lodash的方法,判断两个值是否相等
      if (!isEqual(dragElClassList, relatelClassList)) return false
      return true
    },
    onEnd: async (evt: any) => {
      const { newIndex, oldIndex } = evt
      if (newIndex === oldIndex) return
      const expandRow = tableRef.value.getTreeExpandRecords()
      // 展开的数据id
      const expandId = expandRow.map((item: RowVO) => item.id)
      const newTable: any[] = []
      // 将多维数据展开存为一维数据
      const cloneData = tableData.value.map((o) => o)
      cloneData.forEach((item) => {
        // 如果有子类并且该树已经开展,将子类也push进去
        if (expandId.includes(item.id) && item.children && item.children.length > 0) {
          newTable.push({
            ...item,
            useChild: false
          })
          item.children.forEach((i) => {
            i.parentId = item.id
            newTable.push(i)
          })
        } else {
          newTable.push({
            ...item,
            useChild: true
          })
        }
      })
      const currRow = cloneDeep(newTable[oldIndex])
      newTable?.splice(oldIndex, 1)
      newTable?.splice(newIndex, 0, currRow)
      // 然后把排序成功后的一维数据转为树形数据
      const result: any[] = []
      newTable.forEach((item) => {
        // 一级
        if (!item.parentId) {
          // 设置子级
          if (!item.useChild) {
            item.children = newTable.filter((one) => one.parentId === item.id)
          }
          result.push(item)
        }
      })
      // 赋值到数据,可以传给后端
      tableData.value = result
      nextTick(() => {
        // 重新加载数据
        tableRef.value.reloadData(result)
        nextTick(() => {
            // 之前展开的数据重新展开
          tableRef.value.setTreeExpand(expandRow, true)
        })
      })
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

# 思考更多:如果树的层级有很深呢

层级很深的话呢...🤔 如果只拖动第三层,按照我上面的方法实现思路起来就很耗性能,每拖动一次就得来回转换数据,但其实我只要更换拖动那一层元素的顺序就好了。但是在拖动事件中我们只能拿到显示数据的索引,怎么和真实树形数据对应上呢?好像建立一个映射是不是可以呢,嗯,那就试试!

你们有更好的方法快来告诉我呀~~

查看演示效果 (opens new window)

查看完整代码 (opens new window)

// 主要是 onEnd 事件的逻辑有区别。
onEnd: (evt: any) => {
    const { newIndex, oldIndex } = evt
    if (newIndex === oldIndex) return
    const expandRow = tableRef.value.getTreeExpandRecords()
    // 展开的数据id
    const expandId = expandRow.map((item: RowVO) => item.id)
    // 建立映射
    const cloneData = tableData.value.map((o) => o)
    let count = -1
    const result: any = {}
    const getMap = (data: any[], parentIndex = '') => {
        data.forEach((item, index) => {
            // 如果有子类,将子类也push进去
            item.selfIndex = parentIndex === '' ? `${index}` : `${parentIndex}-${index}`
            result[++count] = item.selfIndex
            if (expandId.includes(item.id) && item.children && item.children.length > 0) {
                getMap(item.children, item.selfIndex)
            }
        })
    }
    getMap(cloneData)
    // 根据映射获取对应数据真正的索引
    const oldRealIndex = result[oldIndex]
    const newRealIndex = result[newIndex]
    if (oldRealIndex.length === 1 && newRealIndex.length === 1) {
        const currRow = cloneDeep(tableData.value[oldRealIndex])
        tableData.value?.splice(oldRealIndex, 1)
        tableData.value?.splice(newRealIndex, 0, currRow)
    } else {
        const parentIndex = oldRealIndex.slice(0, -2)
        const tempOldIndex = oldRealIndex.slice(oldRealIndex.length - 1)
        const tempNewIndex = newRealIndex.slice(oldRealIndex.length - 1)
        const getCurrent = (arr: RowVO[]) => {
            arr.forEach((item) => {
                if (item.selfIndex === parentIndex && item.children?.length) {
                    const currRow = item.children[tempOldIndex]
                    item.children?.splice(tempOldIndex, 1)
                    item.children?.splice(tempNewIndex, 0, currRow)
                }
                if (item.children?.length) {
                    getCurrent(item.children)
                }
            })
        }
        getCurrent(tableData.value)
    }
    console.log('tableData', tableData.value)
    // 赋值到数据,可以传给后端
    tableRef.value.reloadData(tableData.value)
    nextTick(() => {
        tableRef.value.setTreeExpand(expandRow, true)
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 相关文章

上次更新: 10/23/2023, 9:20:45 AM