前端实现连词搜索下拉效果

前言

前端实现连词搜索下拉效果,输入框输入后根据后端返回的数据实现下拉搜索封装。

  • 使用技术 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>
相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax