阅读 91

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

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级联动态加载及回显

作者:四哥0819

原文链接:https://www.jianshu.com/p/06385601003b

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐