需求
最近要实现一个字段展示表格拖动排序的功能,类似于下图:
思路
当时需求澄清会议,一讲这个需求,我脑袋一啪,很快就想到,element-plus table 应该有内置的拖拽功能吧,毕竟也不算啥特殊需求。话不多说,直接上官网一查,是我多想了,这看来是个不常见的需求。那只能自己手搓了。。。
手搓第一步:想想怎么搓?
重点在于:拖动行到某一位置,拿到这一位置的标识,数据插入进这个位置
vueuse的拖拽hooks useDraggable 可以用;html5 drag能拖动行元素;mounsedown、mounsemove时间实现拖拽。
初步选型
- vueuse库 的 useDraggable 自定义 hooks。(第三种实现)
- html5 的新 api draggable 。
- JavaScript 原生 onomusedown onmouseup onmousemove 等。
分析及实现
useDraggable 自定义hooks 与 onmousedown ❌
如果使用 mousemove 等方法实现:
- 在表格每一行注册一个
mousedown
事件 - 在
mousedown
事件中监听鼠标的竖向移动位置 - 将拖动行采用绝对定位来跟随鼠标移动
- 获取鼠标移动到某一行的位置,拿到此位置信息 ❌
- 元素插入,数据请求
- 表格刷新
当我采用这种方式时,在第四步拖动一行数据,由于是绝对定位,所以不能通过mouseenter方法来拿到元素的信息,所以这一步卡住,这种方法暂时被我放弃。。。
dragable Api
如果使用 dragable 等方法实现:
- 在表格的每一行添加 draggable 属性为 true
- 拖动目标行
- 拖动时 使用drag 传递数据 拿到行数据信息 ✅
- 元素插入,数据请求
- 表格刷新
实现
- 在 template 模版中 定义mousedown方法,表示开始拖拽,并拿到记录拖拽元素标识信息
<el-table-column
header-align="center"
type="index"
align="center"
label=""
:width="60"
>
<template #default="{ row, $index }">
<el-space
:class="filedInfoClass['drag-table-item']"
@mousedown="dragHandle.dragStart(row, $index)" // 这是重点
>
<el-icon><drag-icon /></el-icon>
</el-space>
</template>
</el-table-column>
- 拖拽时采用原生js获取行维度数据,设置 effectAllowed = 'move' 表示每行都可以放置
- 拖动到每一行上时拿到行标识,并动态插入交换表格数据,vue通过 diff算法分析,dom变动实现拖动效果
- 放置时拿到拖动行id ,目标行 id 请求数据,动态刷新表格数据
// script
const dragHandle = {
// 开始拖拽
dragStart(row: ColumnModel, i: number) {
dragI.value = i; // 拖拽起点
// 采用原生 js 获取 第 i 行
const con = document.querySelector(
`.${filedInfoClass['field-info-container']} .row-class-${i}`
) as HTMLElement;
const tbody = document.querySelector(
`.${filedInfoClass['field-info-container']} tbody`
) as HTMLElement;
let tr = document.querySelectorAll(
`.${filedInfoClass['field-info-container']} tbody tr`
);
dragKey.value = row.gid; // 拖拽的元素标识
if (con) {
con.setAttribute('draggable', 'true');
con.ondragstart = (ev) => {
console.log('start', ev);
};
// 设置 effectAllowed = 'move' 表示每行都可以放置
tbody.ondragover = (ev) => {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move';
}
ev.preventDefault();
};
tbody.ondragenter = (ev) => {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move';
}
ev.preventDefault();
};
tr.forEach((el, n) => {
(el as HTMLElement).ondragover = debounce(
() => {
// dataSetDetail.value?.columnModelList 表格数据
const item = dataSetDetail.value?.columnModelList[
dragI.value
] as ColumnModel;
// n 移动到 第 n 个
// i 从 第 i 个
if (n > dragI.value) {
dataSetDetail.value?.columnModelList.splice(n + 1, 0, item);
dataSetDetail.value?.columnModelList.splice(dragI.value, 1);
} else if (n < dragI.value) {
const start = n - 1 < 0 ? 0 : n;
dataSetDetail.value?.columnModelList.splice(start, 0, item);
dataSetDetail.value?.columnModelList.splice(dragI.value + 1, 1);
}
dragI.value = n;
},
1000,
true
);
});
tr = document.querySelectorAll(
`.${filedInfoClass['field-info-container']} tbody tr`
);
tr.forEach((el) => {
(el as HTMLElement).ondragend = () => {
const columns = dataSetDetail.value?.columnModelList || [];
const beforeI =
dragI.value + 1 >= columns.length ? -1 : dragI.value + 1;
const columnId = columns[dragI.value].resourceId || '';
const beforeColumnId = columns[beforeI]?.resourceId || null;
// 数据请求
dragField({ beforeColumnId, columnId })
.then((res) => {
if (res) {
emits('update', true); // 更新数据
}
})
.catch((err) => {
emits('update', true); // 更新数据
ElMessage({
message: err.errMsg,
type: 'warning',
});
});
};
});
// 销毁事件
window.onmouseup = () => {
con.onmousemove = null;
tr.forEach((el) => {
(el as HTMLElement).ondragover = null;
(el as HTMLElement).ondragend = null;
});
};
}
},
};