vue3+elementplus表格拖拽排序
·
elementplus的表格组件默认是没有拖拽功能的,不过最近遇到一个拖拽排序的需求,就记录分享一下
一、引入 sortablejs 库
这个库可以完美处理dom拖拽
npm install sortablejs
二、封装通用的 Hook (useTableSort.js)
因为我许多表格要用到拖拽功能,所以封装一个通用的拖拽,这个 Hook 负责处理拖拽逻辑、数据重排以及生成发送给后端的 Payload
在 src/hooks 或 src/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);
}
}
更多推荐


所有评论(0)