特性:
- 支持本地记录搜索关键词
- 后台接口匹配搜索关键词
- 支持自定义填充字段名
- 支持user或address两种匹配列表布局样式
sgAutocomplete_v2
<template>
<div :class="$options.name" @mouseover="inside = true" @mouseout="inside = false">
<div class="search-layer">
<el-input
class="search-input"
v-model.trim="inputSearchValue"
maxlength="50"
:show-word-limit="false"
:placeholder="`你在找什么?快告诉我...`"
clearable
@keyup.native.enter="search"
@focus="focusSearchInput"
@clear="clearSearchInput"
@input="inputingSearchInput"
>
<el-button slot="append" icon="el-icon-search" @click="search"></el-button>
</el-input>
</div>
<div class="absolute-dropdownlist-layer" v-if="showAbsoluteDropdownlistLayer">
<!-- 搜索建议 -->
<div
class="search-suggestion"
v-if="historyKeywords.length > 0 && visible_search_suggestion"
>
<!-- 搜索历史 -->
<div class="search-suggestion-item history">
<div class="search-suggestion-item-head">
<h1>最近搜索</h1>
<el-link
icon="el-icon-delete"
type="danger"
:underline="false"
@click.stop="clearAll"
>清除
</el-link>
</div>
<ul class="historyKeywords">
<li v-for="(a, i) in historyKeywords" :key="i">
<el-tag
:title="a"
type="info"
closable
@click.stop="clickKeyword(a)"
@close="closeTag(a)"
>
<span class="text">{{ a }}</span></el-tag
>
</li>
</ul>
</div>
</div>
<!-- 后台查询推荐匹配项 -->
<div class="matchList" v-if="matchList.length > 0">
<ul>
<li
v-for="(a, i) in matchList"
:key="i"
@click.stop="clickMatchItem(a)"
:type="matchType"
>
<template v-if="matchType == `user`">
<div class="face">
<img
:src="a.src"
v-if="a.src && !a.hideImg"
@error="error($event, a, i)"
/>
<el-avatar
v-else
:style="
$g.convertData({ obj: `ROLE.PROFESSIONAL`, value: a.type }).style
"
>
{{
a.username
? a.username.slice(-1)
: a[matchKeyID] === "master"
? "超"
: "未"
}}
</el-avatar>
</div>
<b class="username" v-html="a.username_HTML"> </b>
<span class="userid" v-html="a.userid_HTML"></span>
<span
class="unit"
v-html="`${a.unit_HTML ? `(${a.unit_HTML})` : ``}`"
></span>
</template>
<template v-if="matchType == `address`">
<b class="username" v-html="a.username_HTML"> </b>
<span
class="address"
v-html="`${a.address_HTML ? `[${a.address_HTML}]` : ``}`"
></span>
</template>
</li>
</ul>
<div class="foot" v-if="showMoreBtn">
<div class="more-btn" @click.stop="readmoreMatch" :loading="moreBtnLoading">
{{ moreBtnLoading ? `加载中…` : `查看更多` }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "sgAutocomplete_v2",
components: {},
data() {
return {
form: {},
// ----------------------------------------
inside: false, //鼠标是否在组件内
showAbsoluteDropdownlistLayer: false, //显示absolute-dropdownlist-layer
inputSearchValue: this.$route.query.keyword || "", //搜索关键词
// 搜索建议----------------------------------------
visible_search_suggestion: false, //显示搜索建议
historyKeywords: [],
// 搜索匹配----------------------------------------
visible_search_match: false, //显示搜索匹配
mainID: `ID`, //主键
matchKeyID: `NAME`, //当点击匹配项,自动填充输入框的字段
matchType: `user`, //默认匹配类型是user,枚举值:user、address
matchList: [],
keywordClassName: "match-autocomplete-keyword", //匹配高亮关键词样式
showMoreBtn: false,
moreBtnLoading: false,
// ----------------------------------------
};
},
props: ["value", "data"],
computed: {},
watch: {
value: {
handler(d) {
this.inputSearchValue = d;
},
deep: true,
immediate: true,
},
inputSearchValue(d) {
this.$emit("input", d);
},
data: {
handler(newValue, oldValue) {
//console.log(`深度监听${this.$options.name}:`, newValue, oldValue);
if (Object.keys(newValue || {}).length) {
this.form = JSON.parse(JSON.stringify(newValue));
this.form.matchType && (this.matchType = this.form.matchType);
this.form.matchKeyID && (this.matchKeyID = this.form.matchKeyID);
this.showMoreBtn = this.form.showMoreBtn;
this.moreBtnLoading = this.form.moreBtnLoading;
this.matchList = this.form.matchList || [];
this.matchList = [
...new Set(this.matchList.map((v) => v[this.matchKeyID])),
].map((ID) => this.matchList.find((v) => v[this.matchKeyID] === ID)); //去重同样的ID数据
this.setHighlightMatchList();
this.$nextTick(() => {
let matchListScrollDOM = this.$el.querySelector(`.matchList>ul`);
matchListScrollDOM &&
matchListScrollDOM.scrollTo({
top: matchListScrollDOM.scrollHeight,
behavior: "smooth",
});
});
}
},
deep: true, //深度监听
immediate: true, //立即执行
},
matchList: {
handler(newValue, oldValue) {
//console.log(`深度监听${this.$options.name}:`, newValue, oldValue);
if (Object.keys(newValue || {}).length) {
newValue.length > 0 && (this.visible_search_suggestion = false);
}
},
deep: true, //深度监听
immediate: true, //立即执行
},
},
created() {
this.initHistoryKeywords();
},
mounted() {
this.__add();
},
destroyed() {
this.__remove();
},
methods: {
// 搜索输入框----------------------------------------
search({ keyword = this.inputSearchValue } = {}) {
this.visible_search_suggestion = false;
this.saveHistoryKeywords();
this.$emit(`search`, { keyword });
},
inputingSearchInput(keyword) {
if (keyword === "") {
this.visible_search_suggestion = true;
}
this.$emit(`change`, { keyword, isReset: true });
},
clearSearchInput(d) {
this.visible_search_suggestion = true;
},
getmatchList({ matchList } = {}) {
if (matchList.length > 0) {
this.visible_search_suggestion = false;
}
},
focusSearchInput(d) {
this.showAbsoluteDropdownlistLayer = true;
if (this.inputSearchValue.length === 0) {
this.visible_search_suggestion = this.matchList.length === 0;
} else {
this.visible_search_suggestion = false;
this.$emit(`change`, { keyword: this.inputSearchValue, isReset: true });
}
},
saveHistoryKeywords(d) {
let historyKeywords = (localStorage.historyKeywords || "").split("|");
historyKeywords.unshift(this.inputSearchValue);
historyKeywords = historyKeywords.filter(Boolean); //清空数组中的empty、undefined、null
historyKeywords = [...new Set(historyKeywords)];
this.historyKeywords = historyKeywords;
localStorage.historyKeywords = historyKeywords.join("|");
},
// 搜索建议----------------------------------------
//初始化历史搜索记录
initHistoryKeywords({ d } = {}) {
this.historyKeywords = [
...new Set(
(localStorage.historyKeywords || "").split("|").filter((v, i, ar) => v !== ``)
),
];
},
clickKeyword(d) {
this.inputSearchValue = d;
this.visible_search_suggestion = false;
this.search();
this.$nextTick(() => {
this.focusSearchInput();
});
},
closeTag(d) {
this.historyKeywords = this.historyKeywords.filter((v, i, ar) => v !== d);
localStorage.historyKeywords = this.historyKeywords.join("|");
},
clearAll(d) {
this.visible_search_suggestion = false;
this.historyKeywords = [];
delete localStorage.historyKeywords;
},
// 搜索匹配----------------------------------------
error($event, a, i) {
this.$set(this.matchList[i], "hideImg", true);
},
clickMatchItem(a) {
this.matchList = [];
this.inputSearchValue = a[this.matchKeyID];
this.search();
},
// 高亮匹配内容里面的关键词
setHighlightMatchList() {
let cls = this.keywordClassName;
let keyword = this.inputSearchValue;
this.matchList.forEach((d) => {
Object.keys(d || {}).forEach((k) => {
let originContent = this.$g.stripHTML(d[k]);
d[`${k}_HTML`] = this.$g.highLightMatchString(originContent, keyword, cls);
});
});
},
readmoreMatch(d) {
this.$emit(`readmoreMatch`, d);
},
// 全局监听----------------------------------------
__add() {
this.__remove();
addEventListener("mousedown", this.__mousedown);
},
__remove() {
removeEventListener("mousedown", this.__mousedown);
},
__mousedown(e) {
this.inside || this.__hide(e); // 点击其他区域隐藏
},
__hide(e) {
this.$emit("hide", e);
this.showAbsoluteDropdownlistLayer = false;
},
__show(e) {
this.$emit("show", e);
this.showAbsoluteDropdownlistLayer = true;
},
},
};
</script>
<style lang="scss" scoped>
.sgAutocomplete_v2 {
// 输入框搜索图层
.search-layer {
position: relative;
z-index: 1;
margin-bottom: 5px;
.search-input {
}
}
// 建议区域图层
.absolute-dropdownlist-layer {
z-index: 1;
position: relative;
width: 100%;
$maxHeight: 400px; //下拉框最大高度
// 搜索建议
.search-suggestion {
max-height: $maxHeight;
position: absolute;
top: 0;
left: 0;
box-shadow: 0 5px 20px 0 #00000022;
background-color: white;
border-radius: 4px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
padding: 20px;
.search-suggestion-item {
margin-bottom: 20px;
&:last-of-type {
margin-bottom: 0;
}
.search-suggestion-item-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h1 {
font-size: 14px;
color: #2c343e;
}
}
// 搜索历史
.historyKeywords {
display: flex;
flex-wrap: wrap;
margin-right: -10px;
margin-bottom: -10px;
max-height: calc(#{$maxHeight} - 80px);
overflow-y: auto;
li {
margin-right: 10px;
margin-bottom: 10px;
>>> .el-tag {
cursor: pointer;
$tagMaxWidth: 200px;
max-width: $tagMaxWidth;
display: flex;
align-items: center;
&:hover,
&:focus,
&:active {
border-color: #999;
background-color: #00000009;
span {
color: black;
}
}
.text {
max-width: calc(#{$tagMaxWidth} - 20px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
.el-tag__close {
flex-shrink: 0;
}
}
}
}
}
}
// 搜索匹配
.matchList {
max-height: $maxHeight;
position: absolute;
top: 0;
left: 0;
box-shadow: 0 5px 20px 0 #00000022;
background-color: white;
border-radius: 8px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
padding: 10px;
ul {
width: 100%;
max-height: calc(#{$maxHeight} - 80px);
overflow-y: auto;
li {
width: 100%;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 10px;
cursor: pointer;
transition: 0.2s;
border-radius: 8px;
&[type="user"] {
}
&[type="address"] {
}
&:hover {
filter: brightness(1.1);
background-color: #00000009;
}
.face {
margin-right: 10px;
img,
.el-avatar {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
object-position: center;
object-fit: cover;
border-radius: 88px;
overflow: hidden;
background-color: #00000009;
}
}
.username {
margin-right: 5px;
color: black;
}
.userid {
color: #999;
margin-right: 10px;
}
.unit {
color: #999;
}
.address {
color: #999;
}
// 匹配高亮关键词样式
>>> .match-autocomplete-keyword {
color: #409eff;
}
}
}
.foot {
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
.more-btn {
padding: 10px;
width: 100%;
height: 100%;
text-align: center;
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
color: #409eff;
&[loading] {
pointer-events: none;
}
&:hover {
background-color: #00000009;
}
}
}
}
}
}
</style>
demo
<template>
<div :class="$options.name" style="display: flex; flex-wrap: nowrap">
<sgAutocomplete_v2
style="width: 500px"
:data="data_sgAutocomplete_v2"
v-model="inputValue"
@change="changeMatch"
@readmoreMatch="readmoreMatch"
@search="search"
/>
<el-radio-group
v-model="data_sgAutocomplete_v2.matchType"
size="small"
style="margin-left: 10px; margin-top: 10px"
>
<el-radio
v-for="(radio, index) in radios"
:key="index"
:label="radio.value"
:disabled="radio.disabled"
>{{ radio.label }}</el-radio
>
</el-radio-group>
</div>
</template>
<script>
import sgAutocomplete_v2 from "@/vue/components/admin/sgAutocomplete_v2";
export default {
name: "demoSgAutocomplete_v2",
components: { sgAutocomplete_v2 },
data() {
return {
radios: [
{ value: `user`, label: "用户" },
{ value: `address`, label: "地址" },
],
// ----------------------------------------
inputValue: ``,
data_sgAutocomplete_v2: {
matchType: `user`, //默认匹配类型是user,枚举值:user、address
matchKeyID: `NAME`, //当点击匹配项,自动填充输入框的字段
matchList: [], //匹配项列表
},
// ----------------------------------------
currentPage: 1,
pageSize: 6,
};
},
methods: {
// 动态获取匹配项
changeMatch({ keyword = this.inputValue, isReset } = {}) {
if (!keyword) return (this.data_sgAutocomplete_v2.matchList = []);
if (isReset) {
this.data_sgAutocomplete_v2.matchList = [];
this.currentPage = 1;
}
let data = {
start: this.currentPage - 1, //当前页数(从0开始)
limit: this.pageSize, //每页显示条目个数
KEY: keyword, //匹配关键词
};
this.$f.biz_person_query(
{
...data,
l: {
show: () => this.$set(this.data_sgAutocomplete_v2, "moreBtnLoading", true),
close: () => this.$set(this.data_sgAutocomplete_v2, "moreBtnLoading", false),
},
cb: (d) => {
//回调函数
if (isReset) {
this.data_sgAutocomplete_v2.matchList = []; //避免重复追加
}
let old_matchList = this.data_sgAutocomplete_v2.matchList;
let matchList = old_matchList.concat(d.data || []);
//转义适配字段
matchList.forEach((v) => {
v.type = v.TYPE;
v.src = v.PHOTO_T ? this.$d.responseFile(v.PHOTO_T) : null; //头像路径
v.username = v.NAME;
v.userid = v.ACCOUNT;
v.unit = v.UNIT_ID_TEXT;
v.address = v.ADDRESS;
});
this.data_sgAutocomplete_v2.matchList = matchList;
this.data_sgAutocomplete_v2.showMoreBtn = matchList.length < d.totalCount;
},
},
this
);
},
readmoreMatch(d) {
this.currentPage++;
this.changeMatch();
},
search(d) {
// console.log(`搜索内容`, d, this.inputValue);
},
},
};
</script>