前言
前端实现连词搜索下拉效果,输入框输入后根据后端返回的数据实现下拉搜索封装。
- 使用技术 vue2+element ui
效果如下

实现思路
- 使用watch监听输入框的值,当值改变时,传递给父组件
- 父组件接收子组件传递的数据,请求后端接口
- 在接口请求前和响应后做加载效果处理
- 监听滚动区域滚动事件,使用滚动到底部加载下一页
代码实现
html
<template>
<div class="conjunctionSearch-container">
<!-- 搜索输入框 -->
<div class="conjunctHead">
<el-input clearable v-model="keywords" :placeholder="placeholder" @focus="inputFocus">
<el-button slot="append" icon="el-icon-search" size="mini" @click="btnClick" class="searchBtn blueText"></el-button>
</el-input>
</div>
<!-- 下拉结果区 -->
<div class="conjunctContainer" @scroll="scrollEvent" v-if="isCloseConjunction&&(scrollLoading!='state'||keywords)">
<!-- 搜索加载 -->
<slot name="loading">
<div class="loadBox searchLoad" v-if="searchLoading||scrollLoading=='state'">
<div class="loadIcon"></div>
<div class="loadText">加载中...</div>
</div>
</slot>
<!-- 用户自定义结果内容 -->
<div class="resultBox">
<slot>
<div class="resultDefItem" v-for="item in option" :key="item[value]" @click="selectItem(item)">
<div class="resultDefName" :class="{'actrveDef': selectData[value]==item[value]}">{{ item[label] }}</div>
<div class="resultDefIcon" v-if="selectData[value]==item[value]">
<i class="el-icon-circle-check"></i>
</div>
</div>
</slot>
</div>
<!-- 滚动加载 -->
<slot name="scrollLoading">
<div class="loadBox sollLoad" v-if="scrollLoading=='fetch'&&isScrollEnd">
<div class="loadIcon"></div>
</div>
</slot>
<!-- 滚动到底 -->
<slot name="scrollEnd">
<div class="endText" v-if="scrollLoading=='end'&&!(isNoDataShow||noData)">没有更多了</div>
</slot>
<!-- 暂无数据 -->
<slot name="noResult" v-if="isNoDataShow||(noData&&scrollLoading!='state'&&scrollLoading!='fetch')">
<div class="noResultBox">
<img src="@/assets/img/tableEmpty.png" alt="" srcset="">
<div class="noResultText">暂无数据</div>
</div>
</slot>
</div>
</div>
</template>
js
<script>
/**
* 连词搜索功能
*
* 使用方法:
*
* 1.
* <ConjunctionSearch :option="enterpriseList" label="companyName" value="companyInfoId" @itemClick="selectItem" @search="searchChange" @scrollEnd="scrollEnd" :loading="searchLoading" placeholder="企业名称/统一社会信用代码" />
*
2.
<ConjunctionSearch :option="enterpriseList" label="companyName" value="companyInfoId" @itemClick="selectItem" @search="searchChange" @scrollEnd="scrollEnd" :loading="searchLoading" ref="conjunctionSearchRef" placeholder="企业名称/统一社会信用代码" >
<div>自定义结果内容</div>
<ConjunctionSearch/>
// 搜索
searchChange(val) {
this.reqParams.keywords = val
this.reqParams.startIndex = 1
this.getPageList()
}
//滚动到底部
scrollEnd() {
this.reqParams.startIndex += 1
if (this.enterpriseList.length >= this.total) {
this.searchLoading = 'end'
return
}
this.getPageList()
}
// 请求接口
getPageList() {
this.searchLoading = 'fetch'
advanceSearchApi(this.reqParams).then(res => {
if (res.code == 0) {
if (this.reqParams.startIndex == 1) {
this.enterpriseList = res.data.dataList
} else {
this.enterpriseList = this.enterpriseList.concat(res.data.dataList)
}
this.total = res.data.total
}
this.searchLoading = this.enterpriseList.length >= this.total ? 'end' : 'success'
})
}
属性
@loading 加载中效果 state-开始 fetch-加载中 success-加载完成 end-结束
@placeholder 输入框占位符
@option 下拉选择项
@label 下拉选项的label
@value 下拉选项的value
@noData 是否暂无数据
事件
@itemClick 选中项
@search 搜索
@scrollEnd 滚动到底部
*
*
*/
export default {
props: {
// 加载中效果 state-开始 fetch-加载中 success-加载完成 end-结束
loading: {
type: String,
default: 'state'
},
// 输入框占位符
placeholder: {
type: String,
default: '请输入'
},
// 下拉选择项
option: {
type: Array,
default: () => []
},
// 下拉选项的label
label: {
type: String,
default: 'label'
},
// 下拉选项的value
value: {
type: String,
default: 'value'
},
// 是否暂无数据
noData: {
type: Boolean,
default: false
}
},
data() {
return {
// 搜索关键词
keywords: '',
// 防抖定时器
timer: null,
// 搜索时的加载状态
searchLoading: false,
// 滚动加载中效果 state-开始 fetch-加载中 success-加载完成 end-结束
scrollLoading: 'state',
// 控制下拉框显示/隐藏
isCloseConjunction: false,
// 是否暂无数据
isNoDataShow: false,
// 选中项后赋值给keywords不请求接口
isClick: false,
// 选中的数据
selectData: {},
// 是否滚动到底部
isScrollEnd: false
}
},
watch: {
// 搜索关键词
keywords() {
// 选中项后赋值给keywords不请求接口
if (this.isClick) return
this.isCloseConjunction = true
// 防抖
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.$emit('search', this.keywords)
}, 500)
},
// 搜索时的加载状态
loading: {
handler(val) {
this.scrollLoading = val
if (val == 'fetch') {
this.searchLoading = true
} else if (['success', 'end'].includes(val)) {
this.searchLoading = false
this.isScrollEnd = false
}
}
},
// 默认数据
option: {
handler(val) {
this.isNoDataShow = val.length == 0
}
}
},
mounted() {
document.addEventListener('click', this.handleClickOutside)
},
beforeDestroy() {
document.removeEventListener('click', this.handleClickOutside);
},
methods: {
// 点击外部关闭下拉框
handleClickOutside(e) {
if (!this.$el.contains(e.target)) {
this.isCloseConjunction = false
}
},
// 滚动到底部加载
scrollEvent(e) {
if (['fetch', 'end'].includes(this.scrollLoading)) return
let scrollTop = e.target.scrollTop
let scrollHeight = e.target.scrollHeight
let clientHeight = e.target.clientHeight
if (scrollTop + clientHeight >= scrollHeight) {
this.scrollLoading = 'fetch'
this.isScrollEnd = true
this.$emit('scrollEnd')
}
},
// 选中结果
selectItem(row) {
this.selectData = row
this.keywords = row[this.label]
this.isClick = true
this.$emit('itemClick', row)
this.close()
},
// 关闭下拉框
close() {
this.isCloseConjunction = false
},
// 输入框聚焦
inputFocus() {
this.isCloseConjunction = true
this.isClick = false
},
// 按钮点击
btnClick() {
if (!this.keywords) {
return
}
this.$emit('search', this.keywords)
},
}
}
</script>
css
<style lang="scss" scoped>
.conjunctionSearch-container {
width: 100%;
position: relative;
z-index: 2000;
.conjunctHead {
width: 100%;
}
.conjunctContainer {
width: 100%;
max-height: 300px;
overflow: hidden;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 5px;
margin-top: 10px;
background: #fff;
box-shadow: 0 0 10px #ccc;
position: absolute;
z-index: 2100;
.searchLoad {
margin: 50px 0;
}
.sollLoad {
margin: 20px 0;
}
.endText {
margin: 10px 0;
text-align: center;
color: #ccc;
font-size: 12px;
}
.noResultBox {
margin: 30px 0;
text-align: center;
color: #ccc;
font-size: 12px;
.noResultText {
margin-top: 15px;
}
}
.loadBox {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.loadIcon {
width: 20px;
height: 20px;
border-radius: 50%;
border-top: 1px solid #0052cc;
animation: loadAn 1s linear infinite;
}
.loadText {
margin-top: 10px;
color: #0052cc;
}
}
.resultDefItem {
padding: 10px 10px;
box-sizing: border-box;
font-size: 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
.resultDefName {
white-space: normal;
text-overflow: ellipsis;
overflow: hidden;
}
.actrveDef {
color: #35ab6c;
}
.resultDefIcon {
color: #35ab6c;
}
&:hover {
background-color: #f2f5f9;
}
}
}
}
@keyframes loadAn {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>