前言

今天,我们将使用Vue.js来实现一个跨表格相互拖拽。在开发这个业务之前呢,也调研了网上很多解决方案,但个人感觉不太符合现在做的这个需求。所以,压根就自己再开发一套,方便以后维护。

什么需求呢?就是多个表格之间可以实现相互拖拽,即A表格中的表格项可以拖拽到B表格,B表格的表格项可以拖拽到C表格,并且它们之间可以单选、多选表格项相互拖拽。然后,D表格加以限制,每次只能够拖入一项,需输入密码,密码正确后,被拖入的一项替换D表格中的表格项,被替换的D表格项放入A表格,只能被替换,不能被删除。

文字太枯燥,我们放一张动图来看下效果。

上面提到的A、B、C、D表格与下图游客、操作员、电工、管理员一一对应。

在这里插入图片描述

此图非静止~

实战

既然,我们知道了要实现怎样的效果,那么我们就开工吧!

第一步

需要确定我们这个需求需要安装哪些依赖。

  • vue@2.6.11
  • element-ui@2.15.5
  • sortablejs@1.14.0

初始化项目之后,安装以上依赖。你可以在package.json文件中看到:

  "dependencies": {
    "element-ui": "^2.15.5",
    "sortablejs": "^1.14.0",
    "vue": "^2.6.11"
  },

第二步

引入ElementUI,具体怎么引入,可以查看ElementUI官网,这里不过多阐述了。然后,我们在目录src\components(假设你有这个文件夹)文件夹下创建一个文件夹,名字姑且叫DragTables
在文件夹中,我们再创建一个utils文件夹与index.vue文件。在utils文件夹中我们再创建两个文件:data.jsindex.js

即文件目录结构为:

- components
-- DragTables
--- utils
---- data.js
---- index.js
--- index.vue

第三步

utils\data.js文件是存放数据的文件,而utils\index.js则是工具函数文件。

现在我们先来定义数据。

// data.js
export default {
    // 游客
    guestData: [
    {
        userId: "1",
        name: "a1",
        account: "w",
        jobTit: '1',
        auth: '1'
    },
    {
        userId: "211",
        name: "a0",
        account: "w",
        jobTit: '1',
        auth: '1'
    }
    ],
    // 管理员
    managerData: [
    {
        userId: "121",
        name: "a2",
        account: "w",
        jobTit: '1',
        auth: '1'
    }
    ],
    // 电工
    electricianData: [
    {
        userId: "12121",
        name: "a3",
        account: "w",
        jobTit: '1',
        auth: '1'
    }
    ],
    // 操作员
    operatorData: [
    {
        userId: "133e",
        name: "a4",
        account: "w",
        jobTit: '1',
        auth: '1'
    }
    ]
}

userId必须是唯一的。

然后,我们接着定义工具函数,这里我们需要一个深拷贝方法,我们把它定义在utils\index.js文件中。

// utils\index.js

/**
 * Deep copy
 * @param {Object} target
 */
export function deepClone(target) {
    // 定义一个变量
    let result;
    // 如果当前需要深拷贝的是一个对象的话
    if (typeof target === 'object') {
        // 如果是一个数组的话
        if (Array.isArray(target)) {
            result = []; // 将result赋值为一个数组,并且执行遍历
            for (let i in target) {
                // 递归克隆数组中的每一项
                result.push(deepClone(target[i]));
            }
            // 判断如果当前的值是null的话;直接赋值为null
        } else if (target === null) {
            result = null;
            // 判断如果当前的值是一个RegExp对象的话,直接赋值
        } else if (target.constructor === RegExp) {
            result = target;
        } else {
            // 否则是普通对象,直接for in循环,递归赋值对象的所有值
            result = {};
            for (let i in target) {
                result[i] = deepClone(target[i]);
            }
        }
        // 如果不是对象的话,就是基本数据类型,那么直接赋值
    } else {
        result = target;
    }
    // 返回最终结果
    return result;
}

第四步

做完了准备工作,我们就可以进入DragTables\index.vue进行最重要的实战环节。我们先把代码分区域列出来。

1. UI页面代码
<template>
    <div
      class="main-box"
      v-loading="loadLoading"
      element-loading-text="数据加载中"
      element-loading-spinner="el-icon-loading"
    >
      <div class="main-l">
<!-- 游客 -->
        <div class="top-name">
          <div class="top-box">
            <div class="top-count">
              人数:
              {{
                initStatus.newGuestList ? guestData.length : newGuestList.length
              }}
            </div>
          </div>
          <p>游客</p>
        </div>
        <div class="utable-box">
          <el-table
            ref="guestData"
            :data="guestData"
            :row-key="getUserId"
            @row-click="guestDataSelect"
            style="width: 100%; border: 1px solid #002368; margin-bottom: 20px"
            @selection-change="selectionGuestChange"
          >
            <el-table-column type="selection" width="55"> </el-table-column>
            <el-table-column
              prop="name"
              label="姓名"
              align="center"
              show-overflow-tooltip
            ></el-table-column>
            <el-table-column
              prop="account"
              label="账号"
              align="center"
              show-overflow-tooltip
            >
            </el-table-column>
            <el-table-column
              prop="jobTit"
              label="职务"
              align="center"
              show-overflow-tooltip
              style="position: relative"
            >
              <template slot-scope="scope">
                <span>{{ scope.row.jobTit }}</span>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
      <div class="main-r">
<!-- 管理员 -->
        <div class="top-name">
          <p>管理员</p>
        </div>
        <el-table
          ref="managerData"
          :data="managerData"
          :row-key="getUserId"
          style="width: 100%; border: 1px solid #002368; margin-bottom: 10px"
        >
          <el-table-column
            prop="name"
            label="姓名"
            align="center"
            show-overflow-tooltip
          >
          </el-table-column>
          <el-table-column
            prop="account"
            label="账号"
            align="center"
            show-overflow-tooltip
          >
          </el-table-column>
          <el-table-column
            prop="jobTit"
            label="职务"
            align="center"
            style="position: relative"
            show-overflow-tooltip
          >
            <template slot-scope="scope">
              <span>{{ scope.row.jobTit }}</span>
            </template>
          </el-table-column>
        </el-table>
<!-- 操作员 -->
        <div class="top-name">
          <div class="top-box">
            <div class="top-count">
              人数:
              {{
                initStatus.newOperatorList
                  ? operatorData.length
                  : newOperatorList.length
              }}
            </div>
          </div>
          <p>操作员</p>
        </div>
        <div class="table-b">
          <div class="table-box">
            <el-table
              ref="operatorData"
              class="table-l"
              :data="operatorData"
              :row-key="getUserId"
              style="margin-bottom: 20px"
              @row-click="operatorDataSelect"
              @selection-change="selectionOperatorChange"
            >
              <el-table-column type="selection" width="55"> </el-table-column>
              <el-table-column
                prop="name"
                label="姓名"
                align="center"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="account"
                label="账号"
                align="center"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="jobTit"
                label="职务"
                align="center"
                show-overflow-tooltip
              ></el-table-column>
              <el-table-column align="center" label="操作">
                <template>
                   <el-button size="small" @click.stop="">编辑</el-button>
                </template>
              </el-table-column>
            </el-table>
          </div>
        </div>
<!-- 电工 -->
        <div class="top-name">
          <div class="top-box">
            <div class="top-count">
              人数:
              {{
                initStatus.newElectricianList
                  ? electricianData.length
                  : newElectricianList.length
              }}
            </div>
          </div>
          <p>电工</p>
        </div>
        <div class="table-b">
          <div class="table-box">
            <el-table
              ref="electricianData"
              :data="electricianData"
              :row-key="getUserId"
              @row-click="electricianDataSelect"
              class="table-l"
              @selection-change="selectionElectricianChange"
            >
              <el-table-column type="selection" width="55"> </el-table-column>
              <el-table-column
                prop="name"
                label="姓名"
                align="center"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="account"
                label="账号"
                align="center"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="jobTit"
                label="职务"
                align="center"
                show-overflow-tooltip
              ></el-table-column>
              <el-table-column align="center" label="操作">
                <template>
                    <el-button size="small" @click.stop="">编辑</el-button>
                </template>
              </el-table-column>
            </el-table>
          </div>
        </div>
      </div>
<!-- 密码弹窗 -->
      <el-dialog :visible.sync="passwordView"  width="30%">
        <el-form ref="form">
          <el-form-item>
            <el-input
              v-model="password"
              placeholder="请输入密码"
              type="password"
              show-password
            ></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="passwordView = false">取 消</el-button>
          <el-button type="primary" @click="okChangeManager"
            >确 定</el-button
          >
        </span>
      </el-dialog>
    </div>
</template>

分为五个部分:游客表格、管理员表格、操作员表格、电工表格、密码弹窗。每个表格的左上角动态显示表格内的人数。另外,就上面的那个动图来看,如果有一个表格与其他的表格样式布局不统一怎么办?就比如这里的电工表格、操作员表格就与游客表格、管理员表格布局样式不一样,多出来一个操作项。我是这样处理的,我把它们找出相同的部分,即都有姓名、账号、职务这三个项。电工表格、操作员表格只是多出来一个操作项。那就可以把它分成两个表格,操作项单独一个表格。只要监听电工表格或者操作员表格它们对应的数据长度就可以实现同步。为什么要分成两个表格呢?是因为,如果你从游客这个表格拖入到操作员这个表格,因为在游客表格没有操作这个选项,所以当你拖入到操作员表格时,就不会有操作这个选项(这是因为使用的拖拽的插件只是复制对应Node节点)。那肯定不行啊!

2. 逻辑代码
<script>
import Sortable from "sortablejs";
import { deepClone } from "./utils/index";
import tableData from "./utils/data";

export default {
  name: "DragTables",
  data: () => ({
    passwordView: false,
    loadLoading: false,
    password: "",
    guestData: [],
    managerData: [],
    electricianData: [],
    operatorData: [],
    initStatus: {
      newGuestList: true,
      newManagerList: true,
      newOperatorList: true,
      newElectricianList: true,
    },
    fromItem: "",
    newGuestList: [],
    newManagerList: [],
    newOperatorList: [],
    newElectricianList: [],
    selectGuestList: [],
    selectOperatorList: [],
    selectElectricianList: [],
    managerOldIndex: 0,
  }),
  watch: {
    passwordView: "watchPasswordView",
  },
  created() {
    // 定义静态数据
    this.obj = {
      newGuestList: ["guestData", 0],
      newManagerList: ["managerData", 3],
      newOperatorList: ["operatorData", 1],
      newElectricianList: ["electricianData", 2],
    };
    this.guestData = tableData.guestData;
    this.managerData = tableData.managerData;
    this.electricianData = tableData.electricianData;
    this.operatorData = tableData.operatorData;
  },
  mounted() {
    this.sortGuest();
    this.sortOperator();
    this.sortElectrician();
    this.sortManager();
  },
  methods: {
    // 密码框置空
    watchPasswordView(val) {
      if (!val) {
        this.password = "";
      }
    },
    // 选择游客
    guestDataSelect(row) {
      row.flag = !row.flag;
      this.$refs.guestData.toggleRowSelection(row, row.flag);
    },
    // 选择操作员
    operatorDataSelect(row) {
      row.flag = !row.flag;
      this.$refs.operatorData.toggleRowSelection(row, row.flag);
    },
    // 选择电工
    electricianDataSelect(row) {
      row.flag = !row.flag;
      this.$refs.electricianData.toggleRowSelection(row, row.flag);
    },
    // 确定拖拽到管理员
    okChangeManager() {
      if (this.password.trim().length > 0) {
        const item = this[this.fromItem][this.managerOldIndex];
        if (item) {
          this.newManagerList = this.initStatus.newManagerList
            ? deepClone(this.managerData)
            : deepClone(this.newManagerList);
          this.newGuestList = this.initStatus.newGuestList
            ? deepClone(this.guestData)
            : deepClone(this.newGuestList);
          this.initStatus.newGuestList = false;
          this.initStatus.newManagerList = false;
          this.newManagerList = [item];
          this[this.fromItem].splice(this.managerOldIndex, 1);
          this.initStatus[this.fromItem] = false;

          if (this.managerData[0]) {
            const obj = deepClone(this.managerData[0]);
            this.newGuestList.push(obj);
            this.guestData.push(obj);
          }
          this.managerData = [item];
          switch (this.fromItem) {
            case "newGuestList":
              this.guestData = deepClone(this.newGuestList);
              break;
            case "newOperatorList":
              this.operatorData = deepClone(this.newOperatorList);
              break;
            case "newElectricianList":
              this.electricianData = deepClone(this.newElectricianList);
              break;
            default:
              break;
          }
          this.$message({
            message: "拖拽成功!",
            type: "success",
          });
          this.password = "";
          this.passwordView = false;
        } else {
          this.$message({
            message: "拖拽失败",
            type: "warning",
          });
          this.password = "";
          this.passwordView = false;
        }
      } else {
        this.$message({
          message: "请输入密码",
          type: "warning",
        });
      }
    },
    // 获取userId
    getUserId(row) {
      return row.userId;
    },
    // 封装添加数据
    useAddNewData(evt, newData, oldData) {
      const item = this[this.fromItem][evt.oldIndex]; // 添加项
      const loading = this.$loading({
        lock: true,
        text: "加载中",
        spinner: "el-icon-loading",
        background: "rgba(0, 0, 0, 0.7)",
      });

      setTimeout(() => {
        loading.close();
        this.$message({
          message: "拖拽成功!",
          type: "success",
        });
      }, 1000);
      this[newData] = this.initStatus[newData]
        ? deepClone(oldData)
        : deepClone(this[newData]);
      this.initStatus[newData] = false;
      oldData.push(item);
      this[newData].push(item);
      this[this.fromItem].splice(evt.oldIndex, 1);
      this.$refs[this.obj[newData][0]].$el
        .querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
        .removeChild(evt.item);
    },
    // 封装添加(多)数据
    useAddsNewData(evt, newData, oldData) {
      const arr = [];
      for (
        let index = 0;
        index < this[`select${this.fromItem.split("new")[1]}`].length;
        index++
      ) {
        const element = this[`select${this.fromItem.split("new")[1]}`][index];
        arr.push(element.userId);
      }
      const loading = this.$loading({
        lock: true,
        text: "加载中",
        spinner: "el-icon-loading",
        background: "rgba(0, 0, 0, 0.7)",
      });

      setTimeout(() => {
        loading.close();
        this.$message({
          message: "批量拖拽成功!",
          type: "success",
        });
      }, 1000);
      this[newData] = this.initStatus[newData]
        ? deepClone(oldData)
        : deepClone(this[newData]);
      this.initStatus[newData] = false;
      this[newData].push(...this[`select${this.fromItem.split("new")[1]}`]);
      this[this.obj[newData][0]].push(
        ...this[`select${this.fromItem.split("new")[1]}`]
      );
      this.useDel(
        this[`select${this.fromItem.split("new")[1]}`],
        this[this.obj[this.fromItem][0]]
      );
      this.$refs[this.obj[newData][0]].$el
        .querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
        .removeChild(evt.item);
    },
    // 封装初始化数据
    useInitData(fromItem, oldData) {
      this.fromItem = fromItem;
      this[fromItem] = this.initStatus[fromItem]
        ? deepClone(oldData)
        : deepClone(this[fromItem]);
      this.initStatus[fromItem] = false;
    },
    // 批量删除(数组)
    useDel(data, currentData) {
      for (let i = 0; i < data.length; i++) {
        const element = data[i];
        for (let j = 0; j < currentData.length; j++) {
          const item = currentData[j];
          if (item === element) {
            currentData.splice(j, 1);
          }
        }
      }
    },
    // 还原初始状态
    useReduction(i) {
      const arr = [
        {
          data: "guestData",
          sletData: "selectGuestList",
        },
        {
          data: "operatorData",
          sletData: "selectOperatorList",
        },
        {
          data: "electricianData",
          sletData: "selectElectricianList",
        },
      ];
      this.$refs[arr[i].data].clearSelection();
      this[arr[i].sletData] = [];
    },
    // 监听游客表格选择
    selectionGuestChange(val) {
      this.selectGuestList = val;
    },
    // 监听操作员表格选择
    selectionOperatorChange(val) {
      this.selectOperatorList = val;
    },
    // 监听电工表格选择
    selectionElectricianChange(val) {
      console.log(val);
      this.selectElectricianList = val;
    },
    // 拖拽游客
    sortGuest() {
      const el = this.$refs.guestData.$el.querySelectorAll(
        ".el-table__body-wrapper > table> tbody"
      )[0];
      Sortable.create(el, {
        ghostClass: "sortable-ghost",
        sort: false,
        animation: 150,
        group: {
          name: "person",
          pull: true,
          put: true,
        },
        setData: function (
          /** DataTransfer */ dataTransfer,
          /** HTMLElement*/ dragEl
        ) {
          dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
        },
        onStart: () => {
          this.useInitData("newGuestList", this.guestData); // 初始化
        },
        onAdd: (evt) => {
          this.useReduction(0);
          if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
            this.useAddNewData(evt, "newGuestList", this.guestData);
          } else {
            this.useAddsNewData(evt, "newGuestList", this.guestData);
          }
        },
        onEnd: (ev) => {
          if (this[`select${this.fromItem.split("new")[1]}`].length !== 0) {
            this.useReduction(0);
            if (ev.to.outerText.indexOf("管理员") !== -1) {
              this.$nextTick(() => {
                this.newGuestList = deepClone(this.guestData);
                const data = deepClone(this.guestData);
                this.guestData = data;
              });
            } else {
              const data = deepClone(this.guestData);
              this.newGuestList = deepClone(this.guestData);
              this.guestData = [];
              this.$nextTick(() => {
                this.guestData = data;
              });
            }
          } else {
            this.$nextTick(() => {
              this.guestData = this.newGuestList;
            });
          }
        },
      });
    },
    // 拖拽操作员
    sortOperator() {
      const el = this.$refs.operatorData.$el.querySelectorAll(
        ".el-table__body-wrapper > table> tbody"
      )[0];
      Sortable.create(el, {
        ghostClass: "sortable-ghost",
        sort: false,
        animation: 150,
        group: {
          name: "person",
          pull: true,
          put: true,
        },
        setData: function (
          /** DataTransfer */ dataTransfer,
          /** HTMLElement*/ dragEl
        ) {
          dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
        },
        onStart: () => {
          this.useInitData("newOperatorList", this.operatorData); // 初始化
        },
        onAdd: (evt) => {
          this.useReduction(1);
          if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
            this.useAddNewData(evt, "newOperatorList", this.operatorData);
          } else {
            this.useAddsNewData(evt, "newOperatorList", this.operatorData);
          }
        },
        onEnd: (ev) => {
          if (this[`select${this.fromItem.split("new")[1]}`].length !== 0) {
            this.useReduction(1);
            if (ev.to.outerText.indexOf("管理员") !== -1) {
              this.$nextTick(() => {
                this.newOperatorList = deepClone(this.operatorData);
                const data = deepClone(this.operatorData);
                this.operatorData = data;
              });
            } else {
              const data = deepClone(this.operatorData);
              this.newOperatorList = deepClone(this.operatorData);
              this.operatorData = [];
              this.$nextTick(() => {
                this.operatorData = data;
              });
            }
          } else {
            this.$nextTick(() => {
              this.operatorData = this.newOperatorList;
            });
          }
        },
      });
    },
    // 拖拽电工
    sortElectrician() {
      const el = this.$refs.electricianData.$el.querySelectorAll(
        ".el-table__body-wrapper > table> tbody"
      )[0];
      Sortable.create(el, {
        ghostClass: "sortable-ghost",
        sort: false,
        animation: 150,
        group: {
          name: "person",
          pull: true,
          put: true,
        },
        setData: function (
          /** DataTransfer */ dataTransfer,
          /** HTMLElement*/ dragEl
        ) {
          dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
        },
        onStart: () => {
          this.useInitData("newElectricianList", this.electricianData); // 初始化
        },
        onAdd: (evt) => {
          this.useReduction(2);
          if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
            this.useAddNewData(evt, "newElectricianList", this.electricianData);
          } else {
            this.useAddsNewData(
              evt,
              "newElectricianList",
              this.electricianData
            );
          }
        },
        onEnd: (ev) => {
          if (this[`select${this.fromItem.split("new")[1]}`].length !== 0) {
            this.useReduction(2);
            if (ev.to.outerText.indexOf("管理员") !== -1) {
              this.$nextTick(() => {
                this.newElectricianList = deepClone(this.electricianData);
                const data = deepClone(this.electricianData);
                this.electricianData = data;
              });
            } else {
              const data = deepClone(this.electricianData);
              this.newElectricianList = deepClone(this.electricianData);
              this.electricianData = [];
              this.$nextTick(() => {
                this.electricianData = data;
              });
            }
          } else {
            this.$nextTick(() => {
              this.electricianData = this.newElectricianList;
            });
          }
        },
      });
    },
    // 拖拽管理员
    sortManager() {
      const el = this.$refs.managerData.$el.querySelectorAll(
        ".el-table__body-wrapper > table > tbody"
      )[0];
      Sortable.create(el, {
        ghostClass: "sortable-ghost",
        sort: false,
        animation: 150,
        group: {
          name: "person",
          pull: false,
          put: true,
        },
        setData: function (
          /** DataTransfer */ dataTransfer,
          /** HTMLElement*/ dragEl
        ) {
          dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
        },
        onAdd: (evt) => {
          console.log(evt)
          switch (this.fromItem) {
            case "newGuestList":
              {
                const data = deepClone(this.guestData);
                this.guestData = [];
                this.$nextTick(() => {
                  this.guestData = data;
                });
              }
              break;
            case "newOperatorList":
              {
                const data = deepClone(this.operatorData);
                this.operatorData = [];
                this.$nextTick(() => {
                  this.operatorData = data;
                });
              }
              break;
            case "newElectricianList":
              {
                const data = deepClone(this.electricianData);
                this.electricianData = [];
                this.$nextTick(() => {
                  this.electricianData = data;
                });
              }
              break;
            default:
              break;
          }
          if (this[`select${this.fromItem.split("new")[1]}`].length < 2) {
            this.managerOldIndex = evt.oldIndex;
            this.passwordView = true;
          } else {
            this.$message({
              message: "批量失败!",
              type: "warning",
            });
          }
          this.$refs.managerData.$el
            .querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
            .removeChild(evt.item);
        },
      });
    },
  },
};
</script>

我们这里使用的拖拽插件是sortablejs,功能非常强大。我这里就简单介绍下它的使用。

Sortable.create(el,{})

这里,需要给Sortable对象下的create方法传入两个参数,第一个参数是el节点,这个节点是定义可拖拽的每一项,如:

const el = this.$refs.guestData.$el.querySelectorAll(
  ".el-table__body-wrapper > table> tbody"
)[0];

这里的意思就是游客表格中的表格项。

第二个参数是可配置参数,可以定义配置属性与方法。详情参数与方法可以参照中文官网:http://www.sortablejs.com/

下面,我们将分层来讲解逻辑实现。data方法中返回的对象我们看到只是初始化了一些数据,这里先不过多阐述。然后到了watch属性,它监听了data方法中返回的对象的passwordView属性,并对应的监听方法是watchPasswordView

我们往下面methods属性中找到,就是简单地对密码框中的内容每次初始化(置空)。

// 密码框置空
watchPasswordView(val) {
  if (!val) {
    this.password = "";
  }
},

然后,我们进入了created方法,我们主要做了两件事,一件事是定义了一个静态对象(没有定义在data方法中,所以没有做响应式处理,为了性能优化),另一件事是获取表格数据。

created() {
// 定义静态数据
this.obj = {
  newGuestList: ["guestData", 0],
  newManagerList: ["managerData", 3],
  newOperatorList: ["operatorData", 1],
  newElectricianList: ["electricianData", 2],
};
// 获取表格数据
this.guestData = tableData.guestData;
this.managerData = tableData.managerData;
this.electricianData = tableData.electricianData;
this.operatorData = tableData.operatorData;
},

然后,我们进入mounted方法,我们看到方法中调用了这几个拖拽表格方法,为什么会放在mounted方法中呢?是因为要想使用拖拽,必须等到实例被挂载后调用。

最后,我们将进入methods属性,这里定义了很多方法,下面我们还是分功能部分开始分析。

mounted() {
    this.sortGuest();
    this.sortOperator();
    this.sortElectrician();
    this.sortManager();
}

这几个方法主要是点击对应表格项进行勾选。

// 选择游客
guestDataSelect(row) {
  row.flag = !row.flag;
  this.$refs.guestData.toggleRowSelection(row, row.flag);
},
// 选择操作员
operatorDataSelect(row) {
  row.flag = !row.flag;
  this.$refs.operatorData.toggleRowSelection(row, row.flag);
},
// 选择电工
electricianDataSelect(row) {
  row.flag = !row.flag;
  this.$refs.electricianData.toggleRowSelection(row, row.flag);
}

将选择的表格项数据存起来。

// 监听游客表格选择
selectionGuestChange(val) {
  this.selectGuestList = val;
},
// 监听操作员表格选择
selectionOperatorChange(val) {
  this.selectOperatorList = val;
},
// 监听电工表格选择
selectionElectricianChange(val) {
  this.selectElectricianList = val;
}

为每一表格项定义一个唯一的key。

// 获取userId
getUserId(row) {
  return row.userId;
},

我们进入关键部分,也就是拖拽。是不是看起来代码特别多,其实这块还可以优化,但是为了更容易分辨,先这样。我们看到这几个方法中都有一个相同的部分,就是先定义el变量,然后执行Sortable.create(el, {})方法,另外,Sortable.create()的第二个参数中,都有onStart()onAdd()onEnd()这几个方法。

// 拖拽游客
sortGuest() {
  const el = this.$refs.guestData.$el.querySelectorAll(
    ".el-table__body-wrapper > table> tbody"
  )[0];
  Sortable.create(el, {
    ghostClass: "sortable-ghost",
    sort: false,
    animation: 150,
    group: {
      name: "person",
      pull: true,
      put: true,
    },
    setData: function (
      /** DataTransfer */ dataTransfer,
      /** HTMLElement*/ dragEl
    ) {
      dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
    },
    onStart: () => {
      this.useInitData("newGuestList", this.guestData); // 初始化
    },
    onAdd: (evt) => {
      this.useReduction(0);
      if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
        this.useAddNewData(evt, "newGuestList", this.guestData);
      } else {
        this.useAddsNewData(evt, "newGuestList", this.guestData);
      }
    },
    onEnd: (ev) => {
      if (this[`select${this.fromItem.split("new")[1]}`].length !== 0) {
        this.useReduction(0);
        if (ev.to.outerText.indexOf("管理员") !== -1) {
          this.$nextTick(() => {
            this.newGuestList = deepClone(this.guestData);
            const data = deepClone(this.guestData);
            this.guestData = data;
          });
        } else {
          const data = deepClone(this.guestData);
          this.newGuestList = deepClone(this.guestData);
          this.guestData = [];
          this.$nextTick(() => {
            this.guestData = data;
          });
        }
      } else {
        this.$nextTick(() => {
          this.guestData = this.newGuestList;
        });
      }
    },
  });
},
// 拖拽操作员
sortOperator() {
  const el = this.$refs.operatorData.$el.querySelectorAll(
    ".el-table__body-wrapper > table> tbody"
  )[0];
  Sortable.create(el, {
    ghostClass: "sortable-ghost",
    sort: false,
    animation: 150,
    group: {
      name: "person",
      pull: true,
      put: true,
    },
    setData: function (
      /** DataTransfer */ dataTransfer,
      /** HTMLElement*/ dragEl
    ) {
      dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
    },
    onStart: () => {
      this.useInitData("newOperatorList", this.operatorData); // 初始化
    },
    onAdd: (evt) => {
      this.useReduction(1);
      if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
        this.useAddNewData(evt, "newOperatorList", this.operatorData);
      } else {
        this.useAddsNewData(evt, "newOperatorList", this.operatorData);
      }
    },
    onEnd: (ev) => {
      if (this[`select${this.fromItem.split("new")[1]}`].length !== 0) {
        this.useReduction(1);
        if (ev.to.outerText.indexOf("管理员") !== -1) {
          this.$nextTick(() => {
            this.newOperatorList = deepClone(this.operatorData);
            const data = deepClone(this.operatorData);
            this.operatorData = data;
          });
        } else {
          const data = deepClone(this.operatorData);
          this.newOperatorList = deepClone(this.operatorData);
          this.operatorData = [];
          this.$nextTick(() => {
            this.operatorData = data;
          });
        }
      } else {
        this.$nextTick(() => {
          this.operatorData = this.newOperatorList;
        });
      }
    },
  });
},
// 拖拽电工
sortElectrician() {
  const el = this.$refs.electricianData.$el.querySelectorAll(
    ".el-table__body-wrapper > table> tbody"
  )[0];
  Sortable.create(el, {
    ghostClass: "sortable-ghost",
    sort: false,
    animation: 150,
    group: {
      name: "person",
      pull: true,
      put: true,
    },
    setData: function (
      /** DataTransfer */ dataTransfer,
      /** HTMLElement*/ dragEl
    ) {
      dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
    },
    onStart: () => {
      this.useInitData("newElectricianList", this.electricianData); // 初始化
    },
    onAdd: (evt) => {
      this.useReduction(2);
      if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
        this.useAddNewData(evt, "newElectricianList", this.electricianData);
      } else {
        this.useAddsNewData(
          evt,
          "newElectricianList",
          this.electricianData
        );
      }
    },
    onEnd: (ev) => {
      if (this[`select${this.fromItem.split("new")[1]}`].length !== 0) {
        this.useReduction(2);
        if (ev.to.outerText.indexOf("管理员") !== -1) {
          this.$nextTick(() => {
            this.newElectricianList = deepClone(this.electricianData);
            const data = deepClone(this.electricianData);
            this.electricianData = data;
          });
        } else {
          const data = deepClone(this.electricianData);
          this.newElectricianList = deepClone(this.electricianData);
          this.electricianData = [];
          this.$nextTick(() => {
            this.electricianData = data;
          });
        }
      } else {
        this.$nextTick(() => {
          this.electricianData = this.newElectricianList;
        });
      }
    },
  });
},
// 拖拽管理员
sortManager() {
  const el = this.$refs.managerData.$el.querySelectorAll(
    ".el-table__body-wrapper > table > tbody"
  )[0];
  Sortable.create(el, {
    ghostClass: "sortable-ghost",
    sort: false,
    animation: 150,
    group: {
      name: "person",
      pull: false,
      put: true,
    },
    setData: function (
      /** DataTransfer */ dataTransfer,
      /** HTMLElement*/ dragEl
    ) {
      dataTransfer.setData("Text", dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
    },
    onAdd: (evt) => {
      console.log(evt)
      switch (this.fromItem) {
        case "newGuestList":
          {
            const data = deepClone(this.guestData);
            this.guestData = [];
            this.$nextTick(() => {
              this.guestData = data;
            });
          }
          break;
        case "newOperatorList":
          {
            const data = deepClone(this.operatorData);
            this.operatorData = [];
            this.$nextTick(() => {
              this.operatorData = data;
            });
          }
          break;
        case "newElectricianList":
          {
            const data = deepClone(this.electricianData);
            this.electricianData = [];
            this.$nextTick(() => {
              this.electricianData = data;
            });
          }
          break;
        default:
          break;
      }
      if (this[`select${this.fromItem.split("new")[1]}`].length < 2) {
        this.managerOldIndex = evt.oldIndex;
        this.passwordView = true;
      } else {
        this.$message({
          message: "批量失败!",
          type: "warning",
        });
      }
      this.$refs.managerData.$el
        .querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
        .removeChild(evt.item);
    },
  });
},

因为onStart()onAdd()onEnd()这几个方法在sortGuest()以及其与几个拖拽方法中逻辑都差不多,所以我们就单独摘出sortGuest()方法进行分析下。

首先,可以看到onStart()方法中调用了this.useInitData(),意思就是当你开始拖拽的时候调用this.useInitData()方法,这个方法第一个参数传一个字符串,第二个参数传一个数组,即当前表格数据。这个方法做了两个工作,一是定义一个开始拖拽时记录当前表格的标识,二是将当前表格的数据克隆到新数组中。

// 封装初始化数据
useInitData(fromItem, oldData) {
  this.fromItem = fromItem;
  this[fromItem] = this.initStatus[fromItem]
    ? deepClone(oldData)
    : deepClone(this[fromItem]);
  this.initStatus[fromItem] = false;
}

然后,我们再分析onAdd()方法,它的意思是当被拖入添加到当前表格时触发。这个方法中做了两项工作,一是调用了useReduction方法,二是根据旧表格项是否有选择数据来调用不同的方法。

onAdd: (evt) => {
    this.useReduction(0);
    if (this[`select${this.fromItem.split("new")[1]}`].length === 0) {
      this.useAddNewData(evt, "newGuestList", this.guestData);
    } else {
      this.useAddsNewData(evt, "newGuestList", this.guestData);
    }
}

以下是useReduction方法。

// 还原初始状态
useReduction(i) {
  const arr = [
    {
      data: "guestData",
      sletData: "selectGuestList",
    },
    {
      data: "operatorData",
      sletData: "selectOperatorList",
    },
    {
      data: "electricianData",
      sletData: "selectElectricianList",
    },
  ];

  this.$refs[arr[i].data].clearSelection(); // 将选中的勾选框置空
  this[arr[i].sletData] = []; // 将选择数据置空
}

接着,我们来看下useAddNewData()方法,这个方法是封装了一个拖拽添加单项的方法。第一个参数是onAdd()方法中的第一个参数,第二个参数是一个字符串,即新数据的标识,第三个参数是当前被添加的表格数据对象。

首先,我们取到需要添加的表格项,然后使用this.$loading()方法首先一个数据加载动画。更新新旧数据,将当前项添加到当前表格,并且删除旧表格中的数据,使用removeChild方法删除页面元素。

useAddsNewData方法同理,只不过遍历选择数据。

// 封装添加数据
useAddNewData(evt, newData, oldData) {
  const item = this[this.fromItem][evt.oldIndex]; // 添加项

  const loading = this.$loading({
    lock: true,
    text: "加载中",
    spinner: "el-icon-loading",
    background: "rgba(0, 0, 0, 0.7)",
  });

  setTimeout(() => {
    loading.close();
    this.$message({
      message: "拖拽成功!",
      type: "success",
    });
  }, 1000);

  this[newData] = this.initStatus[newData]
    ? deepClone(oldData)
    : deepClone(this[newData]);
  this.initStatus[newData] = false;
  oldData.push(item);
  this[newData].push(item);
  this[this.fromItem].splice(evt.oldIndex, 1);
  this.$refs[this.obj[newData][0]].$el
    .querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
    .removeChild(evt.item);
},

// 封装添加(多)数据
useAddsNewData(evt, newData, oldData) {
  const arr = [];
  for (
    let index = 0;
    index < this[`select${this.fromItem.split("new")[1]}`].length;
    index++
  ) {
    const element = this[`select${this.fromItem.split("new")[1]}`][index];
    arr.push(element.userId);
  }
  const loading = this.$loading(
    lock: true,
    text: "加载中",
    spinner: "el-icon-loading",
    background: "rgba(0, 0, 0, 0.7)",
  });

  setTimeout(() => {
    loading.close();
    this.$message({
      message: "批量拖拽成功!",
      type: "success",
    });
  }, 1000);
  this[newData] = this.initStatus[newData]
    ? deepClone(oldData)
    : deepClone(this[newData]);
  this.initStatus[newData] = false;
  this[newData].push(...this[`select${this.fromItem.split("new")[1]}`]);
  this[this.obj[newData][0]].push(
    ...this[`select${this.fromItem.split("new")[1]}`]
  );
  this.useDel(
    this[`select${this.fromItem.split("new")[1]}`],
    this[this.obj[this.fromItem][0]]
  );
  this.$refs[this.obj[newData][0]].$el
    .querySelectorAll(".el-table__body-wrapper > table > tbody")[0]
    .removeChild(evt.item);
},

我们在useAddsNewData方法中有一个方法是useDel方法,这个方法的作用是批量删除数组中的元素,这里旧数据删除指定项。

// 批量删除(数组)
useDel(data, currentData) {
  for (let i = 0; i < data.length; i++) {
    const element = data[i];
    for (let j = 0; j < currentData.length; j++) {
      const item = currentData[j];
      if (item === element) {
        currentData.splice(j, 1);
      }
    }
  }
},

我们还有最后一个方法okChangeManager(),这个方法最外层是判断密码是否为空,我这里简化了逻辑,这里本来是需要调用接口来,但是为了好理解,所以先忽视这部分。
同样,我们需要获取被添加项,因为添加项只能是一个,所以这地方我们直接看条件允许的情况下。我们需要知道被添加项添加到管理员数据表格中,原先的数据会被移到游客表格,并且被添加项从原始表格数据中删除。

// 确定拖拽到管理员
okChangeManager() {
  if (this.password.trim().length > 0) {
    const item = this[this.fromItem][this.managerOldIndex]; // 添加项
    if (item) {
      this.newManagerList = this.initStatus.newManagerList
        ? deepClone(this.managerData)
        : deepClone(this.newManagerList);
      this.newGuestList = this.initStatus.newGuestList
        ? deepClone(this.guestData)
        : deepClone(this.newGuestList);
      this.initStatus.newGuestList = false;
      this.initStatus.newManagerList = false;
      this.newManagerList = [item];
      this[this.fromItem].splice(this.managerOldIndex, 1);
      this.initStatus[this.fromItem] = false;

      if (this.managerData[0]) {
        const obj = deepClone(this.managerData[0]);
        this.newGuestList.push(obj);
        this.guestData.push(obj);
      }
      this.managerData = [item];
      switch (this.fromItem) {
        case "newGuestList":
          this.guestData = deepClone(this.newGuestList);
          break;
        case "newOperatorList":
          this.operatorData = deepClone(this.newOperatorList);
          break;
        case "newElectricianList":
          this.electricianData = deepClone(this.newElectricianList);
          break;
        default:
          break;
      }
      this.$message({
        message: "拖拽成功!",
        type: "success",
      });
      this.password = "";
      this.passwordView = false;
    } else {
      this.$message({
        message: "拖拽失败",
        type: "warning",
      });
      this.password = "";
      this.passwordView = false;
    }
  } else {
    this.$message({
      message: "请输入密码",
      type: "warning",
    });
  }
},

3. 样式代码
<style scoped>
.top-name {
  padding: 10px;
  background: #333;
  font-size: 14px;
  position: relative;
}
.top-name > p {
  color: #00a7ff;
  text-align: center;
  font-size: 16px;
}
.main-box {
  display: flex;
  justify-content: space-between;
}
.main-l,.main-r {
  width: 48%;
  position: relative;
}
.table-b {
  position: relative;
}
.isdel {
  text-align: center;
  font-size: 18px;
  color: #fff;
}
.top-box {
  display: flex;
  height: 30px;
  justify-content: space-between;
  align-items: center;
}
.top-count {
  color: #fff;
  font-size: 14px;
  margin-left:10px ;
}
.utable-box {
  overflow: auto;
  height: 700px;
}
.table-box {
  overflow: auto;
  height: 230px;
  margin-bottom: 10px;
  border: 1px solid #333;
}
</style>

样式在这里就不过多的分析了。

结语

如果想看下完整代码,看下真实操作效果,可以访问下方的源码地址:

https://github.com/maomincoding/drag-tables

关于

作者:Vam的金豆之路

曾获得2019年CSDN年度博客之星,CSDN博客访问量已达到数百万。掘金博客文章多次推送到首页,总访问量已达到数十万。

另外,我的公众号:前端历劫之路,公众号持续更新最新前端技术及相关技术文章。欢迎关注我的公众号,让我们一起在前端道路上历劫吧!Go!

Logo

前往低代码交流专区

更多推荐