Skip to content

使用 Sortable.js、Dragula 和 Gridstack.js 实现可拖拽表格

当涉及到实现可拖拽的表格时,Sortable.js、Dragula 和 Gridstack.js 是三个常用的 JavaScript 库。

下面是它们的详细介绍:

  1. Sortable.js:

    • Sortable.js 是一个简单易用的库,用于实现拖拽排序和重新排列表格行。
    • 它支持在同一表格内进行行的拖拽排序,也可以实现不同表格之间的拖拽操作。
    • Sortable.js 具有许多配置选项,例如设置拖拽手柄、限制拖拽的方向、自定义排序规则等。
    • 它还提供了一组事件回调函数,使你可以在拖拽过程中处理自定义逻辑。
    • Sortable.js 的文档清晰明了,提供了大量的示例代码和演示效果,可在其官方文档(https://sortablejs.github.io/Sortable/)中找到。
  2. Dragula:

    • Dragula 是一个轻量级的库,专门用于在多个容器之间实现拖拽操作。
    • 它可以很容易地应用于表格中的行拖拽,可以将行从一个表格拖动到另一个表格,并在不同表格之间重新排序。
    • Dragula 支持多个容器之间的复杂拖拽配置,并提供了丰富的事件回调函数,使你可以处理拖拽的各个阶段。
    • 与 Sortable.js 不同,Dragula 不提供内置的排序功能,而是专注于拖拽操作。
    • 更多关于 Dragula 的信息和示例可以在其 GitHub 页面(https://github.com/bevacqua/dragula)中找到。
  3. Gridstack.js:

    • Gridstack.js 是一个功能强大的库,用于创建可拖拽和可调整大小的网格布局。
    • 它适用于构建仪表盘、网格系统和栅格布局,非常适合用于表格布局。
    • Gridstack.js 具有灵活的配置选项,可自定义网格大小、调整大小的方式和限制等。
    • 它还支持拖拽的可视化布局管理,允许用户自由调整和重新排列表格中的单元格。
    • Gridstack.js 提供了丰富的 API 和事件,使你可以与表格中的元素进行交互。
    • Gridstack.js 的官方文档(https://gridstackjs.com/)提供了全面的介绍和示例。

这些库各自适用于不同的应用场景,根据你的具体需求选择最适合的库。它们都有良好的文档和示例,可以帮助你更好地理解和使用它们。

Sortable.js 集合 Vue3 element-plus el-table 的使用

1. 效果图

sortablejs-el-table.gif

2. 代码实现

https://github.com/xieerduos/electron-ffmpeg-video-to-gif/commit/b97fbb1c6f96af847fa438c625fb648731c4fad7

  • (1) 安装 sortablejs
bash
npm i sortablejs --save
  • (2) 在组件中引入并使用
vue
<template>
  <el-table
    ref="tableRef"
    :data="tableData"
    :row-key="(row) => row.id"
    @row-contextmenu="onRowClick"
    v-bind="$attrs"
    v-on="$attrs"
    height="calc(100vh - 160px - 40px)"
    style="width: 100%">
    <el-table-column type="selection" reserve-selection width="55" align="center" />
    <el-table-column type="index" :index="calculateIndex" label="序号" width="55" align="center" />
    <el-table-column prop="startTime" label="日期" width="114px">
      <template #default="scope">
        {{ scope.row.startTime ? dayjs(scope.row.startTime).format('YYYY/MM/DD HH:mm:ss') : '' }}
      </template>
    </el-table-column>
    <el-table-column prop="id" label="任务ID" width="70" align="center" />
    <el-table-column prop="name" label="文件名称">
      <template #default="scope">
        <span class="file-name" @click="handleShowRowFolder(scope.row.path)" :title="scope.row.path">{{
          scope.row.name
        }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="size" label="大小" width="100px">
      <template #default="scope">
        {{ bytes(scope.row.size) }}
      </template>
    </el-table-column>
    <el-table-column prop="status" label="状态" align="center" width="100px">
      <template #default="scope">
        <el-tag class="disabled-transitions" :type="MAP_STATUS.get(scope.row.status).tag">{{
          MAP_STATUS.get(scope.row.status).text
        }}</el-tag>
      </template>
    </el-table-column>
    <el-table-column prop="progress" label="完成进度" align="left">
      <template #default="scope">
        <div class="progress-wrap">
          <el-progress :percentage="scope.row.progress" :format="(percentage) => percentage.toFixed(1) + '%'" />
        </div>
      </template>
    </el-table-column>
    <el-table-column prop="" label="操作" align="center" width="100px">
      <template #default="scope">
        <el-button
          @click="handleResultFolder(scope.row)"
          :disabled="scope.row.status !== 2"
          text
          :title="scope.row.path"
          ><el-icon><Folder /></el-icon
        ></el-button>
      </template>
    </el-table-column>
  </el-table>
  <p>
    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :page-sizes="[5, 10, 20, 30]"
      :total="total"
      background
      layout="total, sizes, prev, pager, next, jumper"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange" />
  </p>
</template>
<script setup>
import {onMounted, defineExpose, computed, ref} from 'vue';
import dayjs from 'dayjs';
import useElectron from '@/renderer/index/composables/useElectron.js';
import {MAP_STATUS} from '@/renderer/index/utils/constant.js';
import bytes from 'bytes';
import Sortable from 'sortablejs';
const {handleResultFolder, handleShowRowFolder} = useElectron();

const props = defineProps({
  data: {type: Array, required: true, default: () => []},
  total: {type: Number, required: true, default: 0}
});

const defaultCurrentPage = 1;
const defaultPageSize = Number(localStorage.getItem('pageSize') || 10);

const currentPage = ref(Number.isNaN(defaultCurrentPage) ? 1 : defaultCurrentPage);
const pageSize = ref(Number.isNaN(defaultPageSize) ? 10 : defaultPageSize);

const tableData = computed(() => {
  const startIndex = (currentPage.value - 1) * pageSize.value;
  const endIndex = startIndex + pageSize.value;
  return props.data.slice(startIndex, endIndex);
});

const calculateIndex = (index) => {
  const startIndex = (currentPage.value - 1) * pageSize.value;
  return startIndex + index + 1;
};

const handleSizeChange = (val) => {
  localStorage.setItem('pageSize', pageSize.value);
};
const handleCurrentChange = (val) => {
  localStorage.setItem('pageSize', pageSize.value);
};

const onRowClick = (row) => {
  console.log('[onRowClick]', JSON.parse(JSON.stringify(row)));
};

const tableRef = ref();

onMounted(() => {
  Sortable.create(tableRef.value.$el.querySelector('.el-table__body-wrapper tbody'), {
    animation: 150,
    onEnd: ({newIndex, oldIndex}) => {
      const currRow = tableData.value.splice(oldIndex, 1)[0];
      tableData.value.splice(newIndex, 0, currRow);
    }
  });
});

defineExpose({
  getTableRef: () => tableRef.value
});
</script>
<style lang="scss" scoped>
.disabled-transitions {
  transition: none !important;
}

.file-name {
  cursor: pointer;
}
.progress-wrap ::v-deep(.el-progress__text) {
  width: 60px;
  font-size: 12px;
}
</style>

实现拖拽列排序代码

js
onMounted(() => {
  Sortable.create(
    tableRef.value.$el.querySelector(".el-table__header-wrapper thead tr"),
    {
      animation: 150,
      onMove: () => {},
      onUpdate: () => {},
      onSort: () => {},
      onEnd: ({ newIndex, oldIndex }) => {
        // 获取表格列定义
        const table = tableRef.value;
        const oldColumns = table.store.states.columns;

        // 重新排列列定义的顺序
        const newColumns = [...oldColumns.value];
        const movedColumn = newColumns.splice(oldIndex, 1)[0];
        newColumns.splice(newIndex, 0, movedColumn);

        oldColumns.value = newColumns;
      },
    }
  );
});

最后加上销毁 Sortablejs 创建的实例

js
const tableRef = ref();
const sortableInstanceRow = ref(null);
const sortableInstanceColumn = ref(null);

onMounted(() => {
  sortableInstanceRow.value = Sortable.create(
    tableRef.value.$el.querySelector(".el-table__body-wrapper tbody"),
    {
      animation: 150,
      onEnd: ({ newIndex, oldIndex }) => {
        const currRow = tableData.value.splice(oldIndex, 1)[0];
        tableData.value.splice(newIndex, 0, currRow);
      },
    }
  );
  sortableInstanceRow.value = Sortable.create(
    tableRef.value.$el.querySelector(".el-table__header-wrapper thead tr"),
    {
      animation: 150,
      onMove: () => {},
      onUpdate: () => {},
      onSort: () => {},
      onEnd: ({ newIndex, oldIndex }) => {
        // 获取表格列定义
        const table = tableRef.value;
        const oldColumns = table.store.states.columns;

        // 重新排列列定义的顺序
        const newColumns = [...oldColumns.value];
        const movedColumn = newColumns.splice(oldIndex, 1)[0];
        newColumns.splice(newIndex, 0, movedColumn);

        oldColumns.value = newColumns;
      },
    }
  );
});

onBeforeUnmount(() => {
  if (sortableInstanceRow.value) {
    sortableInstanceRow.value.destroy();
    sortableInstanceRow.value = null;
  }
  if (sortableInstanceColumn.value) {
    sortableInstanceColumn.value.destroy();
    sortableInstanceColumn.value = null;
  }
});