当前位置: 首页 > ds >正文

vue3 + element plus -- table表格使用sortablejs实现表格拖拽换位功能

table表格实现表格拖拽换位功能

思考:
1、项目框架vue3 + element plus
2、使用sortablejs实现表格拖拽换位功能
3、表格列也的是动态的一个数组来保存

安装sortablejs

npm install sortablejs --save

使用

代码主要是通过个人思路提需求使用插件来生成的。行和列都可拖拽换位。

<template><div class="content"><h1>sortablejs实现表格拖拽</h1><div class="table"><el-tableref="boxRef":data="tableData"style="width: 100%"striperow-key="id"class="draggable-table":fit="true"border><el-table-columnv-for="item in tableList":key="item.type":prop="item.prop":label="item.label"show-overflow-tooltipmin-width="120":width="null"></el-table-column></el-table></div><div><el-button type="primary" @click="addRow">添加一行</el-button></div></div>
</template><script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue'
import Sortable from 'sortablejs';//sortable实例
const rowSortable:any = ref(null)
const columnSortable:any = ref(null)const tableList = reactive([{type: '1',label: '姓名',prop: 'name',},{type: '2',label: '日期',prop: 'date',},{type: '3',label: '地址',prop: 'address',},
])const tableData = reactive([{date: '2016-05-03',name: 'Tom1',id: '1',address: 'No. 189, Grove St, Los Angeles',},{date: '2016-05-02',name: 'Tom2',id: '2',address: 'No. 189, Grove St, Los Angeles',},{date: '2016-05-04',name: 'Tom3',id: '3',address: 'No. 189, Grove St, Los Angeles',},{date: '2016-05-01',name: 'Tom4',id: '4',address: 'No. 189, Grove St, Los Angeles',},
])// 添加一行数据
const addRow = () => {const newId = String(tableData.length + 1);tableData.push({date: `2023-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`,name: `User${newId}`,id: newId,address: `Address ${newId}, Some City`,});// 添加新行后重新初始化排序nextTick(() => {initRowSortable();initColumnSortable();});
}//获取容器dom元素
const boxRef:any = ref(null)// 初始化行排序功能
const initRowSortable = () => {const tbody = boxRef.value.$el.querySelector('.el-table__body-wrapper tbody');if (!tbody) return;if (rowSortable.value) {rowSortable.value.destroy();}rowSortable.value = Sortable.create(tbody, {animation: 150,ghostClass: 'sortable-ghost',onEnd: ({ newIndex, oldIndex }:{ newIndex: number; oldIndex: number}) => {if (newIndex === oldIndex) return;const currRow = tableData.splice(oldIndex, 1)[0];tableData.splice(newIndex, 0, currRow);}});
}// 初始化列排序功能
const initColumnSortable = () => {const headerRow = boxRef.value.$el.querySelector('.el-table__header-wrapper tr');if (!headerRow) return;if (columnSortable.value) {columnSortable.value.destroy();}columnSortable.value = Sortable.create(headerRow, {animation: 150,ghostClass: 'sortable-ghost',handle: '.cell',onEnd: ({ newIndex, oldIndex }:{ newIndex: number; oldIndex: number}) => {if (newIndex === oldIndex) return;const currColumn = tableList.splice(oldIndex, 1)[0];tableList.splice(newIndex, 0, currColumn);}});
}// 监听数据变化,重新初始化排序
watch([tableData, tableList], () => {nextTick(() => {initRowSortable();initColumnSortable();});
});onMounted(() => {nextTick(() => {initRowSortable();initColumnSortable();});
});onUnmounted(() => {if (rowSortable.value) {rowSortable.value.destroy();}if (columnSortable.value) {columnSortable.value.destroy();}
});
</script><style lang="scss">
.content {min-height: calc(100vh - 150px);.table {margin-bottom: 20px;}
}.draggable-table {position: relative;.el-table {width: 100% !important;table-layout: fixed !important;.el-table__cell {padding: 8px 12px !important;height: auto !important;line-height: 1.5;.cell {overflow: hidden;text-overflow: ellipsis;white-space: normal !important;word-break: break-word;}}}/* 表头样式 */.el-table__header-wrapper {th {position: relative;cursor: move;user-select: none;background-color: #f5f7fa;.cell {position: relative;}&:hover {background-color: #e6f1fc;&::after {content: "⋮⋮";position: absolute;right: 16px;top: 50%;transform: translateY(-50%) rotate(90deg);color: #409EFF;font-size: 14px;opacity: 0.8;z-index: 1;}}}}/* 表格行样式 */.el-table__row {position: relative;cursor: move;transition: all 0.2s;td:first-child {position: relative;}&:hover {background-color: #f5f7fa;td:first-child::before {content: "⋮⋮";position: absolute;left: -15px;top: 50%;transform: translateY(-50%) rotate(90deg);color: #409EFF;font-size: 14px;opacity: 0.8;z-index: 1;}}}.sortable-ghost {background-color: #e6f1fc !important;opacity: 0.8;}.el-table__body-wrapper {overflow-x: hidden !important;}
}
</style>

在这里插入图片描述

新要求左侧添加一个行的增加,在当前行的前面或者后面可选则

<template><div class="content"><h1>sortablejs实现表格拖拽</h1><div class="table"><el-tableref="boxRef":data="tableData"style="width: 100%"striperow-key="id"class="draggable-table":fit="true"border><!-- 添加操作列,不参与拖拽 --><el-table-columnwidth="50"label=""class-name="operation-column"fixed="left"><template #header><div class="non-draggable-header">操作</div></template><template #default="scope"><div class="add-row-icon" @click="showAddRowOptions(scope.row, scope.$index)"><i class="el-icon-plus">+</i></div></template></el-table-column><el-table-columnv-for="item in tableList":key="item.type":prop="item.prop":label="item.label"show-overflow-tooltipmin-width="120":width="null"></el-table-column></el-table></div><div><el-button type="primary" @click="addRow">添加一行</el-button></div></div>
</template><script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue'
import Sortable from 'sortablejs';
import { ElMessageBox } from 'element-plus';//sortable实例
const rowSortable:any = ref(null)
const columnSortable:any = ref(null)// 显示添加行选项
const showAddRowOptions = (row: any, index: number) => {ElMessageBox.confirm('请选择添加位置','添加行',{confirmButtonText: '在此行之前',cancelButtonText: '在此行之后',type: 'info',showClose: true,distinguishCancelAndClose: true,}).then(() => {// 在当前行之前添加addRowAt(index);}).catch((action) => {if (action === 'cancel') {// 在当前行之后添加addRowAt(index + 1);}});
};// 在指定位置添加行
const addRowAt = (index: number) => {const newId = String(Date.now()); // 使用时间戳作为唯一IDconst newRow = {date: `2023-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`,name: `User${newId.slice(-4)}`,id: newId,address: `Address ${newId.slice(-4)}, Some City`,};tableData.splice(index, 0, newRow);// 添加新行后重新初始化排序nextTick(() => {initRowSortable();});
};const tableList = reactive([{type: '1',label: '姓名',prop: 'name',},{type: '2',label: '日期',prop: 'date',},{type: '3',label: '地址',prop: 'address',},
])const tableData = reactive([{date: '2016-05-03',name: 'Tom1',id: '1',address: 'No. 189, Grove St, Los Angeles',},{date: '2016-05-02',name: 'Tom2',id: '2',address: 'No. 189, Grove St, Los Angeles',},{date: '2016-05-04',name: 'Tom3',id: '3',address: 'No. 189, Grove St, Los Angeles',},{date: '2016-05-01',name: 'Tom4',id: '4',address: 'No. 189, Grove St, Los Angeles',},
])// 添加一行数据
const addRow = () => {const newId = String(tableData.length + 1);tableData.push({date: `2023-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`,name: `User${newId}`,id: newId,address: `Address ${newId}, Some City`,});// 添加新行后重新初始化排序nextTick(() => {initRowSortable();initColumnSortable();});
}//获取容器dom元素
const boxRef:any = ref(null)// 初始化行排序功能
const initRowSortable = () => {const tbody = boxRef.value.$el.querySelector('.el-table__body-wrapper tbody');if (!tbody) return;if (rowSortable.value) {rowSortable.value.destroy();}rowSortable.value = Sortable.create(tbody, {animation: 150,ghostClass: 'sortable-ghost',onEnd: ({ newIndex, oldIndex }:{ newIndex: number; oldIndex: number}) => {if (newIndex === oldIndex) return;const currRow = tableData.splice(oldIndex, 1)[0];tableData.splice(newIndex, 0, currRow);}});
}// 初始化列排序功能
const initColumnSortable = () => {const headerRow = boxRef.value.$el.querySelector('.el-table__header-wrapper tr');if (!headerRow) return;if (columnSortable.value) {columnSortable.value.destroy();}// 获取所有表头单元格const headerCells = headerRow.querySelectorAll('th');// 确保第一个单元格不可拖动if (headerCells.length > 0) {headerCells[0].classList.add('operation-column-header');headerCells[0].setAttribute('data-no-sortable', 'true');}columnSortable.value = Sortable.create(headerRow, {animation: 150,ghostClass: 'sortable-ghost',handle: '.cell',filter: '.operation-column-header, .non-draggable-header', // 添加过滤器,防止操作列被拖动preventOnFilter: true, // 阻止在过滤的元素上触发拖动onStart: function(evt:any) {// 如果是第一列,取消拖动if (evt.oldIndex === 0) {this.options.disabled = true;setTimeout(() => {this.options.disabled = false;}, 0);return false;}},onMove: function(evt:any) {// 阻止拖动到第一列位置const targetIndex = evt.related ? Array.from(evt.related.parentNode.children).indexOf(evt.related) : 0;if (targetIndex === 0) {return false;}},onEnd: ({ newIndex, oldIndex }:{ newIndex: number; oldIndex: number}) => {if (newIndex === oldIndex || oldIndex === 0 || newIndex === 0) return;// 由于第一列是操作列,需要调整索引const adjustedNewIndex = newIndex > 0 ? newIndex - 1 : 0;const adjustedOldIndex = oldIndex > 0 ? oldIndex - 1 : 0;// 只有当操作的不是第一列时才进行排序if (oldIndex > 0) {const currColumn = tableList.splice(adjustedOldIndex, 1)[0];tableList.splice(adjustedNewIndex, 0, currColumn);}}});
}// 监听数据变化,重新初始化排序
watch([tableData, tableList], () => {nextTick(() => {initRowSortable();initColumnSortable();});
});onMounted(() => {nextTick(() => {initRowSortable();initColumnSortable();});
});onUnmounted(() => {if (rowSortable.value) {rowSortable.value.destroy();}if (columnSortable.value) {columnSortable.value.destroy();}
});
</script><style lang="scss">
.content {min-height: calc(100vh - 150px);.table {margin-bottom: 20px;}
}/* 操作列样式 */
.add-row-icon {cursor: pointer;// border: 1px solid #eee;i {font-size: 26px;color: #ccc;font-style: normal;}&:hover {i {color: #67c23a;}}
}/* 不可拖动的表头样式 */
.non-draggable-header {cursor: default !important;
}/* 确保操作列不显示拖动指示器 */
.el-table__header-wrapper th.operation-column-header,
.el-table__header-wrapper th:first-child {cursor: default !important;&:hover::after {display: none !important;content: none !important;}.cell {cursor: default !important;}
}/* 操作列样式 */
.operation-column {.cell {cursor: default !important;}
}.draggable-table {position: relative;.el-table {width: 100% !important;table-layout: fixed !important;.el-table__cell {padding: 8px 12px !important;height: auto !important;line-height: 1.5;.cell {overflow: hidden;text-overflow: ellipsis;white-space: normal !important;word-break: break-word;}}}/* 表头样式 */.el-table__header-wrapper {th {position: relative;cursor: move;user-select: none;background-color: #f5f7fa;.cell {position: relative;}&:hover {background-color: #e6f1fc;&::after {content: "⋮⋮";position: absolute;right: 16px;top: 50%;transform: translateY(-50%) rotate(90deg);color: #409EFF;font-size: 14px;opacity: 0.8;z-index: 1;}}}}/* 表格行样式 */.el-table__row {position: relative;cursor: move;transition: all 0.2s;td:first-child {position: relative;}&:hover {background-color: #f5f7fa;td:first-child::before {content: "⋮⋮";position: absolute;left: -15px;top: 50%;transform: translateY(-50%) rotate(90deg);color: #409EFF;font-size: 14px;opacity: 0.8;z-index: 1;}}}.sortable-ghost {background-color: #e6f1fc !important;opacity: 0.8;}.el-table__body-wrapper {overflow-x: hidden !important;}
}
</style>

在这里插入图片描述

Sortable.js 使用指南

目录

  • 简介
  • 安装
  • 基本用法
  • 配置选项
  • 事件回调
  • 常见示例
  • 最佳实践

简介

Sortable.js 是一个功能强大的 JavaScript 库,用于实现拖拽排序功能。它支持现代浏览器,不依赖 jQuery 或其他库,可以在任何 HTML 元素上创建可拖拽排序的列表。

主要特点

  • 支持触摸设备
  • 支持跨列表拖拽
  • 支持嵌套拖拽列表
  • 无依赖
  • 支持 Vue、React、Angular 等框架

安装

NPM 安装

npm install sortablejs

直接引入

<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>

基本用法

HTML 结构

<ul id="sortable"><li>Item 1</li><li>Item 2</li><li>Item 3</li>
</ul>

JavaScript 初始化

// 基本初始化
const el = document.getElementById('sortable');
const sortable = Sortable.create(el);// 带配置的初始化
const sortable = Sortable.create(el, {animation: 150,ghostClass: 'sortable-ghost'
});

配置选项

核心选项

选项类型默认值描述
groupString | Objectundefined用于跨列表拖拽的组名
sortBooleantrue是否允许列表内排序
disabledBooleanfalse是否禁用排序
animationNumber0动画过渡时间(毫秒)
handleStringnull拖拽把手选择器
filterString | Functionnull不可拖拽项的选择器或函数

外观选项

选项类型默认值描述
ghostClassStringnull拖动时克隆元素的类名
chosenClassStringnull选中元素的类名
dragClassStringnull拖动中元素的类名

行为选项

选项类型默认值描述
delayNumber0延迟拖动的时间(毫秒)
delayOnTouchOnlyBooleanfalse是否仅在触摸设备上使用延迟
preventOnFilterBooleantrue是否阻止被过滤元素的默认事件
touchStartThresholdNumber0触摸时移动多少像素才开始拖动

事件回调

主要事件

Sortable.create(el, {// 开始拖动onStart: function(evt) {const { item, from } = evt;},// 拖动中onMove: function(evt, originalEvent) {const { dragged, related, to } = evt;// 返回 false 可以阻止移动},// 改变列表onChange: function(evt) {const { newIndex, oldIndex } = evt;},// 添加到列表onAdd: function(evt) {const { item, from, to } = evt;},// 更新列表onUpdate: function(evt) {const { item, newIndex, oldIndex } = evt;},// 从列表移除onRemove: function(evt) {const { item, from, to } = evt;},// 拖动结束onEnd: function(evt) {const { item, from, to, newIndex, oldIndex } = evt;}
});

常见示例

基本列表排序

Sortable.create(document.getElementById('list'), {animation: 150,ghostClass: 'blue-background-class'
});

带把手的排序

Sortable.create(document.getElementById('list'), {handle: '.my-handle',animation: 150
});

跨列表拖拽

Sortable.create(document.getElementById('list1'), {group: 'shared',animation: 150
});Sortable.create(document.getElementById('list2'), {group: 'shared',animation: 150
});

禁止特定项目拖拽

Sortable.create(document.getElementById('list'), {filter: '.disabled',preventOnFilter: false
});

嵌套列表

Sortable.create(document.getElementById('nested'), {group: 'nested',animation: 150,fallbackOnBody: true,swapThreshold: 0.65
});

最佳实践

1. 性能优化

  • 使用 animation 时设置合适的动画时长
  • 大列表考虑使用虚拟滚动
  • 避免在拖动时进行复杂计算

2. 用户体验

  • 添加适当的视觉反馈(如 ghostClass
  • 使用 handle 明确可拖动区域
  • 设置合适的 delay 防止意外拖动

3. 错误处理

  • 在事件回调中添加错误处理
  • 使用 try/catch 包装关键操作
  • 保存重要数据的备份

4. 可访问性

  • 添加适当的 ARIA 属性
  • 确保键盘可访问性
  • 提供替代操作方式

5. 调试技巧

  • 使用 setDatagetData 跟踪数据变化
  • 利用浏览器开发工具监控事件
  • 添加日志记录关键操作

常见问题解决

1. 拖动不生效

  • 检查元素是否可见
  • 确认选择器是否正确
  • 验证 handlefilter 设置

2. 性能问题

  • 减少列表项数量
  • 优化事件处理器
  • 使用防抖/节流

3. 移动设备问题

  • 调整 touchStartThreshold
  • 设置适当的 delay
  • 确保触摸区域足够大

更多资源

  • 官方文档
  • API 参考
  • 示例集合
http://www.xdnf.cn/news/13365.html

相关文章:

  • 麒麟Kylin V10 SP3服务器操作系统安装
  • 项目进度管理软件是什么?项目进度管理软件有哪些核心功能?
  • LoRA(Low-Rank Adaptation,低秩适应)
  • leetCode- 两数相加
  • 【AI学习】一、向量表征(Vector Representation)
  • 报告精读:金融算力基础设施发展报告 2024【附全文阅读】
  • 构建欺诈事件的结构化威胁建模框架
  • Coze 和 Dify 对比
  • 销售心得分享
  • 保险风险预测数据集insurance.csv
  • vivado IP核High speed/Low latency设置对系统性能的影响
  • 深入浅出Diffusion模型:从原理到实践的全方位教程
  • 改进系列(13):基于改进U-ResNet的脊椎医学图像分割系统设计与实现
  • 游戏盾的功能是什么
  • 关于前端常用的部分公共方法(二)
  • 2.6 查看动态库或程序的依赖库
  • PH热榜 | 2025-06-06
  • 高保真组件库:上传
  • “深时数字地球”新进展!科学智能助推地球科学研究范式变革
  • if综合演练——石头剪刀布
  • 命令行关闭Windows防火墙
  • 网络爬虫解析技术与实战代码详解
  • 可编程光子处理器新范式:《APL Photonics》封面级成果展示多功能集成突破
  • 报文口令重写功能分析(以某巢为例)
  • 一款 AI 驱动的 Wiki 知识库
  • python中的闭包
  • 安装和使用G4F(GPT4Free) 最新0.5.3.2 版本
  • 算法从0到1 Day 17 二叉树part 06
  • Linux 关键目录解析:底层机制与技术细节
  • 从制造出海到智造全球,艾芬达如何拥抱工业互联网革命?