关于vxe-table的使用心得及扩展【表格虚拟滚动】(非插件)

概要

如果你需要一个大数据表格,我想你肯定会考虑尝试一下《vxe-table》。而我是亲身体验了它对数据性能上处理的强大。
我们日常使用的vue2.x的《el-table》会多一些,但它对大数据性能上的处理确实不怎么样,如果你想尝试改变下它,请点击移步另一篇文章
或者使用element-plus已对vue2.x的表格进行了升级,但实际上对比vxe-table的方便性上还是有些欠缺。

示例动图

前端虚拟滚动表格列模糊搜索
名称列模糊搜索
行拖拽及拖动范围
在这里插入图片描述
外部新增行或右键菜单新增行
在这里插入图片描述
鼠标拖拽选取单元格。支持ctrl或shift+win追加操作
在这里插入图片描述
多选节点层级右移、左移
在这里插入图片描述
右键插入行、删除行、多选删除
在这里插入图片描述

使用方式

  1. 第一种,把vxe-table和xe-utils下载到本地,放在public目录下,并在index.html中引用
    在这里插入图片描述

在这里插入图片描述

<link href="<%= BASE_URL %>vxe-table/vxe-table@legacy_lib_style.css" rel="stylesheet">

<script src="<%= BASE_URL %>vxe-table/xe-utils.js"></script>
<script src="<%= BASE_URL %>vxe-table/vxe-table@legacy.js"></script>

然后你需要在main.js中再引用一下,挂载vxe-table的组件

/* global XEUtils:false */
/* global VXETable:false */
VXETable.setup({
  zIndex: 1300, // 全局 zIndex 起始值
})
Vue.use(VXETable)

至此,你就可以在页面中使用vxe-*开头的任意组件了

  1. 第二种,使用npm安装
 npm install xe-utils@^3.5.11 vxe-table@^3.6.13

然后在main中进行挂载组件

import 'vxe-table/lib/style.css'
import VXETable from 'vxe-table'
Vue.use(VXETable)

在你需要使用xe-utils方法的地方

import XEUtils from "xe-utils";

使用方式:
XEUtils.isNaN(undefined)

1、普通表格启用虚拟滚动

默认情况下,如果设置了 height、max-height 则会根据触发规则自动启用虚拟渲染,触发规则由 scroll-x={ enabled, gt } | scroll-y={ enabled, gt } 设置。虚拟滚动启用后只会渲染指定范围内的可视区数据,其他的数据将被卷去收起,当滚动到可视区时才被渲染出来

例如:

<vxe-table
	  border
	  show-overflow
	  height="400"
	  :row-config="{isHover: true}"
	  :data="tableData"
	  :scroll-y="{enabled: true}">
	  <vxe-column type="seq" width="100"></vxe-column>
	  <vxe-column field="name" title="Name" sortable></vxe-column>
	  <vxe-column field="role" title="Role"></vxe-column>
	  <vxe-column field="sex" title="Sex"></vxe-column>
</vxe-table>

需要特别说明的是:建议你使用vxe-grid替代vxe-table。vxe-grid是包含vxe-table所有功能的配置版

2、tree型数据启用虚拟滚动

虚拟树表格,虚拟树与虚拟列表行为完全一致,区别是需要设置 transform 开启自动将列表转成树结构。也就是说,你除了要添加高度属性外,还需要在treeConfig中配置transform:true

例如:

<template>
  <div>
    <vxe-grid ref="xGrid" v-bind="gridOptions" row-id="id">
    
      <template #beginDate_edit="scope">
        <vxe-input
          v-model="scope.row.beginDate"
          type="date"
          @change="dateChange(scope)"
        ></vxe-input>
      </template>
      <template #units_default="{ row }">
        <span :class="unitCodeFormat(row) ? '' : 'vxe-cell--placeholder'">{{
          unitCodeFormat(row) || "请选择单位"
        }}</span>
      </template>
      <template #units_edit="{ row }">
        <vxe-select
          v-model="row.units"
          clearable
          filterable
          placeholder="请选择单位"
        >
          <vxe-option
            v-for="item in unitCodeOptions"
            :key="item.dictValue"
            :value="item.dictKey"
            :label="item.dictValue"
          ></vxe-option>
        </vxe-select>
      </template>
      <template v-slot:operation="{ row }">
        <template v-if="$refs.xGrid.isActiveByRow(row)">
          <vxe-button
            icon="vxe-icon-save"
            status="primary"
            title="保存"
            circle
          ></vxe-button>
        </template>
        <template v-else>
          <vxe-button icon="vxe-icon-edit" title="编辑" circle></vxe-button>
        </template>
        <vxe-button icon="vxe-icon-delete" title="删除" circle></vxe-button>
        <vxe-button icon="vxe-icon-eye-fill" title="查看" circle></vxe-button>
      </template>
    </vxe-grid>
  </div>
</template>
<script>
import XEUtils from "xe-utils";
export default {
  name: "vxetable-tree",
  data() {
    return {
      gridOptions: {
        height: "900",
        zIndex: 1100, // 设置此属性会改善tootip、弹窗等功能的层级,防止别遮住
        border: true,
        class: "sortable-tree",
        rowConfig: {
          useKey: true,
          isCurrent: true
        },
        columnConfig: {
          useKey: true,
          resizable: true
        },
        editConfig: {
          trigger: "click",
          mode: "cell",
          showStatus: true
        },
        checkboxConfig: {
          range: true, // 鼠标滑动多选
          // checkStrictly: true, // 父子节点不互相关联
          checkField: "checked" // 提升渲染速度(每行数据中需要有这个字段名,叫checked是我随便定义的)
        },
        treeConfig: {
          expandAll: true,
          rowField: "id",
          transform: true, // 开启虚拟滚动
          // children: "children", // 默认值就是children
          // parentField: "parentId", // 默认值就是parentId
        },
        exportConfig: {}, // 使用自带的导入导出功能必须写这个
        columns: [
          { type: "checkbox", width: 50 },
          { field: "id", title: "ID", width: 240, treeNode: true },
          {
            title: "开始日期",
            field: "beginDate",
            editRender: {
              placeholder: "请选择开始日期",
              autofocus: ".vxe-input--inner",
            },
            slots: { edit: "beginDate_edit" },
          },
          {
            title: "工程量",
            field: "amount",
            editRender: { name: "input" },
          },
          {
            title: "单价(元)",
            field: "price",
            editRender: { name: "input" },
          },
          {
            title: "单位",
            field: "units",
            editRender: { autofocus: ".vxe-input--inner" },
            slots: { default: "units_default", edit: "units_edit" },
          },

          {
            title: "操作",
            width: 200,
            showOverflow: true,
            slots: { default: "operation" },
          },
        ],
        menuConfig: {},
      },
      unitCodeOptions: [
        {
          id: "1485584342101127171",
          tenantId: "",
          parentId: "310060200855869856",
          code: "wmsMeasurements",
          dictKey: "1",
          dictValue: "米",
          sort: 1,
          remark: "",
          isSealed: 0,
          isDeleted: 0,
        },
        {
          id: "1485584342101127172",
          tenantId: "",
          parentId: "310060200855869856",
          code: "wmsMeasurements",
          dictKey: "2",
          dictValue: "㎡",
          sort: 2,
          remark: "",
          isSealed: 0,
          isDeleted: 0,
        },
        {
          id: "1485584342101127173",
          tenantId: "",
          parentId: "310060200855869856",
          code: "wmsMeasurements",
          dictKey: "3",
          dictValue: "升",
          sort: 3,
          remark: "",
          isSealed: 0,
          isDeleted: 0,
        },
        {
          id: "1485584342101127174",
          tenantId: "",
          parentId: "310060200855869856",
          code: "wmsMeasurements",
          dictKey: "4",
          dictValue: "千克",
          sort: 4,
          remark: "",
          isSealed: 0,
          isDeleted: 0,
        },
        {
          id: "1485584342101127175",
          tenantId: "",
          parentId: "310060200855869856",
          code: "wmsMeasurements",
          dictKey: "5",
          dictValue: "吨",
          sort: 5,
          remark: "",
          isSealed: 0,
          isDeleted: 0,
        },
        {
          id: "1485584342101127176",
          tenantId: "",
          parentId: "310060200855869856",
          code: "wmsMeasurements",
          dictKey: "6",
          dictValue: "m³",
          sort: 6,
          remark: "",
          isSealed: 0,
          isDeleted: 0,
        }
      ],
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      let _self = this;
      _self.getData();
    },
    getData() {
      let _self = this;
      let $grid = _self.$refs.xGrid;
      /**此处要进行特别说明
       * 1.数据量特别大时,不建议把数据放在vue的data中,放入会严重造成卡顿
       * 2.如果后端返回的是tree型数据,就是嵌套型数据时,需要使用把它变成扁平数据,并使用loadData进行数据更新。
       * 如果后端直接返回扁平数据,则可以省略XEUtils.toTreeArray(res.data)的过程
       * 例子data.js为tree型数据结构 每个第一级节点的parentId默认值为0
       * [{id:1,parentId:0,children:[{id:2,parentId:1},{id:3,parentId:1}]}]
       **/ 
      _self.axios.get("data.json").then((res) => {
        let rData = XEUtils.toTreeArray(res.data);
        $grid.loadData(rData);
      });
    },
    // 同步默认单位翻译
    unitCodeFormat(row, column) {
      var unit = "";
      for (var i = 0; i < this.unitCodeOptions.length; i++) {
        if (this.unitCodeOptions[i].dictKey == row.units) {
          unit = this.unitCodeOptions[i].dictValue;
          break;
        }
      }
      return unit;
    },
  },
};
</script>

技术细节

提示:因为我们要进行功能复用,所以要对基本参数和方法进行提取。我采取的是mixin方式

/**
 * 判断是否为空
 */
export function validatenull(val) {
    if (typeof val == 'boolean') {
        return false;
    }
    if (typeof val == 'number') {
        return false;
    }
  if (val instanceof Array) {
    if (val.length == 0) return true;
  } else if (val instanceof Object) {
    if (JSON.stringify(val) === '{}') return true;
  } else {
    if (val == 'null' || val == null || val == 'undefined' || val == undefined || val == '') return true;
    return false;
  }
  return false;
}

所需插件版本号,请按需安装, xlsx:表格数据导入、sortablejs:表格拖拽排序

"element-ui": "^2.15.10",
"vxe-table": "^3.6.17",
"xe-utils": "^3.5.12",
"xlsx": "^0.18.5"
"sortablejs": "^1.15.0",
import { validatenull } from "@/utils/validate";
import { Message, MessageBox } from "element-ui";
import { Sortable } from "sortablejs";
import XEUtils from "xe-utils";
import VXETable from "vxe-table";
import * as XLSX from "xlsx";

VXETable.UUID_prefix = "seq_"; // uuid前缀
VXETable.rowUUID = 0; // 新增行或者导入数据的唯一id
export const gridOptions = {
  data() {
    let _gthis = this;
    return {
      highlight_shift_interval: null,
      highlight_shift_setTimeout: null,
      highlight_drap_Interval: null,
      highlight_drap_setTimeout: null,
      sortableRow: null,
      enterStr: "\r\n",
      spaceStr: "\t",
      PROMISE_STATE: {
        PENDING: "pending",
        FULFILLED: "fulfilled", // 成功
        REJECTED: "rejected", // 失败
      },
      gridOptions: {
        zIndex: 1100, // 设置此属性会改善tootip、弹窗等功能的层级,防止别遮住
        class: "sortable-tree",
        loading: false,
        showOverflow: true,
        showHeaderOverflow: true,
        border: true,
        keepSource: true,
        printConfig: {}, // 打印
        rowConfig: {
          useKey: true,
          isCurrent: true,
          height: 55,
        },
        columnConfig: {
          useKey: true,
          resizable: true,
        },
        expandConfig: {
          reserve: true,
        },
        treeConfig: {
          expandAll: true,
          rowField: "id",
          transform: true, // 开启虚拟滚动
          // children: "children", // 默认值就是children
          // parentField: "parentId", // 默认值就是parentId
        },
        checkboxConfig: {
          range: true, // 鼠标滑动多选
          checkStrictly: true, // 父子节点不互相关联 (如果要使左移、右移结果正确,此项必须为true)
          checkField: "checked", // 提升渲染速度(每行数据中需要有这个字段名,叫checked是我随便定义的)
        },
        toolbarConfig: {
          custom: true, // 显示自定义列按钮
          slots: {
            buttons: "toolbar_buttons",
          },
          refresh: true, // 显示刷新按钮
          print: true, // 显示打印按钮
          zoom: true, // 显示全屏按钮
        },
        menuConfig: {
          body: {
            options: [
              [
                {
                  code: "insertRow",
                  name: "向下插入一条空数据",
                  disabled: false,
                },
                {
                  code: "insertForClipboard",
                  name: "向下插入剪贴板数据",
                  disabled: false,
                },
                {
                  code: "remove",
                  name: "删除当前行",
                  disabled: false,
                  callback: function ({ row }) {
                    _gthis.removeRowEvent(row);
                  },
                },
                {
                  code: "removeCkeckedAll",
                  name: "批量删除勾选数据",
                  disabled: false,
                },
              ],
            ],
          },
          visibleMethod({ options, column }) {
            const isDisabled =
              !column ||
              ["checked", "dragBtn", "id", "dirType", "seq"].includes(
                column.field
              );
            options.forEach((list) => {
              list.forEach((item) => {
                item.disabled = isDisabled;
              });
            });
            return true;
          },
        },
      },
    };
  },
  methods: {
    getUUID(seq) {
      return VXETable.UUID_prefix + seq;
    },
    // 新增
    handleAdd() {
      let _self = this;
      const $grid = _self.$refs.xGrid;
      _self._setRow({ $grid });
    },
    /**
     * 主体新增
     * @param {表格对象, defVal:Object 默认值 } params
     * @param {Function} callback
     * @returns
     */
    _setRow({ $grid, defVal }, callback) {
      let props = $grid.$options.propsData;
      if (validatenull(props)) return;
      let columns = props.columns;
      if (validatenull(columns)) return;
      const { fullData } = $grid.getTableData();
      VXETable.rowUUID += 1;
      // 创建行数据模板
      let record = {
        id: this.getUUID(VXETable.rowUUID),
        parentId: 0,
        checked: false, // checkField
      };
      for (let l = 0; l < columns.length; l++) {
        const col = columns[l];
        let field = col.field || "";
        if (field) {
          if (field !== "id") {
            record[field] = "";
          }
        }
      }
      // 设置默认值
      if (!validatenull(defVal)) {
        record = Object.assign(record, defVal);
      }
      // 加载数据
      fullData.push(record);
      let rData = XEUtils.toTreeArray(fullData);
      $grid.loadData(rData);
      if (XEUtils.isFunction(callback)) {
        callback({ fullData, record });
      }
    },
    // 批量删除
    handleDeleteAll() {
      let _self = this;
      const $grid = _self.$refs.xGrid;
      _self._deleteChecked({ $grid });
    },
    /**
     * 批量删除
     * @param {表格对象} params
     * @param {Function} callback
     * @returns
     */
    _deleteChecked({ $grid }, callback) {
      let props = $grid.$options.propsData;
      if (validatenull(props)) return;
      const { fullData } = $grid.getTableData();
      const options = { children: "children" };
      const checkedData = $grid.getCheckboxRecords(); // 勾选的节点数据
      if (validatenull(checkedData)) {
        Message.warning("请勾选后再进行操作");
        return;
      }
      MessageBox.confirm(
        "删除后其子项也将被删除!您确定要批量删除吗?",
        "警告",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).then(function () {
        for (let i = 0; i < checkedData.length; i++) {
          console.log("[ checkedData ]-194", checkedData);
          const selfRow = checkedData[i];
          const selfNode = XEUtils.findTree(
            fullData,
            (row) => row.id === selfRow.id,
            options
          );

          if (!validatenull(selfNode)) {
            if (validatenull(selfNode.parent)) {
              fullData.splice(selfNode.index, 1);
            } else if (!validatenull(selfNode.items)) {
              selfNode.items.splice(selfNode.index, 1);
            }
          }
        }
        let rData = XEUtils.toTreeArray(fullData);
        $grid.loadData(rData);
        if (XEUtils.isFunction(callback)) {
          callback({ fullData });
        }
      });
    },
    // 取消勾选
    handleCheckedCancel() {
      let _self = this;
      const $grid = _self.$refs.xGrid;
      _self._checkedCancel({ $grid });
    },
    /**
     * 取消所有勾选
     * @param {Object} params
     * @param {Function} callback
     * @returns
     */
    _checkedCancel({ $grid }, callback) {
      const checkedData = $grid.getCheckboxRecords(); // 勾选的节点数据
      if (validatenull(checkedData)) {
        Message.warning("暂无勾选数据");
        return;
      }
      $grid.setCheckboxRow(checkedData, false);
      if (XEUtils.isFunction(callback)) {
        callback({ checkedData });
      }
    },
    // 注册左平移
    handleLeft() {
      let _self = this;
      const xGrid = _self.$refs.xGrid;
      _self._leftShift(xGrid);
    },
    /**
     * 勾选节点左移,以第一条勾选的数据的上一条作为父节点
     * 1.只勾选父节点,则子节点跟随移动
     * 2.勾选父节点同时勾选该父节点包含的子节点,且父子节点相邻则移动到同一父级下。如果不相邻则分别移动到不同的新父级
     * @param {Object} params
     * @param {Function} callback
     * @returns
     */
    _leftShift(xGrid, callback) {
      let _self = this;
      const options = { children: "children" };
      let treeConfig = this.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";
      const { fullData } = xGrid.getTableData();
      let treeData = fullData;

      let checkedData = xGrid.getCheckboxRecords(); // 勾选的节点数据
      if (checkedData.length == 0) {
        VXETable.modal.message({
          content: "请先勾选节点",
          status: "warning",
        });
        return;
      }
      // 使用勾选数据重新组织成tree型数据
      let parentId = ""; // 移动的tree最外层行id
      let childrenParentId = "";
      let movable = true; // 是否可移动
      let cursorNextRow = {}; // 要移动的行数据在表格中的相邻数据(下一条)
      let checked_matchObj = {};
      let cursorParentRow = {};
      for (let i = 0; i < checkedData.length; i++) {
        let checkedRow = checkedData[i];
        if (!validatenull(checkedData[i + 1])) {
          cursorNextRow = checkedData[i + 1];
        } else {
          cursorNextRow = {};
        }

        if (checkedRow.parentId == 0) {
          // 是普通行直接移动
          // 是整个树全选,移动整个树,并过滤掉其包含的子节点
          parentId = checkedRow.id;
          continue;
        }
        if (checkedRow.parentId == childrenParentId) {
          continue;
        }
        if (checkedRow.parentId == parentId) {
          continue;
        }
        if (!validatenull(checkedRow.children)) {
          childrenParentId = checkedRow.id;
        }

        // 获取选中节点的第一个节点的上一个节点,当做树的同级
        checked_matchObj = XEUtils.findTree(
          treeData,
          function (item) {
            return checkedRow[rowField] === item[rowField];
          },
          { children: options.children }
        );
        // let matchpath = checked_matchObj.path;
        cursorParentRow = checked_matchObj.parent;

        // 如果第一条数据有同级,相邻数据则一起放到第一条数据的同级里
        // 无父级则跳过此条和它相邻的数据
        if (!validatenull(cursorParentRow) && movable) {
          _self._LeftShiftForTreeChild({
            xGrid,
            checkedRow,
            checked_matchObj,
            cursorParentRow,
          });
        } else {
          // 校验是否相邻
          let { adjoinStatus } = _self.validateNear({
            xGrid,
            checkedRow,
            cursorNextRow,
          });
          // 如果相邻
          if (adjoinStatus) {
            movable = false;
            continue;
          } else {
            movable = true;
          }
        }
      }
      // 加载数据
      let rData = XEUtils.toTreeArray(treeData);
      let { scrollTop, scrollLeft } = xGrid.getScroll();
      xGrid.clearAll();
      Promise.all([xGrid.loadData(rData)]).then(() => {
        setTimeout(() => {
          xGrid.scrollTo(scrollLeft, scrollTop);
        }, 20);
        if (XEUtils.isFunction(callback)) {
          callback(treeData);
        }
      });
    },
    _LeftShiftForTreeChild({
      xGrid,
      checkedRow,
      checked_matchObj,
      cursorParentRow,
    }) {
      const options = { children: "children" };
      let treeConfig = this.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";

      const { fullData } = xGrid.getTableData();
      let treeData = fullData; //props.data;

      if (!validatenull(cursorParentRow)) {
        if (!cursorParentRow.children) {
          cursorParentRow.children = [];
        }
        let prevParentId = checked_matchObj.parent.id;

        let prevParent_matchObj = XEUtils.findTree(
          treeData,
          function (item) {
            return prevParentId === item[rowField];
          },
          { children: options.children }
        );

        if (!validatenull(prevParent_matchObj.parent)) {
          let topParent_matchObj = XEUtils.findTree(
            treeData,
            function (item) {
              return prevParent_matchObj.parent.id === item[rowField];
            },
            { children: options.children }
          );
          if (!validatenull(topParent_matchObj)) {
            if (cursorParentRow.parentId) {
              // 设置父级id
              checkedRow.parentId = prevParent_matchObj.item.parentId;
              // // 删除要移动源数据
              // checked_matchObj.items.splice(checked_matchObj["index"], 1);
              // // 向父级添加要移动的数据
              // topParent_matchObj.item.children.push(checkedRow);
            }
          }
        } else {
          // 设置父级id
          checkedRow.parentId = prevParent_matchObj.item.parentId;
          // 删除要移动源数据
          // checked_matchObj.items.splice(checked_matchObj["index"], 1);
          // props.data.splice(prevParent_matchObj.index + 1, 0, checkedRow);
        }
      }
    },
    // 注册右平移
    handleRight() {
      let _self = this;
      const xGrid = _self.$refs.xGrid;
      _self._RightShift(xGrid);
    },
    _RightShift(xGrid, callback) {
      const options = { children: "children" };
      let treeConfig = this.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";
      const { fullData } = xGrid.getTableData();
      let treeData = fullData;
      let checkedData = xGrid.getCheckboxRecords(); // 勾选的节点数据
      if (checkedData.length == 0) {
        VXETable.modal.message({
          content: "请先勾选节点",
          status: "warning",
        });
        return;
      }
      // 使用勾选数据重新组织成tree型数据
      let parentId = ""; // 移动的tree最外层行id
      let childrenParentId = "";
      let isAdjoinStatus = false; // 是否相邻
      let cursorNextRow = {}; // 要移动的行数据在表格中的相邻数据(下一条)
      let checked_matchObj = {};
      let cursorParentRow = {}; // 要成为父节点的行
      for (let i = 0; i < checkedData.length; i++) {
        let checkedRow = checkedData[i];
        if (!validatenull(checkedData[i + 1])) {
          cursorNextRow = checkedData[i + 1];
        } else {
          cursorNextRow = {};
        }
        // 如果不相邻
        if (!isAdjoinStatus) {
          // 获取选中节点的上一个节点,当做树的父级
          checked_matchObj = XEUtils.findTree(
            treeData,
            function (item) {
              return checkedRow[rowField] === item[rowField];
            },
            { children: options.children }
          );
          let matchpath = checked_matchObj.path;
          cursorParentRow =
            checked_matchObj.items[Number(matchpath.slice(-1)[0]) - 1];
        }

        // 如果第一条数据有父级,相邻数据则一起放到第一条数据的父级里
        // 无父级则跳过此条和它相邻的数据
        if (!validatenull(cursorParentRow)) {
          // 设置父级id
          checkedRow.parentId = cursorParentRow.id;
        }

        // 如果存在下一条。校验当前数据和下一条是否相邻
        if (!validatenull(cursorNextRow)) {
          let { adjoinStatus } = this.validateNear({
            xGrid,
            checkedRow,
            cursorNextRow,
          });
          isAdjoinStatus = adjoinStatus;
        } else {
          isAdjoinStatus = false;
        }
      }

      // 加载数据
      let rData = XEUtils.toTreeArray(treeData);
      let { scrollTop, scrollLeft } = xGrid.getScroll();
      xGrid.clearAll();
      Promise.all([xGrid.loadData(rData)]).then(() => {
        setTimeout(() => {
          xGrid.scrollTo(scrollLeft, scrollTop);
        }, 20);

        if (XEUtils.isFunction(callback)) {
          callback(treeData);
        }
      });
    },
    /**
     * 校验两条数据是否相邻
     * @param {Object} params
     * @returns
     */
    validateNear({ xGrid, checkedRow, cursorNextRow }) {
      const options = { children: "children" };
      let treeConfig = this.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";
      const { fullData } = xGrid.getTableData();
      let treeData = fullData;

      let adjoinStatus = false;
      let cursorNextRowVali = {};
      if (validatenull(cursorNextRow)) {
        return { adjoinStatus, cursorNextRowVali };
      }
      // 获取要移动的行数据在表格中的相邻数据(下一条)
      let matchObj = XEUtils.findTree(
        treeData,
        function (item) {
          return checkedRow[rowField] === item[rowField];
        },
        { children: options.children }
      );
      let matchpath = matchObj.path;
      if (!validatenull(matchpath)) {
        let path_data = "treeData";
        for (let i = 0; i < matchpath.length; i++) {
          const _path = matchpath[i];
          if (i == matchpath.length - 1) {
            path_data += `["${Number(_path) + 1}"]`;
          } else {
            path_data += `["${_path}"]`;
          }
        }
        cursorNextRowVali = eval(path_data) || {};
      }
      if (!validatenull(cursorNextRowVali)) {
        if (cursorNextRowVali.id === cursorNextRow.id) {
          adjoinStatus = true;
        }
      }
      return { adjoinStatus, cursorNextRowVali };
    },
    // 注册行拖动
    rowDrop() {
      let _self = this;
      const $grid = _self.$refs.xGrid;
      _self.createRowSortable({
        $grid,
        handleClass: ".drag-btn",
        gridOptions: _self.gridOptions,
        dom: ".body--wrapper>.vxe-table--body tbody",
      });
    },
    /**
     * 注册行拖动
     * @param {Object} params   
      grid  -- 操作的表格 
      handleClass: ".drag-btn" -- 拖动选择区域,不传则整行
      dom: ".body--wrapper>.vxe-table--body tbody" 
      multiDrag: false -- 是否可以多选
      selectedClass: "selected-v" -- 多选时选中的样式
     * @param {Function} callback 
     * @returns 
     */
    createRowSortable(params, callback) {
      let _self = this;
      let {
        $grid,
        handleClass,
        selectedClass,
        multiDrag = false,
        dom,
      } = params;
      if (validatenull(dom)) return;
      this.sortableRow = Sortable.create($grid.$el.querySelector(dom), {
        handle: handleClass,
        multiDrag: multiDrag, // 多选
        delay: 5,
        fallbackTolerance: 5,
        selectedClass: selectedClass || "selected-v",
        fallbackOnBody: true,
        onChoose: function (evt) {
          if (multiDrag) {
            const targetRowNode = $grid.getRowNode(evt.item);
            const dragRow = targetRowNode.item;
            if (
              !validatenull(targetRowNode) &&
              !validatenull(dragRow.children)
            ) {
              if (!evt.item.classList.contains("selected-v")) {
                evt.item.classList.add("selected-v");
              }
            }
          }
        },
        onEnd: (evt) => {
          const targetTrElem = evt.item;
          const oldIndex = evt.oldIndex;
          const targetTrElems = evt.items;
          const oldIndicies = evt.oldIndicies;
          let parentId = "";
          if (!validatenull(targetTrElems) && !validatenull(oldIndicies)) {
            for (let i = 0; i < targetTrElems.length; i++) {
              const targetTrElem = targetTrElems[i];
              // const targetRowNode = $grid.getRowNode(targetTrElem);
              _self._MergeDrop({
                $grid,
                gridOptions,
                targetTrElem,
                oldIndex,
              });
            }
          } else {
            _self._MergeDrop({
              $grid,
              gridOptions,
              targetTrElem,
              oldIndex,
            });
          }
          if (XEUtils.isFunction(callback)) {
            callback({ evt });
          }
        },
      });
    },
    _MergeDrop({ $grid, targetTrElem, oldIndex }) {
      let _self = this;
      const { fullData } = $grid.getTableData();
      let tableTreeData = fullData;
      const options = { children: "children" };
      const wrapperElem = targetTrElem.parentNode;
      const prevTrElem = targetTrElem.previousElementSibling;
      const targetRowNode = $grid.getRowNode(targetTrElem);
      if (!targetRowNode) {
        return;
      }
      const selfRow = targetRowNode.item;
      const selfNode = XEUtils.findTree(
        tableTreeData,
        (row) => row === selfRow,
        options
      );
      if (prevTrElem) {
        // 移动到节点
        const prevRowNode = $grid.getRowNode(prevTrElem);
        if (!prevRowNode) {
          return;
        }
        const prevRow = prevRowNode.item;
        const prevNode = XEUtils.findTree(
          tableTreeData,
          (row) => row === prevRow,
          options
        );
        if (
          XEUtils.findTree(
            selfRow[options.children],
            (row) => prevRow === row,
            options
          )
        ) {
          // 错误的移动
          const oldTrElem = wrapperElem.children[oldIndex];
          wrapperElem.insertBefore(targetTrElem, oldTrElem);
          return Message.error("存在包含节点,请重新调整");
        }
        if (validatenull(selfNode) || validatenull(selfNode.items)) {
          return;
        }
        const currRow = selfNode.items.splice(selfNode.index, 1)[0];

        let addIndex = 0;
        // 如果是在同父级下移动
        if (
          prevNode.item.parentId == selfNode.item.parentId ||
          prevNode.item.id == selfNode.item.parentId
        ) {
          // 如果从下向上移动+1
          addIndex = selfNode.index < prevNode.index ? 0 : 1;
        } else {
          addIndex = 1;
        }
        // 设置移动节点的parentId
        // if (validatenull(prevNode.item.parentId) || prevNode.item.parentId == 0) {
        //   if (!validatenull(prevNode.item.children)) {
        //     selfRow.parentId = prevNode.item.id;
        //   } else {
        //     selfRow.parentId = 0;
        //   }
        // } else {
        //   selfRow.parentId = prevNode.item.parentId;
        // }
        // 移动到相邻节点
        if (
          $grid.isTreeExpandByRow(prevRow) &&
          !validatenull(prevRow[options.children])
        ) {
          selfRow.parentId = prevNode.item.id;
          // 移动到当前的子节点
          prevRow[options.children].splice(0, 0, currRow);
        } else {
          selfRow.parentId = prevNode.item.parentId;
          prevNode.items.splice(prevNode.index + addIndex, 0, currRow);
        }
      } else {
        // 移动到第一行
        if (!validatenull(selfNode) && !validatenull(selfNode.items)) {
          const currRow = selfNode.items.splice(selfNode.index, 1)[0];
          currRow.parentId = 0;
          tableTreeData.unshift(currRow);
        }
      }
      // 加载数据
      let rData = XEUtils.toTreeArray(tableTreeData);
      Promise.all([$grid.loadData(rData)]).then(() => {
        _self._DrapTrArea({ $grid, selfRow });
      });
    },
    _DrapTrArea({ $grid, selfRow }) {
      let _self = this;
      let tbody = $grid.$el.querySelector(
        ".vxe-table--body-wrapper.body--wrapper"
      ); //操作区
      tbody.querySelectorAll(".drag-highlight").forEach((el) => el.remove());
      clearInterval(this.highlight_drap_Interval);
      this.highlight_drap_Interval = null;
      clearTimeout(this.highlight_drap_setTimeout);
      this.highlight_drap_setTimeout = null;

      let trs = tbody.getElementsByTagName("tr");
      for (var i = 0; i < trs.length; i++) {
        let tr = trs[i];
        const targetRowNode = $grid.getRowNode(tr);
        const row = targetRowNode.item;
        if (row.id == selfRow.id) {
          let tr_offsetHeight = tr.offsetHeight;
          let highlight_box_top = document.createElement("div");
          highlight_box_top.classList.add("drag-highlight", "line-top");
          tr.appendChild(highlight_box_top);
          let highlight_box_right = document.createElement("div");
          highlight_box_right.classList.add("drag-highlight", "line-right");
          tr.appendChild(highlight_box_right);
          let highlight_bottom = document.createElement("div");
          highlight_bottom.classList.add("drag-highlight", "line-bottom");
          tr.appendChild(highlight_bottom);
          let highlight_box_left = document.createElement("div");
          highlight_box_left.classList.add("drag-highlight", "line-left");
          tr.appendChild(highlight_box_left);

          let rows = XEUtils.toTreeArray([selfRow]);
          let setStyle = function () {
            let box_height =
              tr_offsetHeight *
              _self.getIsExpandRowsLength({ $grid, data: rows });
            highlight_box_left.style.cssText = `height:${box_height - 1}px`;
            highlight_box_right.style.cssText = `height:${box_height - 1}px`;
            highlight_bottom.style.cssText = `top:${box_height - 2}px`;
          };
          setStyle();
          if (!_self.highlight_drap_Interval) {
            _self.highlight_drap_Interval = setInterval(setStyle, 300);
            _self.highlight_drap_setTimeout = setTimeout(() => {
              clearInterval(_self.highlight_drap_Interval);
              tbody
                .querySelectorAll(".drag-highlight")
                .forEach((el) => el.remove());
            }, 4500);
          }
          break;
        }
      }
    },
    /**
     * 获取data所包含的展开节点个数
     * @param {data:Array} params
     * @returns
     */
    getIsExpandRowsLength({ $grid, data }) {
      let includeIds = [];
      let IsExpandRowsLength = 0;
      for (let i = 0; i < data.length; i++) {
        const treeItem = data[i];
        if (i == 0) {
          includeIds = XEUtils.toTreeArray([treeItem]).map((item) => item.id);
          if ($grid.isTreeExpandByRow(treeItem)) {
            IsExpandRowsLength += includeIds.length;
          } else {
            let length =
              IsExpandRowsLength - XEUtils.toTreeArray([treeItem]).length;
            IsExpandRowsLength = length < 0 ? 0 : length;
            IsExpandRowsLength += 1;
          }
        }
        // 不包含
        if (!includeIds.includes(treeItem.id)) {
          if ($grid.isTreeExpandByRow(treeItem)) {
            includeIds = XEUtils.toTreeArray([treeItem]).map((item) => item.id);
            IsExpandRowsLength += includeIds.length;
          } else {
            let length =
              IsExpandRowsLength - XEUtils.toTreeArray([treeItem]).length;
            IsExpandRowsLength = length < 0 ? 0 : length;
            IsExpandRowsLength += 1;
          }
        } else {
          if (!$grid.isTreeExpandByRow(treeItem)) {
            let length =
              IsExpandRowsLength - XEUtils.toTreeArray([treeItem]).length;
            IsExpandRowsLength = length < 0 ? 0 : length;
            IsExpandRowsLength += 1;
          }
        }
      }
      return IsExpandRowsLength;
    },

    // 注册列拖动
    columnDrop() {
      let _self = this;
      const $grid = _self.$refs.xGrid;
      _self.sortableColumn = _self.createColumnSortable({
        $grid,
        dom: ".body--wrapper>.vxe-table--header .vxe-header--row",
        handleClass: ".vxe-header--column",
      });
    },
    createColumnSortable(params, callback) {
      let {
        $grid,
        handleClass,
        selectedClass,
        multiDrag = false,
        dom,
      } = params;
      if (validatenull(dom)) return;
      Sortable.create($grid.$el.querySelector(dom), {
        handle: handleClass, // 拖动区域,不传则整行
        multiDrag: multiDrag, // 多选
        selectedClass: selectedClass || "selected",
        fallbackOnBody: true,
        onEnd: (evt) => {
          const targetThElem = evt.item;
          const newIndex = evt.newIndex;
          const oldIndex = evt.oldIndex;
          const { fullColumn, tableColumn } = $grid.getTableColumn();
          const wrapperElem = targetThElem.parentNode;
          const newColumn = fullColumn[newIndex];
          if (newColumn.fixed) {
            // 错误的移动
            const oldThElem = wrapperElem.children[oldIndex];
            if (newIndex > oldIndex) {
              wrapperElem.insertBefore(targetThElem, oldThElem);
            } else {
              wrapperElem.insertBefore(
                targetThElem,
                oldThElem ? oldThElem.nextElementSibling : oldThElem
              );
            }
            Message.error("固定列不允许拖动!");
            return;
          }
          // 获取列索引 columnIndex > fullColumn
          const oldColumnIndex = $grid.getColumnIndex(tableColumn[oldIndex]);
          const newColumnIndex = $grid.getColumnIndex(tableColumn[newIndex]);
          // 移动到目标列
          const currRow = fullColumn.splice(oldColumnIndex, 1)[0];
          fullColumn.splice(newColumnIndex, 0, currRow);
          $grid.loadColumn(fullColumn);
          if (XEUtils.isFunction(callback)) {
            callback();
          }
        },
      });
    },
    // 删除某行指定字段数据
    // row: Object 当前行,keys:Array 要删除值的字段名
    deleteValueForRow(row, keys) {
      if (!validatenull(row) && !validatenull(keys)) {
        keys.forEach((key) => {
          row[key] ? (row[key] = "") : "";
        });
      }
    },
    // 删除表格数据
    removeRowEvent(row) {
      const $grid = this.$refs.xGrid;
      this._RemoveRow({ $grid, row });
    },
    /**
     * 删除选中行
     * @param {*} params row:Object要删除的行,或者{id:xxx}
     * @param {Function} callback
     */
    _RemoveRow({ $grid, row }, callback) {
      const selfRow = row;
      const { fullData } = $grid.getTableData();
      const options = { children: "children" };
      MessageBox.confirm("删除后其子项也将被删除!您确定删除此项吗?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(function () {
        const selfNode = XEUtils.findTree(
          fullData,
          (row) => row.id === selfRow.id,
          options
        );

        if (!validatenull(selfNode)) {
          if (validatenull(selfNode.parent)) {
            fullData.splice(selfNode.index, 1);
          } else if (!validatenull(selfNode.items)) {
            selfNode.items.splice(selfNode.index, 1);
          }
          let rData = XEUtils.toTreeArray(fullData);
          $grid.loadData(rData);
          if (XEUtils.isFunction(callback)) {
            callback({ row });
          }
        }
      });
    },
    // 单元格右键菜单,高亮当前行
    cellContextMenuEvent({ row }) {
      const $grid = this.$refs.xGrid;
      $grid.setCurrentRow(row);
    },
    // 上下文菜单
    contextMenuClickEvent(params) {
      let { row, menu, $grid } = params;
      let $xGrid = this.$refs.xGrid;
      if ($xGrid) {
        switch (menu.code) {
          // 向下插入一条空数据
          case "insertRow":
            this._InsertRow(params);
            break;
          // 向下插入剪贴板数据
          case "insertForClipboard":
            this.insertForClipboard(params);
            break;
          // 删除当前行
          case "remove":
            if (XEUtils.isFunction(menu.callback)) {
              menu.callback(params);
            } else {
              $grid.remove(row);
            }
            break;
          // 批量删除勾选
          case "removeCkeckedAll":
            this.handleDeleteAll();
            break;
        }
      }
    },
    _InsertRow(params) {
      let { menu, row, columns, $grid } = params;
      const options = { children: "children" };
      const { fullData } = $grid.getTableData();
      // tree表格插入
      // 选中节点插入同级
      let treeConfig = this.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";
      let matchObj = XEUtils.findTree(
        fullData,
        function (item) {
          return row[rowField] === item[rowField];
        },
        options
      );

      // 不是树添加
      if (!matchObj.item.parentId) {
        VXETable.rowUUID += 1;
        // 创建行数据模板
        let record = {
          id: this.getUUID(VXETable.rowUUID),
          parentId: 0,
          checked: false,
        };
        for (let l = 0; l < columns.length; l++) {
          const col = columns[l];
          let field = col.field || ""; // config中配置的列的prop名
          if (field) {
            if (field !== "id") {
              record[field] = ""; // 对应行对应列赋值
            }
          }
        }
        fullData.splice(matchObj.index + 1, 0, record);
      }
      // 树添加
      if (matchObj.item.parentId) {
        VXETable.rowUUID += 1;
        // 创建行数据模板
        let record = {
          id: this.getUUID(VXETable.rowUUID),
          parentId: row.parentId,
          checked: false,
        };
        for (let l = 0; l < columns.length; l++) {
          const col = columns[l];
          let field = col.field || ""; // config中配置的列的prop名
          if (field) {
            if (field !== "id") {
              record[field] = ""; // 对应行对应列赋值
            }
          }
        }
        if (validatenull(matchObj.parent)) {
          fullData.splice(matchObj.index + 1, 0, record);
        } else {
          matchObj.parent[options.children].splice(
            matchObj.index + 1,
            0,
            record
          );
        }
      }

      // 加载数据
      let rData = XEUtils.toTreeArray(fullData);
      $grid.loadData(rData);

      if (XEUtils.isFunction(menu.callback)) {
        menu.callback(fullData);
      }
    },
    decidePromiseState(promise) {
      const PROMISE_STATE = {
        PENDING: "pending",
        FULFILLED: "fulfilled", // 成功
        REJECTED: "rejected", // 失败
      };
      const t = {};
      return Promise.race([promise, t])
        .then((v) =>
          v === t ? PROMISE_STATE.PENDING : PROMISE_STATE.FULFILLED
        )
        .catch(() => PROMISE_STATE.REJECTED);
    },
    insertForClipboard(params) {
      let _self = this;
      let clipboardRead = window.navigator.permissions.query({
        name: "clipboard-read",
      });
      if (!clipboardRead) return;
      clipboardRead.then((res) => {
        if (res.state == "denied") {
          Message.error("不支持获取剪切板内容");
          return;
        }
        navigator.clipboard
          .read()
          .then(async (data) => {
            let ps_html = "rejected";
            await _self
              .decidePromiseState(data[0].getType("text/html"))
              .then((state) => {
                ps_html = state;
                if (state === "fulfilled") {
                  data[0].getType("text/html").then((res) => {
                    let reader = new FileReader();
                    //以下这两种方式都可以解析出来,因为Blob对象的数据可以按文本或二进制的格式进行读取
                    //reader.readAsBinaryString(blob);
                    reader.readAsText(res, "utf8");
                    reader.onload = function () {
                      let fileTxt = this.result; //这个就是解析出来的数据
                      let $doc = new DOMParser().parseFromString(
                        fileTxt,
                        "text/html"
                      );
                      const $trs = Array.from(
                        $doc.querySelectorAll("table tr")
                      );
                      if (!validatenull($trs)) {
                        // 解析剪贴板数据,生成行数据
                        let rowsInfo = [];
                        $trs.forEach((tr) => {
                          let trData = [];
                          if (!validatenull(tr.children)) {
                            let childrens = tr.children;
                            for (let l = 0; l < childrens.length; l++) {
                              const td = childrens[l];
                              trData.push(td.textContent);
                            }
                            rowsInfo.push(trData);
                          }
                        });
                        if (!validatenull(rowsInfo)) {
                          _self._SetRowData(params, rowsInfo);
                        }
                      }
                    };
                  });
                }
              });
            if (ps_html == "rejected") {
              _self
                .decidePromiseState(data[0].getType("text/plain"))
                .then((state) => {
                  if (state === "fulfilled") {
                    data[0].getType("text/plain").then((res) => {
                      let reader = new FileReader();
                      //以下这两种方式都可以解析出来,因为Blob对象的数据可以按文本或二进制的格式进行读取
                      //reader.readAsBinaryString(blob);
                      reader.readAsText(res, "utf8");
                      reader.onload = function () {
                        let fileTxt = this.result; //这个就是解析出来的数据
                        let txtRows = fileTxt.split(_self.enterStr);
                        let rowsInfo = [];
                        txtRows.forEach((item) => {
                          let row = item.split(_self.spaceStr);
                          rowsInfo.push(row);
                        });
                        if (!validatenull(rowsInfo)) {
                          _self._SetRowData(params, rowsInfo);
                        }
                      };
                    });
                  }
                });
            }
          })
          .catch((error) =>
            console.error("Failed to read text from clipboard: ", error)
          );
      });
    },
    /**
     * 根据鼠标位置添加行数据
     * @param {*} params
     * @param {Array} rowsInfo - 添加的数据
     */
    _SetRowData(params, rowsInfo) {
      let { menu, row, rowIndex, $grid, columns, columnIndex } = params;
      const options = { children: "children" };
      // 选中节点插入同级
      let treeConfig = this.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";
      const { fullData } = $grid.getTableData();
      let matchObj = XEUtils.findTree(
        fullData,
        function (item) {
          return row[rowField] === item[rowField];
        },
        { children: options.children }
      );
      if (!validatenull(rowsInfo)) {
        let setTaleData = []; // 待插入表格数据
        // 不是树添加
        if (!matchObj.item.parentId) {
          // 使用处理好的数据进行赋值
          for (let i = 0; i < rowsInfo.length; i++) {
            let row = rowsInfo[i];
            VXETable.rowUUID += 1; // 行id
            // 创建行数据模板
            let createRowRecord = {
              id: this.getUUID(VXETable.rowUUID),
              parentId: 0,
              checked: false,
            };
            let num = 0; // 有效列下标
            columns.forEach((column, index) => {
              let field = column.field || ""; // config中配置的列的prop名
              if (field) {
                if (index >= columnIndex - 1) {
                  let value = "";
                  if (
                    ["name", "units", "price", "amount", "totalPrice"].includes(
                      field
                    )
                  ) {
                    if (!validatenull(row[num])) {
                      value = row[num].replace(/\s+/g, "");
                    } else {
                      value = row[num] || "";
                    }
                  } else {
                    value = row[num] || "";
                  }
                  createRowRecord[field] = value || ""; // 对应行对应列赋值
                  num = num + 1;
                }
              }
            });
            setTaleData.push(createRowRecord);
          }
          fullData.splice(rowIndex + 1, 0, ...setTaleData);
        }
        // 树添加
        if (matchObj.item.parentId) {
          let rowIndexCope = Number(matchObj.path.slice(-1)[0]);
          // 使用处理好的数据进行赋值
          for (let i = 0; i < rowsInfo.length; i++) {
            let row = rowsInfo[i];
            VXETable.rowUUID += 1; // 行id
            // 创建行数据模板
            let createRowRecord = {
              id: this.getUUID(VXETable.rowUUID),
              parentId: matchObj.item.parentId || 0,
              checked: false,
            };
            let num = 0; // 有效列下标
            columns.forEach((column, index) => {
              let field = column.field || ""; // config中配置的列的prop名
              if (field) {
                if (index >= columnIndex - 1) {
                  let value = "";
                  if (
                    ["name", "units", "price", "amount", "totalPrice"].includes(
                      field
                    )
                  ) {
                    if (!validatenull(row[num])) {
                      value = row[num].replace(/\s+/g, "");
                    } else {
                      value = row[num] || "";
                    }
                  } else {
                    value = row[num] || "";
                  }
                  createRowRecord[field] = value || ""; // 对应行对应列赋值
                  num = num + 1;
                }
              }
            });
            setTaleData.push(createRowRecord);
          }
          if (!matchObj.parent) {
            fullData.splice(matchObj.index + 1, 0, ...setTaleData);
          } else {
            matchObj.parent[options.children].splice(
              rowIndexCope + 1,
              0,
              ...setTaleData
            );
          }
        }
        // 加载数据
        let rData = XEUtils.toTreeArray(fullData);
        $grid.clearAll();
        $grid.loadData(rData);
        if (XEUtils.isFunction(menu.callback)) {
          menu.callback(fullData);
        }
      }
    },
    /**
     * 导入
     * @param {event} e
     * @param {Object} values 默认值 {field:value}
     * @returns
     */
    changeExcel(e, values = {}) {
      let _self = this;
      _self.gridOptions.loading = true;
      const files = e.target.files;
      if (files.length <= 0) {
        return false;
      } else if (!/\.(xls|xlsx)$/.test(files[0].name.toLowerCase())) {
        return false;
      }
      // 读取表格数据
      const fileReader = new FileReader();
      fileReader.onload = (ev) => {
        const workbook = XLSX.read(ev.target.result, {
          type: "binary",
        });
        const wsname = workbook.SheetNames[0];
        const ws = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]);
        _self.dealExcel(ws, values); //转换数据格式
      };
      fileReader.readAsBinaryString(files[0]);
      _self.$refs.inputFile.value = "";
    },
    dealExcel(ws, values) {
      let _self = this;
      const { fullData } = _self.$refs.xGrid.getTableData();
      // 转换的开头f
      let temMap = {};
      let columns = _self.gridOptions.columns;
      columns.forEach((item) => {
        if (item.title && item.field) {
          temMap[item.title] = item.field;
        }
      });
      // 校验是否为标准数据模板
      // if (ws.length != 0) {
      //   let row1 = ws[0];
      //   let row1Keys = Object.keys(row1);
      //   let temKeys = Object.keys(temMap);
      //   // 1. 验证列数是否相等
      //   if (row1Keys.length !== temKeys.length) {
      //     this.$message.error("导入失败!数据列数不相同", 2000);
      //     return;
      //   }
      //   // 2. 验证列名称是否相等
      //   for (let k = 0; k < temKeys.length; k++) {
      //     const keyName = temKeys[k];
      //     if (keyName !== row1Keys[k]) {
      //       this.$message.error(
      //         `导入失败!数据列名称“${row1Keys[k]}”错误`,
      //         2000
      //       );
      //       return;
      //     }
      //   }
      // }
      // 表格数据为空,外部导入
      if (validatenull(fullData)) {
        _self.$refs.inputFile.row = {};
      }
      let cursorRow = _self.$refs.inputFile.row || {};
      let wsData = [];
      let valuesKeys = Object.keys(values);
      ws.forEach((sourceObj) => {
        let sourceKeys = Object.keys(sourceObj);
        VXETable.rowUUID += 1;
        let obj = {
          checked: false,
          parentId: cursorRow.parentId || 0,
        };
        for (const name in temMap) {
          if (Object.hasOwnProperty.call(temMap, name)) {
            const _key = temMap[name];

            // 设置导入的值
            if (sourceKeys.includes(name)) {
              obj[_key] = sourceObj[name];
            } else {
              // 设置默认值
              obj[_key] = _key == "id" ? VXETable.rowUUID : "";
              if (valuesKeys.includes(_key)) {
                obj[_key] = values[_key];
              }
            }
          }
        }
        wsData.push(obj);
      });

      // 外部导入
      if (validatenull(cursorRow) && !validatenull(wsData)) {
        let metadata = fullData.concat(wsData);
        let rData = XEUtils.toTreeArray(metadata);
        _self.$refs.xGrid.loadData(rData);
      }
      // 如果右键菜单导入
      if (!validatenull(cursorRow) && !validatenull(wsData)) {
        _self.insertImportData(fullData, wsData, cursorRow);
      }
      _self.gridOptions.loading = false;
    },
    /** 插入数据
     * insertImportData(metadata:元数据,data:插入的数据, cursorRow:插入位置, )
     */
    insertImportData(fullData, data, cursorRow) {
      let _self = this;
      let metadata = [...fullData];
      // tree表格插入
      // 选中节点插入同级
      const options = { children: "children" };
      let treeConfig = _self.gridOptions.treeConfig;
      let rowField = treeConfig.rowField || "id";
      let matchObj = XEUtils.findTree(
        metadata,
        function (item) {
          return cursorRow[rowField] === item[rowField];
        },
        { children: options.children }
      );
      // 不是树添加
      if (!matchObj.item.parentId) {
        metadata.splice(matchObj.index + 1, 0, ...data);
      }
      // 树添加
      if (matchObj.item.parentId) {
        matchObj.parent[options.children].splice(
          matchObj.index + 1,
          0,
          ...data
        );
      }
      let rData = XEUtils.toTreeArray(metadata);
      _self.$refs.xGrid.clearAll();
      _self.$refs.xGrid.loadData(rData);
      _self.$refs.inputFile.row = {};
    },
  },
  unmounted() {
    if (this.sortableRow) {
      this.sortableRow.destroy();
    }
  },
};


1、实现表格行、列拖拽(单行拖拽完美实现)

// 在你需要的合适时机绑定拖动事件(当前你可以直接放在mounted中直接执行)
 let _self = this;
 setTimeout(() => {
   _self.$nextTick(() => {
     _self.rowDrop();
     _self.columnDrop();
   });
 }, 800);

2、实现表格行拖拽后,拖拽结果范围框选

该方法是你在行拖拽后默认执行的。要显示范围框你还需要创建一个vxe-table-reset.scss文件,并在main.js中引入

import 'vxe-table/lib/style.css'
import VXETable from 'vxe-table'

VXETable.setup({
  zIndex: 1300, // 全局 zIndex 起始值
})
Vue.use(VXETable)
import '@/styles/vxe-table-reset.scss' // 注意所在顺序
// vxe-table-reset.scss
.sortable-tree .drag-btn {
  cursor: move;
  font-size: 14px;
  &.merge {
    color: #0006ff;
  }
}
.sortable-tree .vxe-body--row.sortable-ghost,
.sortable-tree .vxe-body--row.sortable-chosen {
  background-color: #dfecfb;
}
.selected-v {
  background-color: #c2ebff !important;
}
.importExcel-v {
  position: absolute;
  left: 2px;
  top: 1px;
  width: 100px;
  height: 35px;
  opacity: 0;
  cursor: pointer;
}
.sortable-tree .vxe-body--row {
  position: relative;
}

.sortable-tree .drag-highlight { 
  z-index: 1;
  &.line-top {
    position: absolute;
    top: 0;
    left: 1px;
    right: 2px;
    height: 1px;
    background: linear-gradient(90deg, transparent 50%, #09f32f 0) repeat-x;
    background-size: 12px 1px;
    background-position: 0 0;
    animation: drag-top-anim 1s infinite linear;
  }
  &.line-bottom {
    position: absolute;
    left: 1px;
    right: 2px;
    height: 1px;
    background: linear-gradient(90deg, transparent 50%, #09f32f 0) repeat-x;
    background-size: 12px 1px;
    background-position: 0 0;
    animation: drag-bottom-anim 1s infinite linear;
  }
  &.line-left {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 1px;
    width: 1px;
    background: linear-gradient(0deg, transparent 6px, #0f0ae8 6px) repeat-y;
    background-size: 1px 12px;
    background-position: 100% 0;
    animation: drag-left-anim 1s infinite linear;
  }
  &.line-right {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 2px;
    width: 1px;
    background: linear-gradient(0deg, transparent 50%, #0f0ae8 0) repeat-y;
    background-size: 1px 12px;
    background-position: 100% 0;
    animation: drag-right-anim 1s infinite linear;
  }
}
@keyframes drag-top-anim {
  from {
  }
  to {
    background-position: 12px 0;
  }
}
@keyframes drag-bottom-anim {
  from {
  }
  to {
    background-position: -12px 0;
  }
}
// 0 -12px, 100% 12px, 12px 0, -12px 100%;
@keyframes drag-left-anim {
  from {
  }
  to {
    background-position: 0 -12px;
  }
}

@keyframes drag-right-anim {
  from {
  }
  to {
    background-position: 0 12px;
  }
}


.table-edit-icon {
  color: #939393;
  cursor: pointer;
  & .size2 {
    font-size: 1.2em;
  }
}

方法名为:_DrapTrArea

3、实现表格勾选右平移(多选)

方法名为:handleRight
关联方法:_RightShift、validateNear

4、实现表格勾选左平移(多选)

方法名为:handleLeft
关联方法:_leftShift、_LeftShiftForTreeChild、validateNear

5、定义右键菜单功能:contextMenuClickEvent

你还需要把这个方法挂在@menu-click上

<vxe-grid ref="xGrid" @menu-click="contextMenuClickEvent"></vxe-grid>

功能包含

- 选中行向下插入一条空行:方法名为:_InsertRow

- 选中行向下插入剪贴板数据:方法名为:insertForClipboard

- 删除当前行:方法名为:removeRowEvent、$grid.remove(row)

- 批量删除勾选数据:方法名为:handleDeleteAll

6、单元格右键菜单,高亮当前行**:方法名为:cellContextMenuEvent

链接指引

💡 关于vxe-table的使用心得及扩展2【table表格二次封装】(非插件)
💡💡 关于vxe-table的使用心得及扩展3【vxe-table二次封装组件应用】(非插件)
下一篇文章我将继续完善表格功能。比如实现 鼠标滑动拖拽选取单元格等!敬请期待...

  • 2
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值