引子
正好遇到同事对编辑用的系统提出了优化需求
希望不要用上下移动的按钮对表格数据进行修改顺序 改成拖拽形式
我说ok
可能有人想为啥不用vxe-table 它有插件可以拖拽 里面不是涵盖树型和普通表格吗,
但是vxe-table的树型结构和传统的不同 数据源是扁平化后的数据源 我们原本的数据源 树形结构转成那个结构后 还要做一些额外的操作
那我们改造开始 涵盖了以下三种情况
1.自定义的列表 不借助任何组件编写的 使用vuedraggable 不需要额外编写事件与其他代码
2.使用了el-table的普通列表 使用sortablejs
3.使用了el-table的树型列表 使用sortablejs
安装依赖
npm i vuedraggable -S
npm i element-ui -S
npm i sortablejs -S
ElementUI导入
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
new Vue({
el: '#app',
render: h => h(App)
});
1.自定义列表拖拽实现
实现一个自定义元素列表拖拽超简单版本
界面代码
<el-tab-pane
label="vuedraggable"
name="1"
>
<div class="flex">
<div class="demo">
<div class="title">1.列表-自定义元素拖拽</div>
<ul>
<draggable
v-model="list"
handle=".el-icon-menu"
>
<li
v-for="item in list"
class="item"
>
<span
class="el-icon-menu"
style="cursor:pointer"
/>
<span>{{ item }}</span>
</li>
<el-button slot="footer">底部插槽footer</el-button>
<el-button slot="header">头部插槽header</el-button>
</draggable>
</ul>
</div>
<div class="introduce">
<el-table
:data="attrs"
border
:header-cell-style="{background:'#eee'}"
>
<el-table-column
label="数据字段"
prop="name"
width="200px"
></el-table-column>
<el-table-column
label="数据类型"
prop="type"
width="100px"
></el-table-column>
<el-table-column
label="解释"
prop="detail"
></el-table-column>
</el-table>
</div>
</div>
<div class="flex">
<div class="demo">
<div class="title">2.列表-使用指定元素渲染的拖拽 如ElementUI el-collapse</div>
<draggable
tag="el-collapse"
:list="list"
:component-data="getComponentData()"
>
<el-collapse-item
v-for="item in list"
:title="item"
:name="item"
:key="item"
>
<div>内容</div>
</el-collapse-item>
</draggable>
</div>
</div>
</el-tab-pane>
脚本代码
import draggable from "vuedraggable";
export default {
components: { draggable },
data() {
return {
// 数据源
list: ["1", "2", "3", "4", "5"],
// el-collapse
activeNames: "1",
// draggable属性列表
attrs: [
{
name: "v-model",
type: "Array",
detail: "绑定值",
},
{
name: "handle",
type: "String",
detail: "指定元素渲染的拖拽",
},
{
name: "tag",
type: "String",
detail: "渲染的标签的名称",
},
{
name: "component-data",
type: "Object",
detail: "组件数据",
},
{
name: "sort",
type: "Boolean",
detail:
"是否允许在当前列表内进行排序。值为false表示不允许在当前列表内排序,只能进行拖拽操作。",
},
{
name: "group",
type: "Object",
detail:
"定义拖拽组,用于实现列表之间的拖拽交互。name:组的名称,用于标识拖拽组。pull:定义是否允许从当前列表中拖拽元素。值为'clone'表示拖拽时会复制元素,而不是移动原元素。put:定义是否允许将其他列表的元素拖拽到当前列表。值为false表示不允许。",
},
{
name: "move,start, add, remove, update, end, choose, unchoose, sort, filter, clone",
type: "Function",
detail:
"事件名称 内置事件参数请看sortablejs文档 或者地址 https://www.npmjs.com/package/vuedraggable",
},
],
};
},
methods: {
// 写入组件数据
getComponentData() {
return {
on: {
// 事件
// change: this.handleChange,
// input: this.inputChanged,
},
props: {
value: this.activeNames,
},
};
},
},
};
2.通过sortablejs实现el-table的行列拖拽
界面代码
<el-tab-pane
label="使用sortablejs实现el-table拖拽"
name="2"
>
<div class="title">1.当我们使用某个表格组件 如el-table 但是它不存在拖拽功能的时候 就要通过sortablejs实现</div>
<div class="title">2.实现行列拖拽</div>
<el-table
:data="list"
class="edit-table"
v-if="refresh"
highlight-current-row
:header-cell-style="{background:'#eee'}"
>
<el-table-column
:label="item"
#default="{row}"
v-for="item in headers"
>
{{item}}-{{row}}
</el-table-column>
</el-table>
<div class="title">数据源:{{list}}</div>
<div class="title">表头源:{{headers}}</div>
</el-tab-pane>
脚本代码
import Sortable from "sortablejs";
export default {
data() {
return {
list: ["1", "2", "3", "4", "5"],
refresh: true,
headers: ["列1", "列2", "列3"],
};
},
methods: {
// 行的拖拽
rowDrop() {
// 最重要的一步 要知道哪个dom为作为创建sortable的容器
const tbody = document.querySelector(
".edit-table .el-table__body-wrapper tbody"
);
//创建新实例
Sortable.create(tbody, {
// 因为只是普通的列表拖拽 我们只需要在结束拖拽的时候做数据源的梳理
onEnd: ({ newIndex, oldIndex }) => {
// 删除当前行,放到拖拽后的位置
const currentRow = this.list.splice(oldIndex, 1)[0];
this.list.splice(newIndex, 0, currentRow);
// 刷新表格 并重置事件
// 刷新后dom会重新渲染 所以要重新创建sortable实例
// 并且保证拖拽后的表格顺序与数据源数据顺序正确的
// 不加上这一步 拖拽后的 数据源是符合预期的 但是渲染的表格顺序会错乱
this.refreshTable(() => {
this.rowDrop();
this.columnDrop();
});
},
});
},
// 列的拖拽
columnDrop() {
// 这里的定位和行的拖拽一样 都是要知道哪个dom为作为创建sortable的容器
// 为什么要定位到tr 详情见图
const thead = document.querySelector(
".edit-table .el-table__header .has-gutter tr"
);
//创建新实例
Sortable.create(thead, {
// 结束拖拽
onEnd: ({ newIndex, oldIndex }) => {
// 删除当前行,放到拖拽后的位置 更新标头位置
const currentRow = this.headers.splice(oldIndex, 1)[0];
this.headers.splice(newIndex, 0, currentRow);
// 刷新表格 并重置事件
this.refreshTable(() => {
this.rowDrop();
this.columnDrop();
});
},
});
},
refreshTable(callback) {
this.refresh = false;
this.$nextTick(() => {
this.refresh = true;
this.$nextTick(() => {
callback && callback();
});
});
},
},
mounted() {
this.rowDrop();
this.columnDrop();
},
};
标注列的拖拽 作为sortablejs的容器的dom
3.el-table树型表格的拖拽
界面代码
<el-tab-pane
label="el-table 树型拖拽"
name="3"
>
<div class="title">1.为什么不用el-tree 因为没有表头 自己画个表头多麻烦</div>
<div class="title">2.刚好遇上需要这样操作的树形表格</div>
<el-table
:data="tree"
row-key="id"
:row-class-name="taskRowClass"
class="tree-table"
ref="tree"
highlight-current-row
v-if="refresh"
:expand-row-keys="expand"
@expand-change="expandChange"
:header-cell-style="{background:'#eee'}"
>
<el-table-column
label="名称"
prop="name"
#default="{row}"
>
{{row.name}}
</el-table-column>
</el-table>
</el-tab-pane>
脚本代码
import Sortable from "sortablejs";
export default {
data() {
return {
refresh: true,
// 收集展开的key
expand: [],
// 树型结构
tree: [
{
id: 1,
name: "1",
children: [
{ id: 11, name: "11", children: [{ id: 111, name: "111" }] },
{ id: 12, name: "12" },
],
},
{ id: 2, name: "2", children: [{ id: 21, name: "21" }] },
{ id: 3, name: "3", children: [{ id: 31, name: "31" }] },
{ id: 4, name: "4", children: [{ id: 41, name: "41" }] },
{ id: 5, name: "5", children: [{ id: 51, name: "51" }] },
],
// 最后经过的节点
last: "",
};
},
methods: {
// 标识当前的id 方便后续直接从dom上取到当前拖拽节点的id
taskRowClass({ row, rowIndex }) {
return `index_${row.id}`;
},
// 记录展开情况
expandChange(expandedRows, expand) {
if (expand) {
this.expand.push(expandedRows);
} else {
this.expand = this.expand.filter((x) => x != expandedRows);
}
},
// 扁平化树
flatTree(tree) {
let dic = {};
const handler = (array, father) => {
for (let item of array) {
dic[item.id] = { ...item, father };
item && item.children && handler(item.children, item.id);
}
};
handler(tree, null);
return dic;
},
// 获取当前节点所在的表格
getCurrentTable(id) {
let dic = this.flatTree(this.tree);
return dic[id].father ? dic[dic[id].father].children : this.tree;
},
refreshTable(callback) {
this.refresh = false;
this.$nextTick(() => {
this.refresh = true;
this.$nextTick(() => {
callback && callback();
});
});
},
// 树的拖拽
treeDrop() {
// 初始化sortable
// 树型的拖拽配置复杂一些
// 以下编写了两套拖拽逻辑 各级交换存在一个 还需要实现的情况
// 树型 双方无子集的时候还可以同级变子集 但是还需要判断是交换位置还是变子集
// 不过我们的需求是 同级交换 所以懒得写了
const tbody = document.querySelector(
".tree-table .el-table__body-wrapper tbody"
);
Sortable.create(tbody, {
// 设置拖拽手柄
draggable: ".tree-table .el-table__row",
// 将cloned DOM 元素挂到body元素上。
fallbackOnBody: true,
onStart: () => {
// 清空上次经过的id
this.last = "";
},
onMove: ({ dragged, related }) => {
// 仅允许同级交换
// let d_id = [...dragged.classList]
// .find((x) => x.includes("index_"))
// .replace("index_", "");
// let r_id = [...related.classList]
// .find((x) => x.includes("index_"))
// .replace("index_", "");
// let d_f = this.getCurrentTable(d_id);
// let r_f = this.getCurrentTable(r_id);
// // 记录最后一次经过的对象的id
// this.last = r_id;
// return d_f == r_f;
// 各级交换
let r_id = [...related.classList]
.find((x) => x.includes("index_"))
.replace("index_", "");
this.last = r_id;
return true;
},
onEnd: ({ item }) => {
// 树拖动时的索引是错误的 所以 拿里面的oldIndex 和 newIndex 去计算是无效的
let d_id = [...item.classList]
.find((x) => x.includes("index_"))
.replace("index_", "");
let tasks = this.getCurrentTable(d_id);
// 同级交换
// let oldIndex = tasks.findIndex((x) => x.id == d_id);
// let newIndex = tasks.findIndex((x) => x.id == this.last);
// if (oldIndex == -1 || newIndex == -1) return;
// if (oldIndex == newIndex) return;
// const currRow = tasks.splice(oldIndex, 1)[0];
// if (!currRow) return;
// tasks.splice(newIndex, 0, currRow);
// 各级交换
let target = this.getCurrentTable(this.last);
let newIndex = target.findIndex((x) => x.id == this.last);
let oldIndex = tasks.findIndex((x) => x.id == d_id);
if (target == tasks && oldIndex == newIndex) return;
// 先删除当前行
const currRow = tasks.splice(oldIndex, 1)[0];
if (!currRow) return;
// 再插入到新位置
target.splice(newIndex, 0, currRow);
// 重新渲染树表格并展开原来展开过的
this.refreshTable(() => {
for (let item of this.expand) {
this.$refs.tree.toggleRowExpansion(item, true);
}
this.treeDrop();
});
},
});
},
},
mounted() {
this.treeDrop();
},
};
以上这就是三种不同类型列表实现拖拽功能的代码