自定义列表拖拽/el-table表格拖拽/el-table树型拖拽 超详细实现

引子

正好遇到同事对编辑用的系统提出了优化需求
希望不要用上下移动的按钮对表格数据进行修改顺序 改成拖拽形式
我说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();
  },
};

在这里插入图片描述
以上这就是三种不同类型列表实现拖拽功能的代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值