elementUI级联选择器动态加载静态数据

710 阅读2分钟

最近做了一个基于elementUI的级联选择器组件的省市区单选选择器(多选更改绑定值的读取逻辑即可),踩了几个坑,特此记录一番。坑如下:

  • 问题一:静态数据使用了动态加载lazyLoad之后,保存的数据无法回显
  • 问题二:当checkStrictly:true时,由于是动态加载数据,而panel中选中按钮点击后,下一级无法加载出数据

针对这两个问题,搜到的处理方式大同小异,网上的方法基本都是处理服务端数据动态加载回显问题,无法解决静态数据动态加载的回显问题,若有小伙伴遇到了跟我一样的坑,本文或许能帮助你跳出坑。

对于Cascader级联组件的API若有疑问,请查阅elementUI官方文档,本篇文章不做阐述。

先来看看第一个问题,数据无法回显,其原因在于数据是动态加载的,而组件初始化时只会加载第一层级的数据,从而无法匹配到已选中的数据,也就无法回显选中的内容。

而第二个问题,是因为checkStrictly:true时,选中按钮被选中时,不会触发lazyLoad去动态加载下一级,但会触发下一级panel展开事件,因此展开下一级后没有加载任何数据。

理清了这两个问题的原因之后,就来解决这两个问题:

问题一:解决这个问题,需要在初始化时,将已选中的数据(服务端返回的选项值)对应的选项存放到组件的选项中,绑定的值才会匹配到选项数据。

问题二:在选中按钮被选中时,会出发change事件,在change事件中手动通知Cascader中的panel组件的lazyLoad方法调用,即手动加载下一级数据。

直接奉上省市区组件的全部代码,请花10分钟耐心看完,关键方法和逻辑都已注释:

<template>
  <el-cascader
    v-model="value"
    :options="options"
    :props="{
      multiple,
      checkStrictly,
      lazy: true,
      lazyLoad
    }"
    clearable
    :disabled="readonly"
    :placeholder="placeholder"
    @change="onChange"
    style="width: 100%"
    size="small"
    ref="region"
  ></el-cascader>
</template>

<script>
import { isEqual } from 'lodash'
export default {
  name: 'AreaPicker',
  props: {
    // 只读、禁用
    readonly: {
      type: Boolean
    },
    // 多选
    multiple: {
      type: Boolean,
      default: false
    },
    // 省市区格式
    format: {
      type: String,
      default: 'region' // city省市 region省市区
    },
    // 默认值
    defaultValue: {
      type: Array,
      default: () => []
    },
    placeholder: {
      type: String,
      default: '请选择地区'
    },
    // 是否父子节点不相互关联
    checkStrictly: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      value: [],
      defaultVal: [], // 用于存放初始化时的默认值
      levelCount: { // 级联可选层级,如果是省市,可选0-1-2, 如果是省市区 可选0-1-2-3
        province: 1,
        city: 2,
        region: 3
      },
      options: []
    };
  },
  watch: {
    format () {
      this.$nextTick(() => {
        this.value = []
        this.options = []
        this.onChange([])
      })
    },
    defaultValue: {
      immediate: true,
      handler (value) {
        if (value.length > 0) {
          const valueList = value.map(item => +item) || []
          // 如果绑定的地区数据和传进来的数据相同,不进行默认值设置逻辑
          if (isEqual(valueList, this.value)) return;
          this.$nextTick(() => {
            this.setDefaultOptions(valueList)
            this.value = [...valueList]
            this.defaultVal = [...valueList]
            // console.log("默认值----》", this.value)
          });
        } else {
          this.value = []
          this.defaultVal = []
        }
      }
    }
  },
  methods: {
    // 设置默认值回显的options
    // 该方法是处理回显问题的关键方法
    setDefaultOptions (defaultValue) {
      // 如果默认值只有第一级,无需查找
      if (defaultValue.length <= 1) return
      // 找出第一层的默认值匹配的数据
      const firstLevelNodeIndex = this.options.findIndex(item => item.value === defaultValue[0])
      if (firstLevelNodeIndex > -1) {
        const firstLevelNode = this.options[firstLevelNodeIndex]
        firstLevelNode.children = []
        // 从第二级开始遍历,找出每一层级匹配的数据放在children中
        // 之所以从第二级开始遍历,是因为在lazyLoad中,已经把第一级的数据全部加载完成,无需重复遍历
        defaultValue.slice(1).reduce((parent, id, index) => {
          // 当前应该查找的层级
          const level = index + 1
          // 根据当前层级的路径找出children数据
          const childList = this.findChildrenByPath(defaultValue.slice(0, level))
          // 过滤出匹配当前遍历id的地区
          const child = childList.map(item => ({
            value: item.id,
            label: item.name,
            leaf: level >= this.levelCount[this.format],
            children: []
          }))
          parent.children = child
          // 找出当前匹配id的那个地区
          const currentArea = parent.children.find(item => item.value === id)
          return currentArea
        }, firstLevelNode)
        // 将默认值对应的选项路径都替换,达到回显的目的
        this.options[firstLevelNodeIndex] = firstLevelNode
      }
    },
    // 数据量特别大,采用子节点懒加载方式
    lazyLoad (node, resolve) {
      let { level, path } = node
      let nodes = []
      // 获取当前点击的节点的子节点
      // 如果当前层级为0,表示是最外层,即应渲染所有的一级地区数据
      if (level === 0) {
        // areas是cdn引入的省市区数据,在全局作用域下,也可import导入到当前组件使用
        const firstLevelNodes = areas.map(item => ({
          value: item.id,
          label: item.name,
          leaf: level >= this.levelCount[this.format] ||
            !(item.children &&
            item.children.length > 0), // 层级不超过指定层级,或当没有children时,都不能再展开下一级panel
          children: []
        }))
        nodes = firstLevelNodes
        this.options = firstLevelNodes
      } else {
        // 如过层级不是最外层,则查找其children数据
        // 根据path路径去查找id,找出当前应该加载的children
        const child = this.findChildrenByPath(path)
        nodes = child
          .filter(item => !this.defaultVal.includes(item.id)) // 过滤掉属于默认值内的地区,避免重复出现选项
          .map(item => ({
            value: item.id,
            label: item.name,
            leaf: level >= this.levelCount[this.format] ||
              !(item.children &&
              item.children.length > 0)
          }))
      }
      resolve(nodes)
    },
    // 根据路径去查找children数据
    findChildrenByPath (path) {
      let data = areas
      let children = []
      // 遍历path,找出id匹配的地区
      // 并将找出的地区的children作为下一次遍历的area源数据
      path.forEach(id => {
        const area = data.find(item => item.id === id)
        if (area) {
          data = area.children || []
          children = data
        }
      })
      return children
    },
    onChange (value) {
      const panelRefs = this.$refs.region.$refs.panel
      // 获取选中的节点
      const checkedNodes = panelRefs.getCheckedNodes()[0]
      // 由于开启来了懒加载,且父子节点不关联
      // 当选中radio时,下一级不会自动加载数据
      // 手动调用源码的lazyLoad触发加载下一级
      panelRefs.lazyLoad(checkedNodes)
      let res = value && value.length > 0
        ? {
          value: value || [],
          label: checkedNodes ? checkedNodes.pathLabels : []
        }
        : null
      this.$emit('change', res)
    }
  }
};
</script>

<style></style>

问题解答:

为什么回显的setDefaultOptions只找出绑定的值的路径上的数据?

答:为了减小数据加载的计算量,减轻浏览器渲染压力

优化思考:

在处理问题二的时候,我灵光一闪,既然我能手动通知panel调用lazyLoad加载下一级数据,那么是不是不用通过setDefaultOptions方法去找出并设置默认值路径的选项,而是直接遍历默认值,根据当前遍历的id找出节点的children数据,再调用panel的lazyLoad方法加载下一级数据即可。有兴趣的小伙伴可以尝试尝试~