自入行已开,都是做的vue + element项目开发,个人觉得这一套组合是针对pc端的项目是很流畅丝滑的,但是,最近开始的项目是vue + antd项目,问了一些同行业的大佬,这些大佬基本都是一句“vue + antd吗?莫名其妙的bug解决不了要和蚂蚁金服直接交涉”这样的调侃,反正我觉得antd对于vue框架来说是没有element这么简单易开发,但是,毕竟是成熟的两个重量框架,项目已经使用了,计算遇到问题也是需要尽力去解决,下面我就针对a-select这个组件自行封装一个远程搜索组件,目标是为了页面的复用和简化代码。
一、使用场景
图一 项目使用示例图
图二 antd官网组件
图一为我项目中具体的使用,当获取焦点时会远程搜索相关内容,也可以支持输入内容搜索,支持搜索内容下拉懒加载,当然,也进行了简单的防抖处理
二、代码示例
①、在需要用到该组件的页面进行模板编写、组件引入,注册,比如使用的页面命名为页面A
// 页面A
// html 模板
<a-form-item label="船舶:">
<remote-select
v-model="filter.imo"
:allow-clear="true"
field-name="vessel"
show-word="label"
:default-value="defaultList.imo" // 2021/12/31更新,默认值通过props传给子组件
@change="filterChange('imo')"
/>
</a-form-item>
// 组件引入
import remoteSelect from '@/components/remoteSelect/index.vue'
// 组件注册
components: { remoteSelect }
methods: {
// 2021/12/31更新, 这里是这个例子的一个使用方法,点击页面的编辑按钮,将本来的数据当成默认值传给子组件
// 点击编辑按钮
openPortOfCallEdit(arg) {
this.drawerTitle = '编辑靠港记录'
this.cachePurpose = arg.identifiedPurpose
this.imoDisabled = true
this.placeNameDisabled = false
this.cahceSelectedItem = JSON.stringify(arg)
this.cahceSelectedItem = JSON.parse(this.cahceSelectedItem)
for (const key in this.selectedItem) {
if (key === 'imo') {
this.selectedItem[key].key = arg.imo
this.selectedItem[key].label = arg.imo
// 2021/12/31 这里进行默认值格式的转化,同封装组件的格式保持一致
this.defaultList[key] = {
key: arg.imo,
label: arg.imo
}
} else if (key === 'portId') {
this.selectedItem[key].key = arg.portId
this.selectedItem[key].label = arg.portName
// 2021/12/31 这里进行默认值格式的转化,同封装组件的格式保持一致
this.defaultList[key] = {
key: arg.portId,
label: arg.portName
}
} else if (key === 'placeName') {
// 这里进行靠港目的区分,因为为REP时,用的控件是自行封装的控件,其他的为自带的a-select组件
if (arg.identifiedPurpose === 'REP') {
this.selectedItem[key].key = arg.yardId
} else {
this.selectedItem[key].value = arg.berthTerminalId
}
this.selectedItem[key].label = arg.placeName
this.defaultList[key] = {
key: arg.yardId,
label: arg.placeName
}
} else {
this.selectedItem[key] = arg[key]
}
}
this.ataEdit = arg.ataTime
this.atbEdit = arg.atbTime
this.atdEdit = arg.atdTime
this.isShowEditItemModal = true
},
}
②、remote自定义组件
<template>
<a-select
:key="keyItem" // 1
:value="currentValue" // 2
label-in-value // 3
show-search // 4
style="width: 100%"
:default-active-first-option="false" // 5
:show-arrow="false" // 6
:filter-option="false" // 7
:disabled="disabled" // 8
:allow-clear="clearable" // 9
:option-label-prop="showWord" // 10
@popupScroll="handlePopupScroll" // 11
@focus="handleFocus" // 12
@search="handleSearch" // 13
@change="handleChange" // 14
@blur="handlerBlur" // 15
>
<a-select-option
v-for="item in data" // 16
:key="item.value"
:value="item.value" // 17
:label="item.label" // 18
>
<a-tooltip>
<template slot="title">{{ item.label }}</template>
<div class="option-item">
<div class="option-label">
<span class="label-name">{{ item.label }}</span>
<span v-if="ifShowFieldName" class="field-name">({{ fieldName }})</span>
</div>
<div v-if="fieldName === 'vessel' || fieldName === 'port'">{{ item.value }}</div>
</div>
</a-tooltip>
</a-select-option>
</a-select>
</template>
<script>
import { postAction } from '@/api/manage'
import { typeOf } from '@/utils/commonTools.js'
export default {
props: {
// 返回给父组件的值
value: {
type: [String, Number, Array, Object]
},
// 2021/12/31更新, 默认值
defaultValue: {
type: Object,
default: () => {}
},
// 后台接口根据该字段搜索不同的内容
fieldName: {
type: String,
default: ''
},
// 下拉框选中内容回填字段
showWord: {
type: String,
default: 'label'
},
disabled: {
type: Boolean,
default: false
},
ifShowFieldName: {
type: Boolean,
default: false
},
allowClear: {
type: Boolean,
default: false
}
},
data() {
return {
data: [],
currentValue: '',
searchStr: '',
listQuery: {
pageNo: 1,
pageSize: 10
},
pages: 1,
timer: null,
keyItem: 100 // 这个参数是为了使下拉框的滚动条回到顶端,会更加的秏性能,但是目前没有找到别的办法解决这个问题
}
},
computed: {
// 这里的计算属性:输入框可以清除功能,手动取消清除属性
// 因为a-select组件加上allowClear属性时,初始化的时候也会有清除的小图标,以及清除后还会有小图标,必须再次点击清除图标才会消失的bug
clearable() {
if (this.allowClear) {
if (!this.currentValue || !this.currentValue.key) {
return false
} else {
return true
}
} else {
return false
}
}
},
watch: {
// 这里是因为可能会有默认值的情况,所以进行特别的处理
value: {
handler(newVal, oldVal) {
this.$nextTick(() => {
this.currentValue = typeOf(newVal) === 'object' ? newVal : typeOf(newVal) === 'string' || typeOf(newVal) === 'number' ? { key: newVal } : ''
})
},
immediate: true
},
//2021/12/31更新, 如果有默认值,进行回调操作
defaultValue: {
handler(newVal, oldVal) {
if (Object.keys(newVal).length) {
this.currentValue = newVal
}
}
// immediate: true
}
},
mounted() {},
beforeDestroy() {
clearTimeout(this.timer)
this.timer = null
},
methods: {
// 这个方法是我个人项目url地址的映射处理
getUrlFn() {
switch (this.fieldName) {
case 'vessel':
return '/ship/findShipPage'
case 'port':
return '/port/findPortPage'
case 'berth':
return '/portBerthTerminal/findPortBerthTerminalPage'
case 'shipyard':
return '/shipyard/findShipyardPage'
}
},
// 下拉滚动加载
handlePopupScroll(e) {
const { target } = e
var total = target.scrollTop + target.offsetHeight
var scrollHeight = target.scrollHeight
// this.listQuery.pageNo是当前页 this.pages是总页数
if (total === scrollHeight && this.listQuery.pageNo < this.pages) {
this.listQuery.pageNo++
this.getSearchResult(this.searchStr, data => (this.data = this.data.concat(data)))
}
},
// 获取数据列表 调接口
getSearchResult(value, cb) {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
this.timer = setTimeout(() => {
const params = {
query: value,
...this.listQuery
}
postAction(this.getUrlFn(), params).then(res => {
const result = res.data.records
this.pages = res.data.pages
const data = []
result.forEach(item => {
data.push({
// 船舶imo字段,港口取portCode字段, 船厂和泊位取id字段
value: item.imo || item.portCode || item.id,
// 船舶imo字段,港口取portName字段, 船厂取yardName字段,泊位取berthTerminalName字段
label: item.shipName || item.portName || item.yardName || item.berthTerminalName
})
})
cb(data)
})
}, 500)
},
// 获取焦点的回调
handleFocus(value) {
this.searchStr = value
this.listQuery.pageNo = 1
this.data.length = 0
this.getSearchResult(value, data => (this.data = data))
},
// 失焦的时候
handlerBlur() {
this.keyItem = Math.random()
},
// 输入内容的时候触发
handleSearch(value) {
this.listQuery.pageNo = 1
this.data.length = 0
this.searchStr = value
this.getSearchResult(value, data => (this.data = data))
},
// 选中下拉框内容
handleChange(value, option) {
this.currentValue = value
this.listQuery.pageNo = 1
this.data.length = 0
this.$emit('input', this.currentValue)
this.$emit('change')
}
}
}
</script>
<style lang="scss" scoped>
.option-item {
display: flex;
justify-content: space-between;
}
.option-label {
display: flex;
// width: calc(100% - 70px);
// .label-name {
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
// }
}
.field-name {
margin-left: 5px;
color: #1890ff;
}
</style>
代码中的数字的释义:
1:个人不才,每次获取焦点的时候的下拉滚动条都保持在上一次滚动过的位置,没有通过dom的方式进行还原,所以就采用极端的方式,这里有大佬可以帮忙指点就好了;
2:指定当前选中的条目;
3:参照官方,是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 string
变为 {key: string, label: vNodes}
的格式;
4:使单选模式可搜索;
5:是否默认高亮第一个选项;
6:是否显示下拉小箭头;
7:是否根据输入项进行筛选。当其为一个函数时,会接收 inputValue
option
两个参数,当 option
符合筛选条件时,应返回 true
,反之则返回 false;
8:是否禁用;
9:是否可以清除,这里也有bug,详情可以参照代码中的解释用法;
10:回填到选择框的 Option 的属性值,默认是 Option 的子元素。比如在子元素需要高亮效果时,此值可以设为 value
。这里我进行的props传值可控处理;
11:下拉滚动懒加载方法,具体参照官方说明;
12:获得焦点时回调;
13:文本框值变化时回调;
14:选中 option,或 input 的 value 变化(combobox 模式下)时,调用此函数;
15:失去焦点的时回调;这里同数字1处有关联,失焦的时候销毁重新传染一个a-select组件
16:远程搜索的结果组成的数组结构数据
17:option的属性value
18:option的属性label
③、具体的使用
// 页面A
methods: {
// 这里是remote这个组件选中了对应的项,然后父组件通过同名函数箭筒到的回调,在里面进行一些逻辑操作
filterChange(type) {
// 无论控件选中前后的数值是否一致,都会清空后面关联的控件内容
type === 'imo' ?
this.filter.shipVoyageId = undefined :
this.filter.portBerthTerminalId = undefined
this.ipagination.current = 1
this.getPortOfCallData()
},
}
大概就是这样一些情况,封装的过程中的确遇到了很多的坑,比如下拉滚动懒加载的滚动条的问题、选中options的某一项时的回填显示问题、添加清除内容属性时初始化就有清除图标的问题,我都有在里面进行备注使用的解释,不完美,还有很多的地方需要进行优化更改,也一直在进行优化中,望看见的朋友可以提出自己的建议!!
tips:
①2021/12/31更新(更新的内容见代码注释,注释的开头用的是2021/12/31更新)
上一个版本办忽略了一个默认值的问题,要是有默认值,应该将默认值回填到选中框内,这就需要将默认值的格式同其本身转换的格式保持一致