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

前言

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

  • 使用技术 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>
相关推荐
卸任2 小时前
从0到1搭建react-native自动更新(OTA和APK下载)
前端·react native·react.js
OpenTiny社区2 小时前
OpenTiny NEXT 训练营实操体验 | 四步将你的 Web 应用升级为智能应用
前端·开源·ai编程
持续迷茫3 小时前
lint-staged 中 --verbose 选项的深度解析
前端·git
拜无忧3 小时前
带有“水波纹”或“扭曲”效果的动态边框,进度条
前端
12码力3 小时前
open3d 处理rgb-d深度图
前端
小猪猪屁3 小时前
WebAssembly 从零到实战:前端性能革命完全指南
前端·vue.js·webassembly
EMT3 小时前
记一个Vue.extend的用法
前端·vue.js
RaidenLiu3 小时前
Riverpod 3:组合与参数化的进阶实践
前端·flutter
jason_yang3 小时前
vue3自定义渲染内容如何当参数传递
前端·javascript·vue.js