以下是思考和开发的过程,不感兴趣可以直接看使用文档。
https://github.com/zhuss/lazy-cascadergithub.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级联动态加载的不足,而且简化了动态加载的方法,很方便的实现了回显和搜索。
如果这正是你需要的,或者你预感自己会需要,可以先收藏,对于组件的不足之处也可以交流和讨论,以方便我去优化和改进。
网友评论