返回

PrimeVue DataTable 行拖拽实现方案 (Vue.Draggable)

vue.js

PrimeVue DataTable 组件实现行拖拽功能

在基于 Vue.js 和 PrimeVue 的开发过程中,经常会遇到需要为表格(DataTable)组件增加行拖拽功能的需求,这让数据操作更灵活。遗憾的是,PrimeVue 的 DataTable 组件本身并不直接支持行的拖拽排序。但是借助第三方库,就能轻易实现此功能。

问题:如何在 PrimeVue 的 DataTable 中启用行拖拽功能

在 DataTable 中,通过v-for将数据呈现在表格中, 为了保持数据的准确性和高效,DataTable的每一行都是根据特定的dataKey 来确定的。
问题是在v-for的循环里使用了draggable, 会破坏这种数据关系。

解决方案

使用 Vue.Draggable.Next 和 Sortable.js

Vue.Draggable.Next 是一个流行的 Vue.js 库,它基于 Sortable.js,专门为 Vue.js 提供了拖拽组件的能力。由于 PrimeVue 的 DataTable 没有自带的功能,那么就可以通过这个组件去手动实现拖拽。

原理

利用 Vue.Draggable.Next组件包裹 DataTable 的每一行 ( template #body ), 利用:style绑定左侧填充和可扩展节点的图标,使其视觉上和原本的表格树几乎无差别。利用draggable 组件的@change事件处理节点移动后的重新排序等,可以方便地控制元素的拖拽和排序。在排序逻辑完成后,调用外部的保存函数保存节点数据。

操作步骤
  1. 安装依赖。 使用 npm 或 yarn 安装 vuedraggablesortablejs

    npm install vuedraggable@next sortablejs
    
  2. 引入组件。 在需要使用拖拽功能的组件中引入 vuedraggable

    import draggable from "vuedraggable";
    
  3. 修改 DataTable 结构。 使用 draggable 组件包装原本DataTable中的Element组件, 并配置相应选项。

    <template>
      <div class="card">
        <DataTable :value="visibleRows" dataKey="data.id" class="elements-list">
          <Column>
            <template #body="slotProps">
              <div
                :style="{
                  paddingLeft: slotProps.data.level * 20 + 'px',
                  display: 'flex',
                  alignItems: 'center',
                }"
                class="expandable-row"
              >
                <span
                  v-if="hasChildren(slotProps.data)"
                  class="expander-icon"
                  @click="toggleRow(slotProps.data)"
                >
                  <i
                    :class="[
                      'pi',
                      expandedRows[slotProps.data.data.id]
                        ? 'pi-chevron-down'
                        : 'pi-chevron-right',
                    ]"
                  ></i>
                </span>
                <span v-else style="width: 1em"></span>
                  <draggable 
                    v-model="slotProps.data.childElements"
                    :group="{ name: 'elements' }"
                    handle=".handle"
                    @change="onDragChange"
                    item-key="id">
                    <template #item="{element}">
                      <Element :element-id="element.id"/>
                    </template>
                  </draggable>
              </div>
            </template>
          </Column>
        </DataTable>
      </div>
    </template>
    

    在这里draggable被用来代替Element, 需要利用:style 使拖拽后各个层级的缩进与之前保持一致。需要添加 handle 属性用来确定哪个部分被点击才可以拖拽, 如果没有这个限制那么所有子组件都可被单独拖动。需要利用@change 来添加Element移动的逻辑。

  4. 处理拖拽事件。 监听 draggable 组件的事件,如 @change,以更新数据。

      import { ref, computed, watch, defineProps } from "vue";
      import draggable from "vuedraggable";
      import Element from "./Element.vue";
      import DataTable from "primevue/datatable";
      import Column from "primevue/column";
    
      const props = defineProps({
        elements: {
          type: Array as () => any[],
          required: true,
          default: () => [],
        },
        expanded: {
          type: Boolean,
          required: false,
          default: false,
        },
      });
      const saveNodes = () => {
          //API to save your changed nodes, eg:
          //put('/api/nodes', { nodes: props.elements });
      }
      const onDragChange = (event) => {
          //prevent event from multiple invocation
          if (event?.moved) {
              saveNodes();
          }
      };
    
      //other codes..
    

saveNodes 方法是一个外部方法, 需要将重新排序好的节点发送给服务器并保存, 例子中使用/api/nodes 来保存数据。
* 安全建议:务必校验后端数据正确性。另外要注意在API的返回里根据情况调用expandAllNodes 或者toggleRow 方法重新渲染节点状态, 以防节点状态被清空。

注意事项

利用draggable替代Element的方式将树形的表格完全变成了同级别之间的组件, 由于每个元素都可以拖动, 必须要使用handle指定只有某个class的子组件才能触发拖动, 如果不指定的话会导致Element 的每一个子组件都可以拖动。由于PrimeVue是自带DataTabledraggable功能的, 但是它和树状结构的DataTable 的底层逻辑差异较大, 不适宜改造。这种方案的主要思路是利用CSS 去模拟表格中的行为, 实际使用draggable 去拖动组件。需要注意:由于失去了DataTablev-for 逻辑的强关联, 如果在修改数据的操作外改变了原本数组顺序可能会引起BUG, 请尽量只在拖拽后触发修改数组顺序的逻辑, 避免复杂操作。