【sgLazyTree】自定义组件:动态懒加载el-tree树节点数据,实现增删改、懒加载及局部数据刷新。

简介: 【sgLazyTree】自定义组件:动态懒加载el-tree树节点数据,实现增删改、懒加载及局部数据刷新。


特性

  1. 可以自定义主键、配置选项
  2. 支持预定义节点图标:folder文件夹|normal普通样式
  3. 多个提示文本可以自定义
  4. 支持动态接口增删改节点
  5. 可以自定义根节点id
  6. 可以设置最多允许添加的层级深度
  7. 支持拖拽排序,排序过程还可以针对拖拽的节点深度进行自定义限制
  8. 支持隐藏一级节点(根节点)复选框☑
  9. 支持屏蔽一级节点(根节点)勾选☑

sgLazyTree源码

<template>
  <div :class="$options.name" v-loading="rootLoading">
    <div class="tree-header" v-if="!(readonly || readonly === '')">
      <div class="sg-left">
        <template v-if="uploadData">
          <el-tooltip
            popper-class="sg-el-tooltip"
            :enterable="false"
            effect="dark"
            :content="`支持拖拽到树上传文件`"
            placement="top-start"
          >
            <el-button
              type="text"
              icon="el-icon-upload"
              size="mini"
              @click="(d) => $refs.sgUpload.triggerUploadFile()"
            >
              批量导入
            </el-button>
          </el-tooltip>
          <el-button type="text" icon="el-icon-download" size="mini" @click="downloadTpl">
            下载模板
          </el-button>
        </template>
      </div>
      <div class="sg-right">
        <el-button type="text" size="mini" @click.stop="addRoot"
          >{{ (data.text || {}).addRootButtonText || `添加根节点`
          }}<i class="el-icon-circle-plus-outline"></i
        ></el-button>
      </div>
    </div>
    <div class="tree-container">
      <el-tree
        :class="
          hideRootNodeCheckbox === '' || hideRootNodeCheckbox
            ? 'hideRootNodeCheckbox'
            : ''
        "
        :load="loadNode"
        lazy
        ref="tree"
        :node-key="mainKey"
        :props="data.props || { label: 'label' }"
        :icon-class="`${data.iconType}-tree-node`"
        :indent="data.indent || 25"
        @current-change="current_change"
        @node-click="nodeClick"
        highlight-current
        @node-drag-start="nodeDragStart"
        @node-drag-enter="nodeDragEnter"
        @node-drag-leave="nodeDragLeave"
        @node-drag-over="nodeDragOver"
        @node-drag-end="nodeDragEnd"
        @node-drop="nodeDrop"
        :draggable="draggable === '' || draggable"
        :allow-drop="allowDrop"
        :allow-drag="allowDrag"
        :show-checkbox="showCheckbox"
        :default-checked-keys="defaultCheckedKeys"
        @check-change="handleCheckChange"
      >
        <el-popover
          popper-class="tree-el-popover"
          placement="right"
          trigger="hover"
          title=""
          content=""
          :disabled="readonly || readonly === ''"
          slot-scope="{ node, data }"
        >
          <span class="right">
            <el-button
              title="添加"
              type="text"
              size=""
              icon="el-icon-circle-plus-outline"
              @click.stop="addNode(node, data)"
              v-if="showAddButton(node)"
              >添加</el-button
            >
            <el-button
              title="删除"
              type="text"
              size=""
              icon="el-icon-remove-outline"
              @click.stop="remove(node, data)"
              >删除</el-button
            >
          </span>
          <div slot="reference" class="node-label">
            <div class="left" :title="node.label">
              {{ node.label }}
            </div>
          </div>
        </el-popover>
      </el-tree>
      <!-- 上传组件 -->
      <sgUpload
        :disabledWhenShowSels="['.v-modal']"
        :drag="uploadData ? dragUpload : false"
        ref="sgUpload"
        :data="uploadData"
        @success="uploadSuccess"
        @error="uploadError"
        hideUploadTray
      />
    </div>
  </div>
</template>
<script>
import sgUpload from "@/vue/components/admin/sgUpload";
export default {
  name: "sgLazyTree",
  components: {
    sgUpload,
  },
  data() {
    return {
      // 动态树:增删改_________________________________________________________
      rootNode: null, //根节点
      rootResolve: null, //根节点
      focusNodeId: null, //聚焦高亮新添加ID
      rootLoading: false, //根节点列表加载
      mainKey: "id", //默认主键
      defaultRootId: "root", //默认根节点ID就是root
      maxAddLevel: null, // 最多允许添加的层级
      dragUpload: true, //在拖拽节点过程中控制上传组件能否拖拽上传
      // _________________________________________________________
    };
  },
  props: [
    "data",
    "readonly",
    "draggable", //是否开启拖拽节点功能
    "uploadData",
    /* 例子 uploadData: {
            accept: '.xls,.xlsx',
            actionUrl: `${this.$d.API_ROOT_URL}/core/resource/upload`,
        }, */
    "allowNodeDrag",
    "allowNodeDrop",
    "showCheckbox", //节点是否可被选择
    "hideRootNodeCheckbox", //隐藏一级节点复选框☑
    "disabledRootNode", //屏蔽一级节点勾选☑
    "defaultCheckedKeys", //默认勾选的节点的 key 的数组
  ],
  watch: {
    data: {
      /* 
      data.iconType= 节点图标:
      folder  文件夹
      normal  普通样式
      plus    加减符号样式
      */
      handler(d) {
        d.nodeKey && (this.mainKey = d.nodeKey); //主键
        d.rootId && (this.defaultRootId = d.rootId); //根节点ID
        d.maxAddLevel && (this.maxAddLevel = d.maxAddLevel); // 最多允许添加的层级
      },
      deep: true,
      immediate: true,
    },
  },
  methods: {
    // 取消选中
    unCheckAll(d) {
      this.$refs.tree.setCheckedKeys([]);
      this.handleCheckChange([], []);
    },
    handleCheckChange(data, checked, indeterminate) {
      this.$emit(`checkChange`, {
        checkedNodes: this.$refs.tree.getCheckedNodes(),
        data,
        checked,
        indeterminate,
      });
    },
    // 拖拽----------------------------------------
    nodeDragStart(node, ev) {
      this.dragUpload = false;
      this.$emit(`nodeDragStart`, node, ev);
    },
    nodeDragEnter(draggingNode, dropNode, ev) {
      this.$emit(`nodeDragEnter`, draggingNode, dropNode, ev);
    },
    nodeDragLeave(draggingNode, dropNode, ev) {
      this.$emit(`nodeDragLeave`, draggingNode, dropNode, ev);
    },
    nodeDragOver(draggingNode, dropNode, ev) {
      this.$emit(`nodeDragOver`, draggingNode, dropNode, ev);
    },
    nodeDragEnd(draggingNode, dropNode, dropType, ev) {
      // dropType有'before'、'after'、'inner'和'none'4种情况
      this.dragUpload = true;
      this.$emit(`nodeDragEnd`, draggingNode, dropNode, dropType, ev);
    },
    nodeDrop(draggingNode, dropNode, dropType, ev) {
      // dropType有'before'、'after'、'inner'和'none'4种情况
      this.$emit(`nodeDrop`, draggingNode, dropNode, dropType, ev);
    },
    allowDrop(draggingNode, dropNode, dropType) {
      // 拖拽时判定目标节点能否被放置。dropType 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后(注意:很奇葩上面node开头的绑定方法dropType有before、after、inner和none4种情况)
      return this.allowNodeDrop
        ? this.allowNodeDrop(draggingNode, dropNode, dropType)
        : true;
    },
    allowDrag(draggingNode) {
      return this.allowNodeDrag ? this.allowNodeDrag(draggingNode) : true;
    },
    // ----------------------------------------
    showAddButton(node) {
      if (this.maxAddLevel) {
        return node.level < this.maxAddLevel; // 最多允许添加的层级
      } else return true;
    },
    downloadTpl(d) {
      this.$emit(`downloadTpl`);
    },
    uploadSuccess(d, f) {
      this.$emit(`uploadSuccess`, d, f);
    },
    uploadError(d, f) {
      this.$emit(`uploadError`, d, f);
    },
    // 动态懒加载树:增删改_________________________________________________________
    // 加载根节点
    loadRootNode() {
      this.rootNode.childNodes = [];
      this.loadNode(this.rootNode, this.rootResolve);
    },
    // 加载常规节点
    loadNode(node, resolve) {
      let data = {};
      if (node.level === 0) {
        data = { [this.mainKey]: this.defaultRootId };
        this.rootNode = node; //记录根节点
        this.rootResolve = resolve; //记录根节点
      } else {
        data = node.data;
      }
      this.loadNodeData({
        node,
        data,
        callback: (d) => {
          (this.disabledRootNode === "" || this.disabledRootNode) &&
            node.level === 0 &&
            d.forEach((v) => this.$set(v, "disabled", true)); //屏蔽一级节点勾选
          resolve(d);
          this.rootLoading = false;
          this.$nextTick(() => {
            this.focusNode(this.focusNodeId);
          });
        },
      });
    },
    // 加载节点数据(通过接口向后台获取数据)
    loadNodeData({ node, data, callback }) {
      let resolve = (d) => {
        callback && callback(d);
      };
      this.$emit(`loadNode`, { node, data, resolve });
    },
    // 聚焦到某一个节点
    focusNode(id) {
      if (!id) return;
      this.$nextTick(() => {
        this.$refs.tree.setCurrentKey(id); //高亮显示某个节点
        this.$emit(`currentChange`, this.$refs.tree.getCurrentNode());
        this.$nextTick(() => {
          let dom = document.querySelector(`.el-tree-node.is-current`);
          dom &&
            dom.scrollIntoView({
              behavior: "smooth",
              block: "nearest",
              inline: "nearest",
            }); //缓慢滚动
        });
      });
    },
    // 添加根节点
    addRoot() {
      this.addNode(this.$refs.tree.root, { [this.mainKey]: this.defaultRootId });
    },
    // 通过id展开指定节点
    expandNodeById(id) {
      let node = this.$refs.tree.getNode(id);
      this.expandNode(node);
    },
    // 展开指定节点
    expandNode(node) {
      node.loaded = false; //必须要设置loaded=false,否则第二次展开节点不会触发加载数据
      node.expanded ? node.loadData() : node.expand(); //如果已展开→触发加载,否则就先展开→触发加载
    },
    // 添加节点
    addNode(node, data) {
      let isRootNode = data[this.mainKey] === this.defaultRootId;
      isRootNode && (this.rootLoading = true);
      let resolve = (d) => {
        this.focusNodeId = d[this.mainKey]; //记录加载完毕后需要聚焦的节点ID
        if (isRootNode) {
          this.loadRootNode(); //触发根节点加载
        } else {
          this.expandNode(node);
        }
      };
      this.$emit(`addNode`, { node, data, resolve });
    },
    // 删除节点
    remove(node, data) {
      this.$confirm(
        (this.data.text || {}).removeConfirmTip ||
          `此操作将永久删除该节点及其下面的节点,是否继续?`,
        (this.data.text || {}).removeConfirmTitle || `提示`,
        {
          dangerouslyUseHTMLString: true,
          confirmButtonText: `确定`,
          cancelButtonText: `取消`,
          type: "warning",
        }
      )
        .then(() => {
          this.removeNodeData(node, data);
        })
        .catch(() => {});
    },
    // 删除节点数据(通过接口向后台删除数据)
    removeNodeData(node, data) {
      node.loading = true; //出现加载动画
      let resolve = (d) => {
        node.loading = false;
        this.$message.success(`删除成功`);
        // 从显示界面删除节点
        let childNodes = node.parent.childNodes;
        childNodes.splice(
          childNodes.findIndex((d) => d.data[this.mainKey] === data[this.mainKey]),
          1
        );
      };
      this.$emit(`removeNode`, { node, data, resolve });
    },
    // 当前选中节点变化时触发的事件
    current_change(d) {
      this.$emit(`currentChange`, d);
    },
    //点击节点
    nodeClick(d) {
      this.focusNodeId = null;
      this.$emit(`nodeClick`, d);
    },
  },
};
</script>
<style lang="scss" scoped>
@import "~@/css/sg";
.sgLazyTree {
  $treeHeaderHeight: 30px;
  width: 100%;
  height: 100%;
  display: flex;
  flex-wrap: nowrap;
  flex-direction: column;
  white-space: nowrap;
  flex-shrink: 0;
  flex-grow: 1;
  position: relative;
  .tree-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: $treeHeaderHeight;
    & > .sg-left {
    }
    & > .sg-right {
    }
  }
  .tree-container {
    position: relative;
    overflow: auto;
    box-sizing: border-box;
    // height: calc(100% - #{$treeHeaderHeight});
    flex-shrink: 0;
    flex-grow: 1;
    user-select: none;
    @include scrollbarHover();
    /* >>> .tree-container .el-tree .el-tree-node__content {
      cursor: pointer;
    } */
    >>> .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
      background-color: #409eff22; // 高亮当前选中节点背景
    }
    >>> .el-tree {
      * {
        transition: none;
      }
      .el-tree-node__children {
        min-width: max-content; //这样才会出现水平滚动条
      }
      .normal-tree-node,
      .plus-tree-node,
      .folder-tree-node {
        & + label:not(.el-checkbox) {
          /*单行省略号*/
          overflow: hidden;
          white-space: nowrap;
          text-overflow: ellipsis;
        }
        flex-shrink: 0;
        display: block;
        padding: 0 !important;
        margin: 0;
        width: 20px;
        height: 20px;
        margin-right: 5px;
        background: transparent url("/static/img/fileType/folder/folder.svg") no-repeat
          center / contain;
        margin-left: 20px;
        & ~ span:not(.el-icon-loading) {
          width: 100%;
          .node-label {
            height: 40px;
            display: flex;
            align-items: center;
          }
        }
        &.expanded,
        &.is-leaf {
          flex-shrink: 0;
          transform: rotate(0deg);
          background-image: url("/static/img/fileType/folder/folder-open.svg");
        }
      }
      .normal-tree-node {
        margin-left: 10px;
        background-image: url("/static/img/fileType/folder/arrow-right.svg");
        &.expanded,
        &.is-leaf {
          transform: rotate(90deg);
          background-image: url("/static/img/fileType/folder/arrow-right.svg");
        }
      }
      .plus-tree-node {
        margin-left: 10px;
        background-image: url("/static/img/fileType/folder/plus.svg");
        &.expanded,
        &.is-leaf {
          background-image: url("/static/img/fileType/folder/minus.svg");
        }
      }
      // 隐藏一级节点的复选框
      &.hideRootNodeCheckbox > div > .el-tree-node__content .el-checkbox {
        display: none;
      }
    }
  }
}
.tree-el-popover {
  .el-button {
    padding-top: 0;
    padding-bottom: 0;
  }
}
</style>

用例

<template>
    <div style="width: 300px;padding-right: 100px;">
        <sgLazyTree :data="lazyTreeData" @currentChange="currentChange" @loadNode="loadNode" @addNode="addNode"
            @removeNode="removeNode" draggable :allowNodeDrop="allowNodeDrop" @nodeDragEnd="nodeDragEnd" />
    </div>
</template>
<script>
import sgLazyTree from "@/vue/components/admin/sgLazyTree";
export default {
    components: { sgLazyTree, },
    data() {
        return {
            autoId: 0,//自增编号
            lazyTreeData: {
                nodeKey: `ID`,//主键
                props: { label: 'MC' },//配置选项
                iconType: 'folder',//节点图标:folder文件夹|normal普通样式
                text: {
                    addRootButtonText: '添加根目录',//添加根节点按钮文本
                    removeConfirmTitle: '警告!!!',//删除节点提示标题
                    removeConfirmTip: '此操作将永久删除该文件夹及其下面的文件,是否继续?',//删除节点提示内容
                },
            },
        }
    },
    methods: {
        // 拖拽节点相关方法----------------------------------------
        allowNodeDrop(draggingNode, dropNode, dropType) {
            // 只允许拖拽同级别前后排序
            let isPrevOrNext = dropType === 'prev' || dropType === 'next';
            // 同在第一级根节点下
            let isSameRootLevel = draggingNode.level === dropNode.level && draggingNode.level === 1;
            // 同在一个节点(非根节点)下
            let isSameChildLevel = draggingNode.parent &&
                dropNode.parent &&
                draggingNode.parent.data &&
                dropNode.parent.data &&
                draggingNode.parent.data.ID === dropNode.parent.data.ID;
            return isPrevOrNext && (isSameRootLevel || isSameChildLevel);
        },
        nodeDragEnd(draggingNode, dropNode, dropType, ev) {
            // 只允许拖拽同级别前后排序
            let isBeforeOrAfter = dropType === 'before' || dropType === 'after';
            if (isBeforeOrAfter) {
                let theSameLevelDatas = (dropNode.parent.childNodes || []).map(v => v.data);// 获取同一级节点数据
                theSameLevelDatas.forEach((v, i) => v.PXZ = i);//重新排序
                //console.log(JSON.stringify(theSameLevelDatas, null, 2));
            }
        },
        // ----------------------------------------
        // 获取当前聚焦节点的数据
        currentChange(d) {
            // console.log(`currentChange`, d);
        },
        // 加载节点数据
        loadNode({data, resolve}) { this.$d.column_queryByPid({ data: { PID: data.ID }, doing: { s: d => resolve(d) } }); },
        // 添加节点
        addNode({data, resolve}) {
            this.$d.column_save({
                data: {
                    MC: `新增栏目名称(${++this.autoId})`,//栏目名称
                    LX: 0,     //0、综合 1、视频 2、音频 3、图片 4、文档 5、3d 6、连接 99、其他/
                    PID: data.ID,   //上一级id
                    FBMS: 2,  //发布模式 1、自动; 2、手动
                    LLQX: 2,   //浏览权限 1、公开; 2、登录可见;3、都不可见
                    BZ: '',  //备注,
                }, doing: { s: d => resolve(d) }
            });
        },
        // 删除节点
        removeNode({data, resolve}) {
            this.$d.column_delete({ data: { ID: data.ID }, doing: { s: d => resolve(d) } });
        },
    },
};
</script> 


相关文章
|
20天前
【sgTree】自定义组件:加载el-tree树节点整棵树数据,实现增删改操作。
【sgTree】自定义组件:加载el-tree树节点整棵树数据,实现增删改操作。
|
6月前
|
JavaScript
vue(3) 出现了不能读的属性定义的报错但可以渲染如何解决
vue(3) 出现了不能读的属性定义的报错但可以渲染如何解决
|
2月前
|
JavaScript 前端开发 API
|
8月前
|
JavaScript
原来这么简单!Vue实现动态表头详细步骤
原来这么简单!Vue实现动态表头详细步骤
140 0
|
5月前
基于antd实现动态修改节点的Tree组件
基于antd实现动态修改节点的Tree组件
154 0
|
6月前
|
前端开发 JavaScript Java
70jqGrid - 一次性加载多级表格数据
70jqGrid - 一次性加载多级表格数据
20 0
|
6月前
60EasyUI 树形菜单- 树形网格惰性加载节点
60EasyUI 树形菜单- 树形网格惰性加载节点
20 0
|
7月前
|
JavaScript
动态给vue的data添加一个新的属性时会发生什么?怎样解决?
动态给vue的data添加一个新的属性时会发生什么?怎样解决?
141 1
|
8月前
|
前端开发
react中子组件的数据更新视图未更新解决
react中子组件的数据更新视图未更新解决
166 0
|
9月前
|
JavaScript
Vue中深度拷贝对象属性,但是界面的双向绑定功能失效问题解决
Vue中深度拷贝对象属性,但是界面的双向绑定功能失效问题解决
226 0