elementplus的表格组件默认是没有拖拽功能的,不过最近遇到一个拖拽排序的需求,就记录分享一下

一、引入 sortablejs

这个库可以完美处理dom拖拽

npm install sortablejs

二、封装通用的 Hook (useTableSort.js)

因为我许多表格要用到拖拽功能,所以封装一个通用的拖拽,这个 Hook 负责处理拖拽逻辑、数据重排以及生成发送给后端的 Payload

src/hookssrc/views/hooks 下新建 useTableSort.js

// src/views/hooks/useTableSort.js
import { onMounted, nextTick, onUnmounted } from 'vue'
import Sortable from 'sortablejs'
import { ElMessage } from 'element-plus'

/**
 * 表格拖拽排序通用 Hook
 * @param {Ref} tableRef Element Plus Table 的 ref 实例
 * @param {Ref<Array>} dataList 表格绑定的数据源 (v-model/ref)
 * @param {Function} apiFunc (可选) 后端排序接口函数,接收 List<SortDto>
 * @param {Object} options 配置项
 */
export function useTableSort(tableRef, dataList, apiFunc, options = {}) {
  let sortableInstance = null

  // 默认配置
  const defaultOptions = {
    handle: '.drag-handle', // 只有当鼠标按住带有这个类的元素时才能拖拽
    ghostClass: 'sortable-ghost', // 拖拽时的占位类名
    animation: 150, // 动画时间
    ...options
  }

  onMounted(() => {
    initSortable()
  })

  onUnmounted(() => {
    if (sortableInstance) {
      sortableInstance.destroy()
    }
  })

  const initSortable = () => {
    // 确保 DOM 已经渲染
    nextTick(() => {
      if (!tableRef.value) return

      // 找到 el-table 的 tbody 元素
      const el = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody')
      if (!el) return

      sortableInstance = Sortable.create(el, {
        ...defaultOptions,
        
        // 拖拽结束回调
        onEnd: async ({ newIndex, oldIndex }) => {
          if (newIndex === oldIndex) return

          // 1. 修改前端数据数组顺序
          // 注意:必须操作原数组,保持响应性
          const targetRow = dataList.value.splice(oldIndex, 1)[0]
          dataList.value.splice(newIndex, 0, targetRow)

          // 2. 重新计算所有行的 sort 值,并构建后端需要的 Payload
          const payload = []
          dataList.value.forEach((item, index) => {
            // 更新前端显示的 sort (如果需要立即更新UI上的序号)
            item.sort = index + 1
            
            // 构建发送给后端的对象 {id: 1, sort: 1}
            // 注意:这里要做一个过滤,只有真实存在的 ID (非临时ID) 才发送给后端
            // 假设临时ID是字符串或带有 'temp-' 前缀,具体根据你的业务逻辑调整
            if (item.id && typeof item.id === 'number') {
              payload.push({
                id: item.id,
                sort: item.sort
              })
            }
          })

          // 3. 如果传递了 API 函数,则自动调用
          if (apiFunc && payload.length > 0) {
            try {
              const res = await apiFunc(payload)
              if (res.code === 1) {
                ElMessage.success('排序更新成功')
              } else {
                ElMessage.error(res.msg || '排序更新失败')
                // 失败时可能需要回滚数组顺序,这里暂略,视业务严格程度而定
              }
            } catch (error) {
              console.error(error)
              ElMessage.error('网络异常,排序同步失败')
            }
          }
        }
      })
    })
  }

  return {
    initSortable
  }
}

三、在业务页面中使用

这里我截取了核心代码参考

注意:

1.<el-table> 必须加 ref="tableRef", row-key="id"

2.给拖拽列加 class="drag-handle"

<template>
  <div class="voucher-entry-page">
    <el-card class="voucher-details-card" shadow="never">
      <el-table 
        ref="tableRef" 
        :data="voucherDetails" 
        border 
        style="width: 100%" 
        max-height="500" 
        show-summary
        :summary-method="getSummaryMethod" 
        v-compact 
        class="erp-compact-table"
        row-key="id"
      >
        <el-table-column label="序号" width="60" align="center">
          <template #default="{ $index }">
            <div class="drag-handle row-indexer">
              {{ $index + 1 }}
            </div>
          </template>
        </el-table-column>

        </el-table>
    </el-card>
  </div>
</template>

<script setup>

import { updateVoucherDetailsSort } from '@/api/accounting/voucher' // 导入 API
import { useTableSort } from '@/views/hooks/useTableSort' // 导入通用 Hook



const tableRef = ref(null)

//定义表格数据数组
const voucherDetails = ref([])


 使用 Hook
// 参数1: tableRef
// 参数2: 数据源
// 参数3: API 函数 (如果不传,只会改变前端顺序,不发请求)
useTableSort(tableRef, voucherDetails, async (sortedPayload) => {
  // 这里是一个回调,如果你想做特殊处理(比如只有在已记账状态下不能拖拽)
  // 可以在这里加判断,或者在 Hook 内部处理
  
  // 如果当前凭证还没有保存过(没有主表ID),可能不需要发后端请求,
  // 只需要前端排个序就行了。
  if (!isBooked.value && voucherForm.id) {
    return await updateVoucherDetailsSort(sortedPayload)
  }
  
  // 如果是新增模式(还没有保存到数据库),我们不需要调用后端API,
  // 因为数据只在前端存在,Hook 已经帮我们把 voucherDetails 的顺序换好了。
  return { code: 1 } // 模拟成功
})


</script>

<style scoped lang="scss">

//  添加拖拽时的样式
:deep(.sortable-ghost) {
  opacity: 0.8;
  color: #fff !important;
  background: #409eff !important;
}
// 优化序号列的显示
.row-indexer {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  // 禁止选中文字,提升拖拽体验
  user-select: none; 
}
</style>

四、后端接口这个也通用

1.封装SortDto接受前端参数

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SortDto {
    /**
     * id
     */
    private Integer id;
    /**
     * 新排序
     */
    private Integer sort;
}

2.controller

    /**
     * 批量更新会计凭证明细排序
     * 前端会传来包含{id,sort}的对象数组,按id和新sort更新排序
     */
    @PutMapping("/update-details-sort")
    public Result<String> updateVoucherDetailsSort(@RequestBody List<SortDto> sortList) {
        log.info("批量更新会计凭证明细排序,排序列表: {}", sortList);
        accountingVoucherService.updateVoucherDetailsSort(sortList);
        return Result.success(MessageSuccessConstant.UPDATE_SUCCESS);
    }

3.serviceImpl

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateVoucherDetailsSort(List<SortDto> sortList) {
        if (CollectionUtils.isEmpty(sortList)) {
            throw new BaseException(MessageErrorConstant.PARAM_VALIDATION_ERROR.getCode(), "排序列表不能为空");
        }

        // 验证所有记录是否存在,并构建更新对象列表
        List<AccountingVoucherDetails> updateList = new ArrayList<>();
        for (SortDto item : sortList) {
            if (item.getId() == null) {
                throw new BaseException(MessageErrorConstant.PARAM_VALIDATION_ERROR.getCode(), "明细ID不能为空");
            }
            if (item.getSort() == null) {
                throw new BaseException(MessageErrorConstant.PARAM_VALIDATION_ERROR.getCode(), "排序值不能为空");
            }

            // 验证明细是否存在
            AccountingVoucherDetails detail = accountingVoucherDetailsService.getById(item.getId());
            if (detail == null) {
                throw new BaseException(MessageErrorConstant.PARAM_VALIDATION_ERROR.getCode(), 
                        "明细ID " + item.getId() + " 对应的数据不存在");
            }

            // 只更新sort字段
            AccountingVoucherDetails updateDetail = new AccountingVoucherDetails();
            updateDetail.setId(item.getId());
            updateDetail.setSort(item.getSort());
            updateList.add(updateDetail);
        }

        // 批量更新sort字段
        boolean updated = accountingVoucherDetailsService.updateBatchById(updateList);
        if (!updated) {
            throw new BaseException(MessageErrorConstant.UPDATE_ERROR);
        }
    }

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐