美文网首页前端开发那些事儿
ElementUI cascader级联动态加载回显和搜索看这个

ElementUI cascader级联动态加载回显和搜索看这个

作者: 四哥0819 | 来源:发表于2020-11-17 16:00 被阅读0次
    UI

    以下是思考和开发的过程,不感兴趣可以直接看使用文档。

    https://github.com/zhuss/lazy-cascader​github.com

    为什么要再写一篇呢?

    当然不是思想觉悟有多高,还不是因为产品提的需求,产品哭哭唧唧的说:“类目太多啦,我要有一个搜索的功能。”

    一开始我是拒绝的,毕竟上一次为了解决回显问题,耗费了一波本来就不多的头发,可是后来想一想,都是打工人,打工人和打工人应该是相亲相爱的一家人。

    既然接下这个锅,那就想办法解决吧。

    最开始,延续的原有的思路,既然回显可以,那搜索应该也不在话下,所以就埋头去研究官方文档,然后发现了 filter-method 和 before-filter 这两个方法。

    尝试过之后就pass掉了filter-method,这个只是在选择的时候判断节点是否匹配,不适合动态请求和处理数据。

    而 before-filter 方法是可以的,而且文档写的比较明确。

    筛选之前的钩子,参数为输入的值,若返回 false 或者返回 Promise 且被 reject,则停止筛选

    如果我们在方法中根据输入值去请求后端接口,拿到一组备选项,然后根据拿到的备选项动态更新我们的options参数,那么就可以在组件中筛选出我们想要的节点了吧,至于动态更新options的方法,可以同上一篇的回显逻辑类似。

    按照这个思路,同事在一番倒腾之后,确实可以实现动态搜索的需求。

    但是,这样处理是有不足的地方的。

    1、产品设计的UI和ElementUI的组件并不一致,交互方式也不一样。

    2、在动态更新options参数的时候会动态请求很多节点数据,而这些节点大部分都是不需要展示的。

    所以,为了解决这两个问题,我又陷入的沉思,甚至想不看ElementUI手动撸一个组件(想想而已)。

    那既然原有组件在UI上没办满足产品需求,那我们就自己写这个UI吧,顺着这个思路,那我们就需要用到一个关键的东西,级联面板。

    因为上图UI的组成部分就这么几个东西。

    • 输入框
    • Popover 弹出框
    • 搜索选择框
    • 级联选择面板

    级联选择面板的值仅仅是一个数据,也就是节点id的路径数组,那我们就必须根据这个值,然后遍历options得到对应的label的数组,显示在输入框中。

    通过搜索拿到的数据也可以拿到一个节点的值。

    那么我们只需要根据值的变化,动态请求需求展示的节点,获取到对应的label即可。

    于是就有了最紧要的一段代码。

    html

    /**格式化id=>object */
        async getObject(id) {
          let options = this.options;
          let nameArray = [];
          for (let i = 0; i < id.length; i++) {
            let index = options.findIndex(item => {
              return item[this.props.value] == id[i];
            });
            nameArray.push(options[index][this.props.label]);
            if (i < id.length - 1 && options[index].children == undefined) {
              let list = new Promise(resolve => {
                this.props.lazyLoad(id[i], list => {
                  resolve(list);
                });
              });
              this.$set(options[index], "children", await list);
              options = options[index].children;
            } else {
              options = options[index].children;
            }
          }
          return { value: id, label: nameArray };
        }
    
    

    解决了这个问题,基本上就已经实现了回显了。

    至于搜索,直接用的ElementUI的组件autocomplete就可以了,最后在仿照cascader的参数封装这个组件的,把需要的参数暴露出去就可以了。

    完整的代码看下

    <template>
      <div class="lazy-cascader" :style="{ width: width }">
        <!-- 禁用状态 -->
        <div
          v-if="disabled"
          class="el-input__inner lazy-cascader-input lazy-cascader-input-disabled"
        >
          <span class="lazy-cascader-placeholder" v-show="placeholderVisible">
            {{ placeholder }}
          </span>
          <div class="lazy-cascader-tags" v-if="props.multiple">
            <el-tag
              class="lazy-cascader-tag"
              type="info"
              disable-transitions
              v-for="(item, index) in labelArray"
              :key="index"
              closable
            >
              <span> {{ item.label.join(separator) }}</span>
            </el-tag>
          </div>
          <div class="lazy-cascader-label" v-else>
            <el-tooltip
              placement="top-start"
              :content="labelObject.label.join(separator)"
            >
              <span>{{ labelObject.label.join(separator) }}</span>
            </el-tooltip>
          </div>
        </div>
        <!-- 禁用状态 -->
        <!-- 可选状态 -->
        <el-popover v-else trigger="click" placement="bottom-start" ref="popover">
          <!-- 搜索 -->
          <div class="lazy-cascader-search">
            <el-autocomplete
              :style="{ width: width }"
              v-if="filterable"
              class="inline-input"
              prefix-icon="el-icon-search"
              label="name"
              v-model="keyword"
              :fetch-suggestions="querySearch"
              :trigger-on-focus="false"
              placeholder="请输入"
              @select="handleSelect"
            >
              <template slot-scope="{ item }">
                <div class="name">{{ item[props.label].join(separator) }}</div>
              </template>
            </el-autocomplete>
          </div>
          <!-- 搜索 -->
          <!-- 级联面板 -->
          <div class="lazy-cascader-panel">
            <el-cascader-panel
              ref="panel"
              v-model="current"
              :options="options"
              :props="currentProps"
              @change="change"
            ></el-cascader-panel>
          </div>
          <!-- 级联面板 -->
          <!--内容区域-->
          <div
            class="el-input__inner lazy-cascader-input"
            :class="disabled ? 'lazy-cascader-input-disabled' : ''"
            slot="reference"
          >
            <span class="lazy-cascader-placeholder" v-show="placeholderVisible">
              {{ placeholder }}
            </span>
            <div class="lazy-cascader-tags" v-if="props.multiple">
              <el-tag
                class="lazy-cascader-tag"
                type="info"
                disable-transitions
                v-for="(item, index) in labelArray"
                :key="index"
                closable
                @close="handleClose(item)"
              >
                <span> {{ item.label.join(separator) }}</span>
              </el-tag>
            </div>
            <div class="lazy-cascader-label" v-else>
              <el-tooltip
                placement="top-start"
                :content="labelObject.label.join(separator)"
              >
                <span>{{ labelObject.label.join(separator) }}</span>
              </el-tooltip>
            </div>
          </div>
          <!--内容区域-->
        </el-popover>
        <!-- 可选状态 -->
      </div>
    </template>
    

    js

    export default {
      props: {
        value: {
          type: Array,
          default: () => {
            return [];
          }
        },
        separator: {
          type: String,
          default: " > "
        },
        placeholder: {
          type: String,
          default: "请选择"
        },
        width: {
          type: String,
          default: "400px"
        },
        filterable: Boolean,
        disabled: Boolean,
        props: {
          type: Object,
          default: () => {
            return {};
          }
        }
      },
      data() {
        return {
          keyword: "",
          options: [],
          current: [],
          labelObject: { label: [], value: [] },
          labelArray: [],
          currentProps: {
            multiple: this.props.multiple,
            checkStrictly: this.props.checkStrictly,
            value: this.props.value,
            label: this.props.label,
            leaf: this.props.leaf,
            lazy: true,
            lazyLoad: this.lazyLoad
          }
        };
      },
      computed: {
        placeholderVisible() {
          if (this.current) {
            return this.current.length == 0;
          } else {
            return true;
          }
        }
      },
      watch: {
        current() {
          this.getLabelArray();
        },
        value(v) {
          this.current = v;
        }
      },
      created() {
        this.initOptions();
      },
      methods: {
        //搜索
        querySearch(query, callback) {
          this.props.lazySearch(query, list => {
            callback(list);
          });
        },
        //选中搜索下拉搜索项
        handleSelect(item) {
          if (this.props.multiple) {
            let index = this.current.findIndex(obj => {
              return obj.join() == item[this.props.value].join();
            });
            if (index == -1) {
              this.current.push(item[this.props.value]);
              this.$emit("change", this.current);
            }
          } else {
            //选中下拉选变更值
            if (
              this.current == null ||
              item[this.props.value].join() !== this.current.join()
            ) {
              this.current = item[this.props.value];
              this.$emit("change", this.current);
            }
          }
          this.keyword = "";
        },
        //初始化数据
        async initOptions() {
          this.props.lazyLoad(0, list => {
            this.$set(this, "options", list);
            if (this.props.multiple) {
              this.current = [...this.value];
            } else {
              this.current = this.value;
            }
          });
        },
        async getLabelArray() {
          if (this.props.multiple) {
            let array = [];
            for (let i = 0; i < this.current.length; i++) {
              let obj = await this.getObject(this.current[i]);
              array.push(obj);
            }
            this.labelArray = array;
            this.$emit("input", this.current);
            if (!this.disabled) {
              this.$nextTick(() => {
                this.$refs.popover.updatePopper();
              });
            }
          } else {
            this.labelObject = await this.getObject(this.current || []);
            this.$emit("input", this.current);
          }
        },
        /**格式化id=>object */
        async getObject(id) {
          let options = this.options;
          let nameArray = [];
          for (let i = 0; i < id.length; i++) {
            let index = options.findIndex(item => {
              return item[this.props.value] == id[i];
            });
            nameArray.push(options[index][this.props.label]);
            if (i < id.length - 1 && options[index].children == undefined) {
              let list = new Promise(resolve => {
                this.props.lazyLoad(id[i], list => {
                  resolve(list);
                });
              });
              this.$set(options[index], "children", await list);
              options = options[index].children;
            } else {
              options = options[index].children;
            }
          }
          return { value: id, label: nameArray };
        },
        //懒加载数据
        async lazyLoad(node, resolve) {
          let current = this.current;
          if (this.props.multiple) {
            current = [...this.current];
          }
          if (node.root) {
            resolve();
          } else if (node.data[this.props.leaf]) {
            resolve();
          } else if (node.data.children) {
            resolve();
          } else {
            this.props.lazyLoad(node.value, list => {
              node.data.children = list;
              if (this.props.multiple) {
                this.current = current;
              }
              resolve(list);
            });
          }
        },
        //删除多选值
        /**删除**/
        handleClose(item) {
          let index = this.current.findIndex(obj => {
            return obj.join() == item.value.join();
          });
          if (index > -1) {
            let node = this.$refs.panel.getCheckedNodes().find(n => {
              return n.value == this.current[index][this.current[index].length - 1];
            });
            if (node) {
              node.checked = false;
            }
            this.current.splice(index, 1);
            this.$emit("change", this.current);
          }
        },
        change() {
          this.$emit("change", this.current);
        }
      }
    };
    
    

    css

    .lazy-cascader {
      display: inline-block;
      width: 300px;
      .lazy-cascader-input {
        width: 100%;
        background: #fff;
        height: auto;
        min-height: 36px;
        padding: 5px;
        line-height: 1;
        cursor: pointer;
        > .lazy-cascader-placeholder {
          padding: 0 2px;
          line-height: 28px;
          color: #999;
          font-size: 14px;
        }
        > .lazy-cascader-label {
          padding: 0 2px;
          line-height: 28px;
          color: #606266;
          font-size: 14px;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
      }
      .lazy-cascader-input-disabled {
        background-color: #f5f7fa;
        border-color: #e4e7ed;
        color: #c0c4cc;
        cursor: not-allowed;
        > .lazy-cascader-label {
          color: #c0c4cc;
        }
        > .lazy-cascader-placeholder {
          color: #c0c4cc;
        }
      }
    }
    .lazy-cascader-tag {
      display: inline-flex;
      align-items: center;
      max-width: 100%;
      margin: 2px;
      text-overflow: ellipsis;
      background: #f0f2f5;
      > span {
        flex: 1;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      > .el-icon-close {
        -webkit-box-flex: 0;
        -ms-flex: none;
        flex: none;
        background-color: #c0c4cc;
        color: #fff;
      }
    }
    .lazy-cascader-panel {
      margin-top: 10px;
      display: inline-block;
    }
    

    其实,在自己封装组件的时候,也会不自觉的学习或者掌握一些东西,还是比较有趣的。

    比如,为了解决多选的时候Popover 弹出框错位的问题,看了Element 的源码,发现Popover组件有一个updatePopper方法。

    而且,封装组件需要考虑的问题比较多,倒不是说复杂,就是尽可能要全面。

    好了,这一篇就写到这里,至于适用性,我也不能保证,至少很好的解决了cascader级联动态加载的不足,而且简化了动态加载的方法,很方便的实现了回显和搜索。

    如果这正是你需要的,或者你预感自己会需要,可以先收藏,对于组件的不足之处也可以交流和讨论,以方便我去优化和改进。

    上一篇 👉 ElementUI cascader级联动态加载及回显

    相关文章

      网友评论

        本文标题:ElementUI cascader级联动态加载回显和搜索看这个

        本文链接:https://www.haomeiwen.com/subject/vyoxiktx.html