点击输入框,底部弹出下拉框显示输入建议
需求
- 点击输入框时,或者输入内容时,底部弹出下拉框显示输入建议。
- 选择某项输入建议后,输入框显示该内容。
- 输入建议列表根据输入的内容模糊搜索返回;若输入内容为空,则返回全部输入建议。
效果类似于搜索引擎的搜索栏:
分析
<input />
原生属性 autocomplet
,若为 on
则下拉框形式展示该输入框的输入历史,off
则不展示。
若要自定义输入建议列表,则不能直接使用该属性。
经研究,发现 el-autocomplete 组件 满足需求。
解决
封装组件
由于页面上会使用多个 el-autocomplet
,为了便于复用,封装为 InputLoadMore 组件。
模糊搜索返回输入建议列表的接口,可以通过 props
传递接口地址,封装到 search.js 中。
代码如下:
InputLoadMore/index.js
<template>
<el-autocomplete
:ref="refName"
:value-key="valueKey"
v-scrollLoad="selectLoadMore"
v-model="autocompleteVal"
v-trim
clearable
:fetch-suggestions="querySearch"
:placeholder="placeholder"
:trigger-on-focus="isTriggerFocus"
:disabled="isDisabled"
:popper-append-to-body="false"
@select="handleSelect"
@focus="searchFocus"
/>
</template>
<script>
import { queryData } from "./search.js";
export default {
name: "InputLoadMore",
props: {
refName: {
type: String,
required: true,
},
isDisabled: {
type: Boolean,
default: false,
},
// v-model的绑定值
modelVal: {
type: String,
required: true,
},
// 搜索接口
queryUrl: {
type: String,
required: true,
},
// 后端定义的联想的key
searchKey: {
type: String,
default: "value",
},
// 搜索额外需要的参数,若有分页,要包含 pageNum 和 pageSize
additionParas: {
type: Object,
default: {
pageNum: 1,
pageSize: 10,
},
},
// 输入建议对象中用于显示的键名
valueKey: {
type: String,
default: "value",
},
// 默认点击输入框就列出输入建议,反之,输入内容才触发输入建议
isTriggerFocus: {
type: Boolean,
default: true,
},
// 是否是滑动分页加载数据
isScrollPage: {
type: Boolean,
default: true,
},
placeholder: {
type: String,
default: "请输入",
},
},
data() {
return {
// autocomplete 组件绑定值
autocompleteVal: "",
pageNum: 1,
total: 0,
};
},
watch: {
autocompleteVal(val) {
this.$emit("input-value", val);
},
modelVal: {
handler(val) {
if (val && val !== "null") {
this.autocompleteVal = val;
}
},
immediate: true,
},
},
methods: {
// 点击输入框时,隐藏下拉框
searchFocus() {
const el = this.$refs[this.refName];
if (el) {
el.activated = false;
}
},
// 加载(第一页)数据
async querySearch(queryString, cb) {
try {
let para = {
...this.additionParas,
[this.searchKey]: queryString,
};
if (this.isScrollPage) {
// 输入框失去焦点后再次获焦时,重置页码
this.pageNum = 1;
para.pageNum = this.pageNum;
}
let { data } = await queryData(para, this.queryUrl);
if (data.rows) {
let arr = [];
data.rows.forEach((item) => {
// 下拉框选项要转为字符串,避免选择后,导致 autocomplete 组件绑定值为数字而报错
arr.push({ [this.valueKey]: item.toString() });
});
// 若有数据,则显示,否则隐藏下拉框
if (arr.length) {
this.$refs[this.refName].activated = true;
cb(arr);
} else {
// 无数据,不显示下拉框
cb([]);
}
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
// 选择后触发
handleSelect(item) {
// console.log("handleSelect:", item);
},
// 加载更多
async selectLoadMore() {
if (!this.isScrollPage) return;
const curList = this.$refs[this.refName].$data.suggestions;
if (Number(this.total) <= curList.length) return;
this.pageNum++;
try {
let { data } = await queryData(
{
...this.additionParas,
[this.searchKey]: this.autocompleteVal,
pageNum: this.pageNum,
},
this.queryUrl
);
if (data.rows) {
const arr = data.rows.map((item) => {
return { [this.valueKey]: item.toString() };
});
// 将数据添加到下拉列表
this.$refs[this.refName].$data.suggestions = curList.concat(arr);
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
},
};
</script>
InputLoadMore/search.js
import request from "@/utils/request";
// 模糊搜索查询元件规格数据
export const queryData = (params, queryUrl) => {
return request({
url: queryUrl,
method: "get",
params,
});
};
注意的坑
下拉框不显示选项数据?
阅读文档发现,value-key
表示键名,这说明每条选项数据必须是对象。
因此,需要改造下接口返回的数据,不能直接 cb(data.rows)
,要把它转为对象,如下所示:
相关代码
// 加载(第一页)数据
async querySearch(queryString, cb) {
try {
let para = {
...this.additionParas,
[this.searchKey]: queryString,
};
if (this.isScrollPage) {
// 输入框失去焦点后再次获焦时,重置页码
this.pageNum = 1;
para.pageNum = this.pageNum;
}
let { data } = await queryData(para, this.queryUrl);
if (data.rows) {
let arr = [];
data.rows.forEach((item) => {
// 下拉框选项要转为字符串,避免选择后,导致 autocomplete 组件绑定值为数字而报错
arr.push({ [this.valueKey]: item.toString() });
});
// 若有数据,则显示,否则隐藏下拉框
if (arr.length) {
this.$refs[this.refName].activated = true;
cb(arr);
} else {
// 无数据,不显示下拉框
cb([]);
}
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
怎样实现鼠标滚动到底部显示下一页?
封装自定义指令 v-scrollLoad
,给 .el-autocomplete-suggestion__wrap
盒子添加监听事件:当输入建议列表 .el-autocomplete-suggestion__wrap .el-autocomplete-suggestion__list
滚动到底部时,执行加载更多函数 selectLoadMore()
。
directive/scroll/scrollLoad.js
export default {
// 写法一:
bind(el, binding, vnode) {
const wrapDom = el.querySelector(".el-autocomplete-suggestion__wrap");
const listDom = el.querySelector(
".el-autocomplete-suggestion__wrap .el-autocomplete-suggestion__list"
);
wrapDom.addEventListener(
"scroll",
(e) => {
const condition =
wrapDom.offsetHeight + wrapDom.scrollTop + 10 - listDom.offsetHeight;
if (condition > 0 && !vnode.context.loading) {
//滚动到底部则执行滚动方法,binding.value就是v-scrollLoad绑定的值,加()表示执行绑定的方法
binding.value();
}
},
false
);
},
// 写法二:
// bind(el, binding, vnode) {
// function handleScroll(e) {
// let isBottom =
// e.target.clientHeight + e.target.scrollTop >=
// e.target.scrollHeight - 40;
// if (isBottom && !vnode.context.loading) {
// binding.value();
// }
// }
// // 监听滚动
// let wrapDom = el.querySelector(".el-autocomplete-suggestion__wrap");
// el.__handleScroll__ = handleScroll;
// el.__wrapDom__ = wrapDom;
// wrapDom.addEventListener("scroll", handleScroll, false);
// },
// unbind(el, binding, vnode) {
// // 解除事件监听
// el.__wrapDom__.removeEventListener("scroll", el.__handleScroll__, false);
// },
};
directive/index.js
import scrollLoad from "./scroll/scrollLoad";
const install = function (Vue) {
Vue.directive("scrollLoad", scrollLoad);
};
export default install;
加载更多函数的本质:把新页的数据拼接到当前下拉框列表的底部
当前下拉框列表:this.$refs[this.refName].$data.suggestions
。
要注意的是:如果没有封装独立的组件,页面上有多个 el-autocomplete,且每个 el-autocomplete 的 ref
值相同(假如都设为 autocomplete
),则 this.$refs.autocomplete
得到的是数组。
加载更多函数
// 加载更多
async selectLoadMore() {
if (!this.isScrollPage) return;
const curList = this.$refs[this.refName].$data.suggestions;
if (Number(this.total) <= curList.length) return;
this.pageNum++;
try {
let { data } = await queryData(
{
...this.additionParas,
[this.searchKey]: this.autocompleteVal,
pageNum: this.pageNum,
},
this.queryUrl
);
if (data.rows) {
const arr = data.rows.map((item) => {
return { [this.valueKey]: item.toString() };
});
// 将数据添加到下拉列表
this.$refs[this.refName].$data.suggestions = curList.concat(arr);
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
分页的坑
点击输入框获取到输入建议后,滚动到下拉框底部加载更多获取下一页数据,此时点击输入框外区域失焦,再点击输入框获取输入建议,发现:失焦后请求数据的参数 pageNum
不是 1,而是继续上次的 pageNum++
(比如3)。
原因是:没有重置 pageNum
。
解决:给初次获取数据的函数,加上重置代码即可,如下所示:
if (this.isScrollPage) {
this.pageNum = 1; // 输入框失去焦点后再次获焦时,重置页码
para.pageNum = this.pageNum;
}
自定义接口参数,降低耦合性
如果在组件中把模糊查询的接口参数写死,则不利于复用组件。
该接口的参数,由以下两部分组成:
searchKey
:要模糊查询的值。默认为value
;additionParas
:额外的参数。默认有分页功能,{pageNum: 1, pageSize: 10}
。若有其他参数,可一并传入。
相关代码
props: {
// 后端定义的联想的key
searchKey: {
type: String,
default: "value",
},
// 搜索额外需要的参数,若有分页,要包含 pageNum 和 pageSize
additionParas: {
type: Object,
default: {
pageNum: 1,
pageSize: 10,
},
},
},
methods: {
// 加载(第一页)数据
async querySearch(queryString, cb) {
try {
let para = {
...this.additionParas,
[this.searchKey]: queryString,
};
if (this.isScrollPage) {
// 输入框失去焦点后再次获焦时,重置页码
this.pageNum = 1;
para.pageNum = this.pageNum;
}
let { data } = await queryData(para, this.queryUrl);
if (data.rows) {
let arr = [];
data.rows.forEach((item) => {
// 下拉框选项要转为字符串,避免选择后,导致 autocomplete 组件绑定值为数字而报错
arr.push({ [this.valueKey]: item.toString() });
});
// 若有数据,则显示,否则隐藏下拉框
if (arr.length) {
this.$refs[this.refName].activated = true;
cb(arr);
} else {
// 无数据,不显示下拉框
cb([]);
}
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
// 加载更多
async selectLoadMore() {
if (!this.isScrollPage) return;
const curList = this.$refs[this.refName].$data.suggestions;
if (Number(this.total) <= curList.length) return;
this.pageNum++;
try {
let { data } = await queryData(
{
...this.additionParas,
[this.searchKey]: this.autocompleteVal,
pageNum: this.pageNum,
},
this.queryUrl
);
if (data.rows) {
const arr = data.rows.map((item) => {
return { [this.valueKey]: item.toString() };
});
// 将数据添加到下拉列表
this.$refs[this.refName].$data.suggestions = curList.concat(arr);
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
},
若没有输入建议,则下拉框会一闪而过,然后只显示一半的高度
解决方案有两个:
- 若无输入建议,则加载状态结束后,下拉框内容显示
暂无数据
; - (推荐)若无输入建议,则不显示下拉框。输入建议存在时,才显示下拉框。
经对比,发现采用第二种方案,页面会更简洁。但有个弊端:无法显示获取输入建议的加载状态。
方案二思路:页面初始隐藏下拉框,只有获取的输入建议长度不为 0 时,才让下拉框显示。
控制下拉框的显隐,有两种方法:
- 给下拉框盒子
.el-autocomplete-suggestion
设置样式:display: none
隐藏,display: block
显示; - (推荐)el-autocomplete 组件的
activated
:为 true 则下拉框列表显示,反之隐藏。
经测试,发现方法一会导致页面卡顿、且下拉框只显示一半高度。因此推荐方法二。
相关代码
<template>
<!-- 方法一:给组件加上 @focus="searchFocus" -->
<el-autocomplete
:ref="refName"
:value-key="valueKey"
v-scrollLoad="selectLoadMore"
v-model="autocompleteVal"
v-trim
clearable
:fetch-suggestions="querySearch"
:placeholder="placeholder"
:trigger-on-focus="isTriggerFocus"
:disabled="isDisabled"
:popper-append-to-body="false"
@select="handleSelect"
@focus="searchFocus"
/>
</template>
<script>
export default: {
methods: {
// 点击输入框时,隐藏下拉框
searchFocus() {
const el = this.$refs[this.refName];
if (el) {
el.activated = false;
}
},
async querySearch(queryString, cb) {
try {
let para = {
...this.additionParas,
[this.searchKey]: queryString,
};
if (this.isScrollPage) {
this.pageNum = 1;
para.pageNum = this.pageNum;
}
let { data } = await queryData(para, this.queryUrl);
if (data.rows) {
let arr = [];
data.rows.forEach((item) => {
arr.push({ [this.valueKey]: item.toString() });
});
// 若有数据,则显示,否则隐藏下拉框
if (arr.length) {
// 方法一:会导致下拉框显示样式错乱
// document.getElementsByClassName(
// `autocomplete-${this.refName}`
// )[0].style.display = "block";
// 方法二:
this.$refs[this.refName].activated = true;
cb(arr);
} else {
cb([]);
}
}
this.total = data.totle || 0;
} catch (err) {
console.log({ err });
}
},
}
}
</script>
<style lang="scss" scoped>
// 方法一:初始隐藏下拉框
// ::v-deep .el-autocomplete-suggestion {
// display: none;
// }
</style>
滚动防抖
滚动下拉框触底加载下一页,(有时)会触发多次selectLoadMore函数,性能消耗大。
因此要给 v-scrollLoad
指令设置防抖函数。
directive/scroll/scrollLoad.js 设置防抖
export default {
/* 原代码 */
// bind(el, binding, vnode) {
// const wrapDom = el.querySelector(".el-autocomplete-suggestion__wrap");
// const listDom = el.querySelector(
// ".el-autocomplete-suggestion__wrap .el-autocomplete-suggestion__list"
// );
// wrapDom.addEventListener(
// "scroll",
// (e) => {
// const condition =
// wrapDom.offsetHeight + wrapDom.scrollTop + 10 - listDom.offsetHeight;
// // 节流
// if (condition > 0 && !vnode.context.loading) {
// //滚动到底部则执行滚动方法,binding.value就是v-scrollLoad绑定的值,加()表示执行绑定的方法
// binding.value();
// }
// },
// false
// );
// },
/* 加入防抖:避免滚动到底时,多次触发加载下一页的函数 */
bind(el, binding, vnode) {
// 防抖函数
function debounce(func, wait, immediate) {
// func 函数,wait 时间周期
var timeout;
return function () {
var that = this,
args = arguments;
var later = function () {
timeout = null;
if (!immediate) {
func.apply(that, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(that, args);
}
};
}
const wrapDom = el.querySelector(".el-autocomplete-suggestion__wrap");
const listDom = el.querySelector(
".el-autocomplete-suggestion__wrap .el-autocomplete-suggestion__list"
);
// 将滚动事件通过参数传入防抖函数中
var myEfficientFn = debounce(function () {
const condition =
wrapDom.offsetHeight + wrapDom.scrollTop + 10 - listDom.offsetHeight;
if (condition > 0 && !vnode.context.loading) {
//滚动到底部则执行滚动方法,binding.value就是v-scrollLoad绑定的值,加()表示执行绑定的方法
binding.value();
}
}, 250);
wrapDom.addEventListener("scroll", myEfficientFn, false);
},
};
效果:
参考链接
本文来自博客园,作者:shayloyuki,转载请注明原文链接:https://www.cnblogs.com/shayloyuki/p/17832213.html
posted on 2023-11-15 11:53 shayloyuki 阅读(593) 评论(0) 编辑 收藏 举报