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

概要

如果你需要一个大数据表格,我想你肯定会考虑尝试一下《vxe-table》。而我是亲身体验了它对数据性能上处理的强大。
我们日常使用的《el-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;
}

所需插件版本号,请按需安装

"element-ui": "^2.15.10",
"sortablejs": "^1.15.0",
"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();
    }
  },
};


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

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

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

该方法是你在行拖拽后默认执行的。要显示范围框你还需要创建一个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

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

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

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

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

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

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

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

功能包含

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

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

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

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

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

技术细节

其实你可以吧这些功能定义成全局的,这样就不用每次在表格上进行初始化了

例如:

  • API
  • 支持模型类型

小结

下一篇文章我将继续完善表格功能。比如实现 鼠标滑动拖拽选取单元格等!敬请期待...

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
vxe-table是一个基于vue表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、模态窗口、自定义模板、灵活的配置项、丰富的扩展插件等... 设计理念: 面向现代浏览器,高效的简洁 API 设计 模块化表格、按需加载、插件扩展 为单行编辑表格而设计,支持增删改查及更多扩展,强大的功能的同时兼具性能 功能: Basic table (基础表格) Grid (高级表格) Size (尺寸) Striped (斑马线条纹) Table with border (带边框) Cell style (单元格样式) Column resizable (列宽拖动) Maximum table height (最大高度) Resize height and width (响应式宽高) Fixed column (固定列) Grouping table head (表头分组) Highlight row and column (高亮行、列) Table sequence (序号) Radio (单选) Checkbox (多选) Sorting (排序) Filter (筛选) Rowspan and colspan (合并行或列) Footer summary (表尾合计) Import (导入) Export (导出) Print (打印) Show/Hide column (显示/隐藏列) Loading (加载中) Formatted content (格式化内容) Custom template (自定义模板) Context menu(快捷菜单) Virtual Scroller(虚拟滚动) Expandable row (展开行) Pager(分页) Form(表单) Toolbar(工具栏) Tree table (树形表格) Editable CRUD(增删改查) Validate(数据校验) Data Proxy(数据代理) Keyboard navigation(键盘导航) Modal window(模态窗口) Charts(图表工具) 更新日志: v4.0.20 table 修改单选框、复选框获取值错误问题 grid 修复 KeepAlive 中报错问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值