表格可视化拖拽排序

963 阅读3分钟

我正在参与掘金创作者训练营第6期,点击了解活动详情

前言

技术永无止尽,多看看不同风景

上周利用antv实现了可视化图表,这周又回归到了pc端UI组件库和移动端框架维护,这不,业务部门又有需求了,希望能提供一个表格拖拽排序组件,支持上移下移置顶置底以及多项拖拽排序,有需求就安排上,花了点时间进行调研,因为团队是以Vue技术栈为主,所以比较适合的有原生的sortablejs,Vue进行封装的vuedraggable,最终的选择sortablejs

Why not vuedraggable

可能有人会问,既然用了Vue,并且有对于sortablejs做进一层封装的vuedraggable,为什么还是使用sortablejs,对于有一点思考能力,乐于钻研技术的程序员,每一个选择都是深思熟虑的,有其原因,这里也不例外,我最初的选择也是vuedraggable,不过在使用过程中遇到点问题,这个库对于框架的table不怎么友好,先说一下这个库的使用方式吧,这里是参照官方示例实现。

<template>
    <draggable v-model="list" tag="tbody">                                            
        <tr v-for="item in list" :key="item.name"> 
            <td scope="row">{{ item.id }}</td>         
            <td>{{ item.name }}</td>                   
            <td>{{ item.sport }}</td>                  
        </tr>                                      
    </draggable>
<template>
<script>
import draggable from 'vuedraggable'
export default {
    components: { 
        draggable
    }
}
</script>

这个库本质是一个Vue组件,有个弊端,只能拖拽draggable组件包裹的子级,当然也只能是子级,没办法跨级,一开始我也不相信为啥是这样子,所以我就去看一下它的源码实现,来解决我的疑问,直到我在源码中看到这样一句话,决定了它只能拖拽相邻子级,从示例中可以看出,我想要拖拽table的行,就需要将当前组件替代成tbody作为拖拽的外壳,但是正常的前端UI组件库,不管是ElementUI,还是ant-design-vue对于表格的封装都会避免我们去操作tbody,这样子没办法根据顶级类型做跨级的拖拽排序,对于UI组件库,还是挺不友好的

image.png

准备

安装依赖

npm i sortablejs

常用参数文档

属性说明类型默认值
draggable允许拖拽的项目类名String-
handle拖拽句柄,按住才能拖动String-
animation排序动画的时间Number150
ghostClass当前拖拽项对应的类名,可以设置拖拽项高亮String-
chosenClass拖拽克隆项类名,设置拖拽克隆项样式String-
multiDrag是否多选Booleanfalse
selectedClass多选拖拽类名,multiDrag 为 true 时生效Stringsortable-selected
事件说明
onEnd拖拽结束时触发,用来处理改变后的数据,回调参数{oldIndex: 旧索引,newIndx: 新索引, oldIndicies:旧多选项,multiDrag 时生效,多选拖拽时有值 }

拖拽

sortablejs是很完善很优秀的拖拽排序库,实现一个简单的拖拽排序还是很简单的,需要注意的点:

  • 1.因为需要涉及到dom的获取于绑定,要在moutend生命周期时才进行初始化,
  • 官方示例没有体现关于数据处理,虽然页面实现对应排序,但是对应的数据没有发生相应的变化,需要我们手动处理,处理也比较简单,在监听拖拽排序结束时事件onEnd,根据其提供的回调参数新旧索引oldIndexnewIndex做相应的数组操作就可以了。
<template>
  <div class="draggable">
    <el-table ref="table" :data="list" stripe style="width: 100%">
      <el-table-column prop="order" label="排序"></el-table-column>
      <el-table-column prop="name" label="姓名"></el-table-column>
      <el-table-column prop="age" label="年龄"></el-table-column>
      <el-table-column width="200" label="操作" align="center">
        <template #default="{ $index }">
          <div class="list">
            <div class="list-item handle" title="拖动">
              <img src="./components/icons/move.png" alt="" />
            </div>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
import Sortable from "sortablejs";
export default {
  name: "Draggable",
  data() {
    return {
      list: [
        { id: "1", order: 1, name: "小明1", age: 18 },
        { id: "2", order: 2, name: "小明2", age: 28 },
        { id: "3", order: 3, name: "小明3", age: 33 },
        { id: "4", order: 4, name: "小明4", age: 44 },
        { id: "5", order: 5, name: "小明5", age: 55 },
        { id: "6", order: 6, name: "小明6", age: 66 },
      ],
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      const ele = document.querySelector(".draggable tbody");
      // eslint-disable-next-line no-unused-vars
      const sortable = new Sortable(ele, {
        handle: ".handle",
        draggable: ".el-table__row",
        ghostClass: "draggable-ghost",
        onEnd: ({ oldIndex, newIndex }) => {
          const { list } = this;
          const item = list.splice(oldIndex, 1)[0];
          list.splice(newIndex, 0, item);
        },
      });
    },
  },
};
</script>

image.png

多项拖拽

对于多项拖拽,sortablejs的文档讲的其实不是很浅显易懂,需要做一下分析,不过其实增加了两步操作。

  • 引入MultiDrag,并做相应绑定
import { Sortable, MultiDrag } from 'sortablejs';

Sortable.mount(new MultiDrag());

image.png

  • multiDrag属性设置为true,设置selectedClass相应类名

image.png

完整示例的代码量比较多,具体可以查看Github

置顶置底

对于置顶置底,可能有人会惯性思维,拖拽排序,所有的排序操作都需要依赖于sortablejs,这样子就把事情复杂化了,其实置顶置底很简单的,简单几行代码就能实现,首先判断做一下边界条件判断,第一项不需要置顶,最后一项不需要置底置顶只需要剪切当前项,利用数组方法unshift插入到第一项,置底只需要剪切当前项,利用数组方法push插入到最后一项就可以了。

// 置顶
setTop(index) {
  // 第一行,不需要改变
  if (index === 0) return;
  const { tableData } = this;
  tableData.unshift(tableData.splice(index, 1)[0]);
  this.handleEmit(tableData);
},
// 置底
setBottom(index) {
  const { tableData } = this;
  // 最后一行,不需要改变
  if (index === tableData.length - 1) return;
  tableData.push(tableData.splice(index, 1)[0]);
  this.handleEmit(tableData);
},

上下移动

上移下移比较一致,本质都是删除当前项,然后插入到新索引位置,下移是删除当前项,插入到后一位索引,上移是删除当前项,插入到前一位索引,但是要添加边界判断,第一项不需要上移,最后一项不需要下移,插入一项到数组中,可以使用splice数组API。

// 上移
moveUp(index) {
  // 第一行,不需要改变
  if (index === 0) return;
  const { tableData } = this;
  tableData[index] = tableData.splice(index - 1, 1, tableData[index])[0];
  this.handleEmit(tableData);
},
// 下移
moveDown(index) {
  const { tableData } = this;
  // 最后一行,不需要改变
  if (index === tableData.length - 1) return;
  tableData[index] = tableData.splice(index + 1, 1, tableData[index])[0];
  this.handleEmit(tableData);
},

多项置顶置底

多项操作,这些项的选择可能是连续的,也可能是隔开的,所以处理起来比较麻烦,养成好习惯,先做边界判断,判断是否选中行,没有选中的话,什么也不需要操作,至于置顶置底,不去考虑用户怎么选择,连续选择,还是跨行选择,拿到选中行和完整表格数做对比,计算出差集,置顶则选择项在差集前面插入,置底则选择项在差集后面插入,这样子逻辑清晰又高效的完成了多项选择的置顶置底。

// 获取表格选中项
handleSelect() {
  return this.selectList;
},
// 判断是否选中行
hasSelectRow() {
  const list = this.handleSelect();
  if (!list.length) {
    this.$message.error("请先选择行!");
  }
  return list.length > 0;
},
// 处理差集
handleDiff(list, selectList) {
  return [...list].filter((x) =>
    [...selectList].every((y) => y.id !== x.id)
  );
},
// 置顶
setMultiTop() {
  // 未选择行
  if (!this.hasSelectRow()) return;
  const { tableData } = this;
  const selectList = this.handleSelect();
  const diffList = this.handleDiff(tableData, selectList);
  this.handleEmit([...selectList, ...diffList]);
},
// 置底
setMultiBottom() {
  // 未选择行
  if (!this.hasSelectRow()) return;
  const { tableData } = this;
  const selectList = this.handleSelect();
  const diffList = this.handleDiff(tableData, selectList);
  this.handleEmit([...diffList, ...selectList]);
},

小结

文章对于每个核心点做了阐述,还是比较清晰易懂的,当然也提供了完整的示例代码,放在Github

这篇文章虽然是以Vue为示例,但是核心逻辑是不变的,Sortablejs初始化是一致的,上移下移置顶置底以及多项拖拽排序的核心逻辑还是基础JS逻辑,这里只是抛砖引玉,希望大家可以举一反三,有问题欢迎评论区讨论,共同成长,fighting~