草庐IT

Element——el-tree懒加载

拾柒念 2023-08-15 原文

本文章项目项目全程使用Vue2和Element2!

懒加载:点击节点时才进行该层数据的获取。

注意:使用了懒加载之后,一般情况下就可以不用绑定:data

基础使用

懒加载需要再指定一个lazy和懒加载数据的方法:load

<template>
	<el-tree :props="props" :load="loadNode" lazy></el-tree>
</template>
<script>
  export default {
    data() {
      return {
        props: { // 映射配置
          label: 'name', // 将获取数组中的name作为显示节点(label)进行展示
          children: 'zones', // 将获取数组中的zones作为子节点(children)的展示
          isLeaf: 'leaf' // 将获取数组中的leaf作为判断是否是叶子节点(即没有子节点的最底层节点)
        },
      };
    },
    methods: {
      loadNode(node, resolve) { // 懒加载数据时载入的方法,只会执行一次
        if (node.level === 0) { // 初始的级数(最顶层)
          return resolve([{ name: 'region' }]); // 最顶层数据渲染为region
        }
        if (node.level > 1) return resolve([]);

        setTimeout(() => {
          const data = [{
            name: 'leaf',
            leaf: true
          }, {
            name: 'zone'
          }];
          resolve(data);
        }, 500);
      }
    }
  };
</script>

懒加载的方法会得到两个参数,一个是获取的当前节点(node)的信息(包括它的层级数据等);另一个是一个重新渲染当前节点下子节点的方法(resolve),它接收一个数组,该数组也会按照props中的映射关系,进行显示。

注意:懒加载的方法(:load)在初始载入时执行一次,而后每次点击节点前面的箭头获取子节点的时候再次触发;即使初始载入的数据有变换也不会再触发,点击展开子节点后再次点击收缩节点时也不会再触发!!

由于懒加载是一级一级往下获取,所以对每一级来说都要使用resolve来渲染它显示的子节点,如果该节点下没有显示的内容,它则会一直转圈,这个时候需要设置resolve返回一个空数组,这样如果它没有获取到子节点的内容则会在转圈之后显示为空(并去掉前面的向下展开的箭头),不会一直转圈:

return resolve([]);

现实场景:可以在通过node取到每一层的id,根据这个id调用接口得到数据。再通过resolve进行回显,回显的数据为这个id的子节点

二次封装

场景由于用到树形控件的地方很多,而且需要显示的数据都不一样,所以将树形控件再封装一层,然后根据外部组件传来的不同参数,进行树形图的不同显示。

思路:通过监听外部传入数据的变化,重新渲染树,完成不同数据的显示;但是:load只会初始加载一次并获取当前绑定树上node,如果后面监听数据的时候再次调用loadTree是获取不到它的node和resolve,所以会导致渲染失败。这个时候可以通过:data显示数据,当我们树上有节点时,就可以正常触发:load进行子节点的懒加载了。具体实现如下:

<template>
  <div class="org-tree">
    <el-tree :data="orgList" :props="defaultProps" lazy :load="loadTree" :expand-on-click-node="false" @node-click="nodeClick">
    </el-tree>
  </div>
</template>

<script>
    export default {
        name: 'Tree',
        props: {
            logicParams: { // 外部组件传入的参数
                type: Object
            }
        },
        watch: {
            logicParams: {
                handler(newVal) {
                    this.logicParams = newVal;
                    if (this.circleI >0) { // 限制首次加载时只显示loadTree加载的树,而不是:data和loadTree加载的都有
                        this.resetNode();
                    }
                    this.circleI++;
                },
                immediate: true,
                deep: true
            }
        },
        data() {
            return {
                defaultProps: {
                    children: 'children',
                    label: 'name',
                },
                circleI: 0,
                orgList: []
            }
        },
        methods: {
            // 懒加载加载方法,首次加载树的时候会被触发
            loadTree(node, resolve) {
              listByTree(this.logicParams).then(res => {
                // this.rootNode = node;
                // this.rootResolve = resolve;
                let rootMainResolve = resolve;
                let treedata = [];
                if(node.level == 0) {
                  return resolve([{ name: res.data[0].name }])
                }
                if (node.level == 1 ) {
                  treedata.push(res.data)
                  return resolve(...treedata)
                };
                if (node.data.isParent && node.data.pId != '') {
                  this.getChild(node.data, node.data.pId, rootMainResolve)
                }
                else  {
                  return resolve([]) // 防止不停转圈
                }
              })
            },
            getChild(data, type, resolve) { // 每个节点使用同一个接口获取子节点,只是传入的参数不同,将其抽出来
              this.logicChildDataParam.id =data.id;
              this.logicChildDataParam.type = type+ ";;;";
              listByTree(this.logicChildDataParam).then(res => {
                return resolve(res.data); 
              })
            },
            // 重新渲染树的根节点
            resetNode() {
              listByTree(this.logicParams).then(res => {
                  this.orgList = [res.data[0]]
              })
            },
        }
    }
</script>

数据回显

场景在懒加载的树上设置复选框,需要将之前添加好的懒加载选中的部分在表格的编辑中回显出来。

思路:由于懒加载的数据是一级一级获取的,所以可以利用default-expanded-keysdefault-checked-keys属性,将需要进行回显的节点在渲染树的时候就设置上去。(注意在使用这两个属性的时候)

<template>
  <div class="org-tree">
    <el-tree ref="tree" :props="defaultProps" lazy :load="loadTree" :expand-on-click-node="false" 
    show-checkbox  @check-change="checkChange" :default-expanded-keys="defaultExpandKeys" :default-checked-keys="defaultCheckedKeys" node-key="id">
    </el-tree>
  </div>
</template>
<script>
    export default {
        name: 'OrgTree',
        data() {
            return {
                defaultExpandKeys: [],
                defaultCheckedKeys: []
            }
        },
        methods: {
             // 当节点选中或取消选中的时候触发,可接受三个参数(具体见官网,本项目只用触发这个事件的时机)
            checkChange(data,state,childChecked) {
                let selectedAllList = [];
                let checkedList = [];
                // 选中所有的节点,包括半选节点(用作展开的节点)
                selectedAllList = this.$refs.tree.getCheckedNodes(false,true);
                // 选中所有全选节点,不包括半选(用作选中的节点)
                checkedList = this.$refs.tree.getCheckedNodes();
                
                // 触发父组件方法,将这两个数组传递出去,并在父组件的添加点击事件中调用添加方法,添加时需拿到这两个节点数组用作数据的回显
                this.$emit('selectorg', selectedAllList, checkedList);
            },
            loadTree(node, tree) {
                if (node.level == 0) {
                    let che = [];
                    let exp = [];
                    // 此处的checkedList和selectedAllList是通过调用编辑接口获取到的数据,为了方便理解写做与checkChange中一样,以下是伪代码
                    checkedList.forEach(el => { // 遍历选中的节点数组,拿到它们的id
                        if (el.id) {
                            che.push(el.id);
                        }
                    })
                    selectedAllList.forEach(el => { // 遍历包括半选的节点数组,拿到它们的id(可以将半选节点都筛出来,将所有半选节点作为展开的节点,如果嫌麻烦可以将全选的接节点也展开,不过这样可能会在树的数据量过多的情况下出现延迟和卡顿,影响性能)
                        if (el.id) {
                            exp.push(el.id);
                        }
                    })
                    this.defaultCheckedKeys = che; // 将得到的回显节点数组赋值给默认选中的数组
                    this.defaultExpandKeys = exp; // 将得到的展开节点数组赋值给默认展开的数组
                }
            },
        }
    }
</script>

回显问题

场景在使用懒加载进行数据回显时,当添加选中的数据里存在以下情况:一个父节点下的第一和第二个子节点同时被选中,回显时得到的默认选中的节点数组里也只有这两个节点的id,但是最终懒加载回显的数据是这个父节点下的所有子节点全都被选中(获取其他类似情况)。

分析:由于懒加载的树是异步加载的,树在判断子节点是否选中的时候可能由于选中的子节点,而导致其父节点因为关联而被计算判断出选中。

解决一:如果不需要父节点的复选框,或者父节点没有复选框,只有子节点有,或者不需要父子节点关联的情况下,可以使用check-strictly属性,断开父子之间的连接:

<template>
  <div class="org-tree">
    <el-tree ref="tree" :props="defaultProps" lazy :load="loadTree" :expand-on-click-node="false" 
    show-checkbox  @check-change="checkChange" :default-expanded-keys="defaultExpandKeys" :default-checked-keys="defaultCheckedKeys" node-key="id" :check-strictly="checkStrictly">
    </el-tree>
  </div>
</template>
<script>
    export default {
        name: 'OrgTree',
        data() {
            return {
                checkStrictly: true, // 根据需要在可不同的位置定义
            }
        }
</script>

解决二:对选中节点的回显不使用default-checked-keys,而是利用$nextTick和setCheckedKeys设置节点的选中,此方法必须设置node-key属性:

<template>
  <div class="org-tree">
    <el-tree ref="tree" :props="defaultProps" lazy :load="loadTree" :expand-on-click-node="false" 
    show-checkbox  @check-change="checkChange" :default-expanded-keys="defaultExpandKeys" :default-checked-keys="defaultCheckedKeys" node-key="id">
    </el-tree>
  </div>
</template>
<script>
    export default {
        name: 'OrgTree',
        data() {
            return {
                defaultExpandKeys: [],
                defaultCheckedKeys: []
            }
        },
        methods: {
             // 当节点选中或取消选中的时候触发,可接受三个参数(具体见官网,本项目只用触发这个事件的时机)
            checkChange(data,state,childChecked) {
                let selectedAllList = [];
                let checkedList = [];
                // 选中所有的节点,包括半选节点(用作展开的节点)
                selectedAllList = this.$refs.tree.getCheckedNodes(false,true);
                // 选中所有全选节点,不包括半选(用作选中的节点)
                checkedList = this.$refs.tree.getCheckedNodes();
                
                // 触发父组件方法,将这两个数组传递出去,并在父组件的添加点击事件中调用添加方法,添加时需拿到这两个节点数组用作数据的回显
                this.$emit('selectorg', selectedAllList, checkedList);
            },
            loadTree(node, tree) {
                if (node.level == 0) {
                    let che = [];
                    let exp = [];
                    // 此处的checkedList和selectedAllList是通过调用编辑接口获取到的数据,为了方便理解写做与checkChange中一样,以下是伪代码
                    selectedAllList.forEach(el => { // 遍历包括半选的节点数组,拿到它们的id
                        if (el.id) {
                            exp.push(el.id);
                        }
                    })
                    this.defaultExpandKeys = exp; // 将得到的展开节点数组赋值给默认展开的数组
                    this.$nextTick(() => { // 利用$nextTick更新节点
                        checkedList.forEach(el => { // 遍历选中的节点数组,拿到它们的id
                        if (el.id) {
                            che.push(el.id);
                        }
                    	})
                        this.$refs.tree.setCheckedKeys(che); // 手动赋值节点 
                    })
                }
            },
        }
    }
</script>

注意:该方法因为在load方法中,所以每次触发load的时候(每次首次下拉节点)都会重新获取一边数据,这会导致之前可能选中的节点又被回显节点重置了。

这种情况如果在层级没有超过2级时,可以通过设置一个计数器,让这个 n e x t T i c k 只执行一次,但是如果层级过多,下层还是会出现全选的情况。这个问题我暂时无法避免,所以综合考虑下来还是采用每执行一次 l o a d 就执行一次 nextTick只执行一次,但是如果层级过多,下层还是会出现全选的情况。这个问题我暂时无法避免,所以综合考虑下来还是采用每执行一次load就执行一次 nextTick只执行一次,但是如果层级过多,下层还是会出现全选的情况。这个问题我暂时无法避免,所以综合考虑下来还是采用每执行一次load就执行一次nextTick,这样可以保证更深的层级节点能正确显示,对于回显编辑来说,只要做到先下拉节点再选择,就不会出错了。

复选框显隐

场景现在需要去掉所有的叶子节点(没有子节点的节点)的复选框,默认选中了父节点则其下所有子节点都 不做判断。

思路:由于element的tree并未提供这个属性或方法,需要我们自己手动去修改element内部代码,然后再重新打包,将打好的包替换自己项目中element里的ilb文件夹:

将对应版本的element源码下载下来,安装依赖并查看项目是否启动成功:

npm install
npm run dev

运行成功后找到packages/tree/src/tree-node修改源码:

<template>
  <div class="el-tree-node">
    ... 
      <!-- 找到复选框的位置,根据node.data中的某个字段判断(我是根据isParent判断)设置复选框的显隐 -->
      <el-checkbox
        v-if="showCheckbox"
        v-model="node.checked"
        :style="{ 'display': node.data.isParent || node.parent == null ?'':'none'}"
        :indeterminate="node.indeterminate"
        :disabled="!!node.disabled"
        @click.native.stop
        @change="handleCheckChange"
      >
      </el-checkbox>
    ...
  </div>
</template>

源码修改好之后,进行打包:

npm run dist

打包完成之后会得到新的lib文件夹,将其替换自己项目中的对应位置的lib文件夹即可。

注意:直接修改自己项目中的packages里的代码是无效的,因为项目中所运行的是lib文件夹里的,packages只是方便查看内部源码!

有关Element——el-tree懒加载的更多相关文章

  1. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. ruby-on-rails - 使用 config.threadsafe 时从 lib/加载模块/类的正确方法是什么!选项? - 2

    我一直致力于让我们的Rails2.3.8应用程序在JRuby下正确运行。一切正常,直到我启用config.threadsafe!以实现JRuby提供的并发性。这导致lib/中的模块和类不再自动加载。使用config.threadsafe!启用:$rubyscript/runner-eproduction'pSim::Sim200Provisioner'/Users/amchale/.rvm/gems/jruby-1.5.1@web-services/gems/activesupport-2.3.8/lib/active_support/dependencies.rb:105:in`co

  4. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  5. ruby-on-rails - 使用 gmaps4rails 动态加载谷歌地图标记 - 2

    如何只加载map边界内的标记gmaps4rails?当然,在平移和/或缩放后加载新的。与此直接相关的是,如何获取map的当前边界和缩放级别? 最佳答案 我是这样做的,我只在用户完成平移或缩放后替换标记,如果您需要不同的行为,请使用不同的事件监听器:在你看来(index.html.erb):{"zoom"=>15,"auto_adjust"=>false,"detect_location"=>true,"center_on_user"=>true}},false,true)%>在View的底部添加:functiongmaps4rail

  6. ruby-on-rails - 是否可以让 ActiveRecord 为使用 :joins option? 加载的行创建对象 - 2

    我需要做这样的事情classUser'User',:foreign_key=>'abuser_id'belongs_to:gameendclassGame['JOINabuse_reportsONusers.id=abuse_reports.abuser_id','JOINgamesONgames.id=abuse_reports.game_id'],:group=>'users.id',:select=>'users.*,count(distinctgames.id)ASgame_count,count(abuse_reports.id)asabuse_report_count',:

  7. ruby - 运行 rackup private_pub.ru -s thin -E production 命令时无法加载此类文件 -- thin (LoadError) - 2

    我指的是pubrailscasttutorial并已正确执行所有步骤,但在运行最后一个命令时,即rackupprivate_pub.ru-sthin-Eproduction为了架设faye服务器,我收到以下错误:/usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in`require':cannotloadsuchfile--thin(LoadError)from/usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in`require'from/var/lib/gems/1.9.1/gems

  8. ruby - libxml-ruby 无法在 x86_64 上加载 - 2

    我们在服务器端遇到libxml-rubygem的问题可能是因为它使用x86_64架构:$uname-aLinuxip-10-228-171-642.6.21.7-2.fc8xen-ec2-v1.0#1SMPTueSep110:25:30EDT2009x86_64GNU/Linuxrequire'libxml'LoadError:/usr/local/ruby-enterprise/lib/ruby/gems/1.8/gems/libxml-ruby-1.1.4/lib/libxml_ruby.so:invalidELFheader-/usr/local/ruby-enterprise/

  9. Ruby 不从 stdlib 加载 CSV - 2

    我不太确定为什么这不起作用,我一直在寻找解决方案。很简单,我正在运行一个执行require'CSV'的小脚本。,它在我的Mac1.9.3-p327上运行良好,但在p374上的服务器上无法运行。我得到的错误是/home/deployer/.rbenv/versions/1.9.3-p374/lib/ruby/1.9.1/rubygems/custom_require.rb:36:inrequire':cannotloadsuchfile--CSV(LoadError)from/home/deployer/.rbenv/versions/1.9.3-p374/lib/ruby/1.9.1/

  10. ruby - 无法加载此类文件——脚本/rails : Getting this error while remote debugging through RubyMine - 2

    我在通过RubyMineIDE进行远程调试时遇到以下错误。$bundleexecrdebug-ide--port1234--script/railsserverFastDebugger(ruby-debug-ide0.4.9)listenson:1234/home/amit/.rvm/gems/ruby-1.9.3-p125/gems/ruby-debug-ide19-0.4.12/lib/ruby-debug-ide.rb:123:in`debug_load'/home/amit/.rvm/gems/ruby-1.9.3-p125/gems/ruby-debug-ide19-0.4.

随机推荐