简述:如何在pc端实现像手机移动端才有的indexLIst索引列表呢,查阅很多组件库,都没能找到相应组件,那只能自己来手搓了
这里vue版本vue2.0,deepClone为深拷贝方法,网上自行百度找吧,组件库用的elementUI,字体图标取自elementUI,本组件可适应任意key、value、children的键值,具体使用请看下方props配置,数据格式
效果预览
视频预览
这里代码中都有注释,样式,js一并贴入,看不懂那我也没办法了,强调一点,数据一定要让后端返回此类格式(自己整理数据成这样也可)如图:
其中字母索引,和children是必须的!!!!
- 组件使用
<template>
<div>
<indexList placeholder="请选择保险公司" v-model="headId" style="width: 200px" :options="testList" :props="{ name: 'icName', value: 'id', letter: 'brandInitial', data: 'children', }" />
</div>
</template>
- 组件封装-输入框+下拉列表封装
<template>
<div :class="['index_list_content' + client,'bg_fff']">
<div class="relative">
<transition name="el-zoom-in-top">
<!-- 下拉列表区域 -->
<index-options
v-show="visible"
class="index_area absolute bor_rad4"
:style="{top:offsetTop + 'px'}"
:multiple="multiple"
:option="list"
:selectValue="selectValue"
:letterList="letterList"
@deleteItem="deleteItem"
@closeList="confirm"
/>
</transition>
<!-- 单选输入框区域 -->
<div
v-if="!multiple"
ref="itembox"
class="inlin_block"
>
<el-input
style="width: 100%;"
ref="input"
:clearable="false"
:selectValue="selectValue"
v-model="valueName"
:placeholder="innerPlaceholder"
@input="fetchSuggestions"
@focus="open"
@blur="close"
/>
<span
v-if="iconT !== 'el-icon-circle-close'"
class="input__suffix inline_block"
@mouseover="iconChage"
@mouseout="iconOut"
@click="clear"
>
<i :class="iconT"></i>
</span>
<span
v-else
class="input__suffix pointer inline_block"
@mouseover="iconChage"
@mouseout="iconOut"
@click="clear"
>
<i :class="iconT"></i>
</span>
</div>
<!-- 多选输入框区域 -->
<div
v-if="multiple"
ref="itembox"
class="inlin_block"
>
<div
v-show="valueNameArr.length > 0"
:class="['flex flex-align-center input_box bor_rad4 pr30',{'is_focus':isFocus}]"
>
<div
v-show="valueNameArr.length > 0"
class="flex flex-align-center c_gap4 r_gap4"
style="width: 100%;max-width: 178px;"
>
<span
class="f12 inlien_block bor_rad4 tag"
@click.stop="clickFocus"
>
{{valueNameArr[0]}}
<i
style="font-size: 12px;"
class="iconfont icon_close_bg"
@click.prevent.stop="deleteItem(valueNameArr[0])"
></i>
</span>
<span
v-show="valueNameArr.length-1 > 0"
@click.stop="clickFocus"
class="f12 inlien_block bor_rad4 tag"
> + {{ valueNameArr.length-1 }}
</span>
</div>
<el-input
style="width: 100% !important;"
class="ml8"
ref="input"
:selectValue="selectValue"
v-model="searchName"
@input="fetchSuggestions"
@focus="open"
@blur="close"
:clearable="false"
/>
<span
v-if="iconT !== 'el-icon-circle-close'"
class="input__suffix inline_block"
@mouseover="iconChage"
@mouseout="iconOut"
@click="clear"
>
<i :class="iconT"></i>
</span>
<span
v-else
class="input__suffix pointer inline_block"
@mouseover="iconChage"
@mouseout="iconOut"
@click="clear"
>
<i :class="iconT"></i>
</span>
</div>
<el-input
v-show="valueNameArr.length === 0"
:selectValue="selectValue"
:placeholder="innerPlaceholder"
v-model="searchName"
:suffix-icon="iconT"
@input="fetchSuggestions"
@focus="open"
@blur="close"
/>
</div>
</div>
</div>
</template>
<script>
import { deepClone } from '@/utils/util';
import indexOptions from './options';
/**
* @remarks
* value:v-model绑定
* options:array
* placeholder:输入框背景文字
* props:配置选项,具体:
* {
value: 'id',
name: 'name',
letter: 'letter',index索引
data: 'data',索引下的list
}
**/
export default {
components: {
indexOptions,
},
props: {
value: {
type: [String, Number, Array],
default: () => {
return null;
},
},
options: {
type: Array,
default: () => {
return [];
},
},
placeholder: {
type: String,
default: '请选择',
},
props: {
type: Object,
default: () => {
return {
value: 'id',
name: 'name',
letter: 'letter',
data: 'data',
};
},
},
multiple: {
type: Boolean,
default: false,
},
// 组件唯一class尾缀,防止两个组件类名重复,导致组件无法下拉(可不传,这里取默认)
client: {
type: String,
default: function name(params) {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '');
},
},
},
watch: {
options: {
deep: true,
immediate: true,
handler() {
this.setList();
},
},
selectValue(newVal) {
this.$emit('input', this.selectValue);
},
valueName(newVal) {
if (!newVal) {
this.restoreData()
}
},
value(newVal) {
if (
((typeof newVal === 'string' && !newVal.length) || typeof newVal === 'object') &&
!this.multiple
) {
this.valueName = '';
}
if (!newVal.length && this.multiple) {
this.valueNameArr = [];
}
this.selectValue = newVal;
},
//优化筛选为空时,再次下拉无数据情况
visible(val) {
if (!val && !this.list.length) {
this.restoreData();
}
if (this.list.length !== this.oldList.length) {
this.restoreData();
}
},
},
data() {
return {
visible: false, //是否显示下拉
searchName: '', //多选时用于搜索
valueName: '', //单选显示的数据name
selectValue: this.value, //选中数据
valueNameArr: [], //多选选中数据的name
iconT: 'el-icon-arrow-down',
list: [], //数据
letterList: [],
oldList: [],
oldLetterList: [],
oldPlaceholder: '',
innerPlaceholder: this.placeholder,
offsetTop: 0, //下拉距离顶部距离
isFocus: false, //记录是否获取焦点
};
},
mounted() {
// 创建点击监听
document.addEventListener('click', this.handleDocumentClick);
this.offsetTop = this.getBoundingClientRect();
},
destroyed() {
document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
// 初始化数据
setList() {
this.oldPlaceholder = deepClone(this.innerPlaceholder);
this.options.forEach((item, index) => {
let arr = item[this.props.data || 'letter'].map((item2) => {
return {
value: item2[this.props.value || 'value'],
name: item2[this.props.name || 'name'],
letter: item[this.props.letter || 'letter'],
};
});
this.list.push({ letter: item[this.props.letter || 'letter'], data: arr });
});
this.dataInit();
this.letterList = this.options.map((item) => {
return item[this.props.letter || 'letter'];
});
// this.letterList = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
this.oldList = deepClone(this.list);
this.oldLetterList = deepClone(this.letterList);
},
// 初始化数据回显name
dataInit() {
this.list.forEach((item, index) => {
item.data.forEach((item2) => {
if ((typeof this.selectValue === 'array' || typeof this.selectValue === 'string') && this.selectValue.indexOf(item2.value) >= 0) {
if (this.multiple) {
this.valueNameArr.push(item2.name);
} else {
this.valueName = item2.name;
}
}else if(typeof this.selectValue === 'number' && !this.multiple && this.selectValue === item2.value) {
this.valueName = item2.name;
}
});
});
},
restoreData() {
setTimeout(() => {
this.fetchSuggestions('');
}, 50);
},
// 处理筛选数据
fetchSuggestions: _.debounce(function (queryString) {
if (!queryString) {
this.list = deepClone(this.oldList);
this.letterList = deepClone(this.letterList);
} else {
var restaurants = deepClone(this.oldList);
const duplicates = [];
restaurants.forEach((item) => {
item.data.forEach((item2) => {
if (item2.name.indexOf(queryString) >= 0) {
duplicates.push(item2);
}
});
});
this.list = this.organizationData(duplicates);
this.letterList = this.list.map((item) => item.letter);
}
}, 100),
organizationData(arr) {
return Object.values(
arr.reduce((acc, cur) => {
// 如果累加器对象中没有当前字母的键,则初始化它
if (!acc[cur.letter]) {
acc[cur.letter] = { letter: cur.letter, data: [] };
}
// 将当前对象添加到对应字母的数据数组中
acc[cur.letter].data.push(cur);
return acc;
}, {})
);
},
iconOut() {
if (this.isFocus) {
this.iconT = 'el-icon-arrow-up';
} else {
this.iconT = 'el-icon-arrow-down';
}
},
// 图标hover效果
iconChage() {
this.iconT = 'el-icon-circle-close';
},
// 点击使输入框获取焦点
clickFocus() {
this.$refs.input.focus();
},
open(e) {
this.isFocus = true;
this.iconT = 'el-icon-arrow-up';
this.visible = true;
e.currentTarget.focus();
if (this.valueName) {
this.innerPlaceholder = deepClone(this.valueName);
this.valueName = '';
}
},
close() {
this.$refs.input.blur();
this.iconT = 'el-icon-arrow-down';
// 单选
if (!this.multiple) {
if (!this.selectValue && this.valueName) {
this.valueName = '';
this.innerPlaceholder = deepClone(this.oldPlaceholder)
this.$emit('input', this.selectValue);
return;
}
if (this.selectValue && this.oldPlaceholder !== this.innerPlaceholder) {
this.valueName = this.innerPlaceholder;
}
} else {
//多选
if (!this.selectValue) {
this.selectValue = this.multiple ? [] : '';
this.innerPlaceholder = this.placeholder;
}
this.$emit('input', this.selectValue);
}
},
// 清除
clear(e) {
this.selectValue = this.multiple ? [] : '';
if (this.multiple) {
this.valueNameArr = [];
this.iconT = 'el-icon-arrow-down';
} else {
this.valueName = '';
}
this.innerPlaceholder = this.placeholder;
this.isFocus = false;
this.visible = false;
},
// 下拉列表选择
confirm(e) {
if (this.multiple) {
this.valueNameArr.push(e.name);
this.selectValue.push(e.value);
if (this.searchName) {
this.searchName = '';
this.restoreData();
}
this.$refs.input.focus();
} else {
this.valueName = e.name;
this.selectValue = e.value;
this.visible = false;
}
this.offsetTop = this.getBoundingClientRect();
},
// 获取输入框高度
getBoundingClientRect() {
return this.$refs.itembox.getBoundingClientRect().height + 12;
},
// // 节流删除操作
deleteItem: _.throttle(function (item) {
let index = this.valueNameArr.indexOf(item);
if (index >= 0) {
this.valueNameArr.splice(index, 1);
this.selectValue.splice(index, 1);
this.$emit('input', this.selectValue);
}
if (this.searchName) {
this.searchName = '';
this.restoreData();
}
}, 200),
// 点击输入框外部元素隐藏下拉列表
handleDocumentClick(e) {
if (document.getElementsByClassName('index_list_content' + this.client)[0]) {
if (!document.getElementsByClassName('index_list_content' + this.client)[0].contains(e.target)) {
//这句话是说如果我们点击到了class为keywordContainer以外的区域
this.visible = false;
this.isFocus = false;
}
}
},
},
};
</script>
<style scoped lang="scss">
.bg_fff{background-color: #fff;}
.index_area {
width: 100%;
background-color: #fff;
z-index: 9;
padding: 12px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transform-origin: center top;
}
.input_box {
border: 1px solid #c0c4cc;
height: 36px;
.tag {
padding: 0px 8px;
background-color: #f4f4f5;
color: #909399;
height: 24px;
line-height: 24px;
margin-left: 6px;
white-space: nowrap;
}
}
.is_focus {
border: 1px solid #48c07f !important;
}
.input_box ::v-deep .el-input__inner:hover {
border: none !important;
}
.input_box ::v-deep {
.el-input__inner {
border: none !important;
width: 100% !important;
padding: 0 !important;
height: 24px !important;
line-height: 24px !important;
}
}
.input__suffix {
position: absolute;
width: 25px;
height: 100%;
right: 5px;
top: 0;
text-align: center;
color: #c0c4cc;
-webkit-transition: all 0.3s;
transition: all 0.3s;
}
.pr30 {
padding-right: 30px;
}
.inline_block {
display: inline-block;
}
.pointer {
cursor: pointer;
}
.ml8 {margin-left: 8px;}
.f12 {font-size: 12px;}
.flex{display: flex; flex-direction: row;flex-wrap: nowrap; justify-content: flex-start;}/***默认水平左对齐不换行**/
.flex-align-center{align-items: center;}/**垂直居中对齐**/
.c_gap4 {column-gap: 4px;}
.r_gap4 {row-gap: 4px;}
.relative {position: relative;}
.absolute {position: absolute;}
.bor_rad4{border-radius: 4px;}
</style>
- 下拉列表的封装
<template>
<div>
<div class="popper__arrow"></div>
<div
class="menu"
v-if="reFresh"
:class="'menu' + client"
>
<div
v-if="option.length > 0"
class="w100"
>
<div
class="city_menu_container"
:class="'city_menu_container' + client"
@scroll="onScroll"
>
<div
v-for="(item,index) in option"
:key="index"
class="area"
:class="'area' + client"
>
<div
:id="item.letter"
ref="item.letter"
class="item_tit"
:class="[{high_light: activeIndex === index },'item_tit' + client]"
>{{ item.letter }}</div>
<!-- 通过 typeof selectValue === 'number'区分value类型 -->
<template v-if="typeof selectValue === 'number'">
<div
v-for="(list,newIndex) in item.data"
:key="list.name"
class="item action pointer"
:class="{active_brand: selectValue === list.value}"
@click="chose(list.name,index,list.value,newIndex)"
>
{{ list.name }}
</div>
</template>
<template v-else>
<div
v-for="(list,newIndex) in item.data"
:key="list.name"
class="item action pointer"
:class="{active_brand: selectValue.indexOf(list.value) >= 0}"
@click="chose(list.name,index,list.value,newIndex)"
>
{{ list.name }}
</div>
</template>
</div>
</div>
<ul
class="city_list_container"
:class="'city_list_container' + client"
>
<li
v-for="(item,index) in letterList"
:key="index"
class="item_wort"
:class="{active: activeIndex === index }"
@click="jump(index,$event)"
>{{ item }}</li>
</ul>
</div>
<span
v-else
class="col_999 text-center w100"
>
无匹配数据
</span>
</div>
</div>
</template>
<script>
export default {
props: {
selectValue: {
type: [Array, String, Number],
default: () => {
return '';
},
},
letterList: {
type: Array,
default: () => {
return [];
},
},
option: {
type: Array,
default: () => {
return [];
},
},
multiple: {
type: Boolean,
},
client: {
type: String,
default: function name(params) {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '');
},
},
},
data() {
return {
activeStep: null,
activeIndex: 0,
reFresh: true,
};
},
watch: {
option() {
this.reFresh = false;
this.$nextTick(() => {
this.reFresh = true;
});
},
},
computed: {},
activated() {
// this.onScroll()
},
methods: {
chose(name, index, value, newIndex) {
// 单选操作
console.log(!this.multiple);
if (!this.multiple) {
this.$emit('closeList', { name, value });
} else {
//多选
if (this.selectValue.indexOf(value) < 0) {
//判断去重
this.$emit('closeList', { name, value });
} else {
//删除
this.$emit('deleteItem', name);
}
}
},
onScroll(e) {
const scrollItems = document.querySelectorAll('.item_tit' + this.client);
for (let i = scrollItems.length - 1; i >= 0; i--) {
// 判断滚动条滚动距离是否大于当前滚动项可滚动距离
const judge = e.target.scrollTop >= scrollItems[i].offsetTop - scrollItems[0].offsetTop;
if (judge) {
this.activeStep = i;
this.activeIndex = i;
break;
}
}
},
jump(index, event) {
this.activeIndex = index;
const target = document.querySelector('.city_menu_container' + this.client);
const scrollItems = document.querySelectorAll('.area' + this.client);
// 判断滚动条是否滚动到底部
if (target.scrollHeight <= target.scrollTop + target.clientHeight) {
this.activeStep = index;
}
// console.log(this.activeIndex, index)
const roll = scrollItems[index].offsetTop - scrollItems[0].offsetTop; // 锚点元素距离其滚动窗口顶部的距离(待滚动的距离)
let distance = document.querySelector('.city_menu_container' + this.client).scrollTop; // 滚动条距离滚动区域顶部的距离
// 滚动条距离滚动区域顶部的距离(滚动区域为窗口)
// 滚动动画实现, 使用setTimeout的递归实现平滑滚动,将距离细分为10小段,10ms滚动一次
// 计算每一小段的距离
let step = roll / 10;
if (roll > distance) {
rollDown(target);
} else {
const newTotal = distance - roll;
step = newTotal / 10;
rollUp(target);
}
// node为滚动区域节点
function rollDown(node) {
if (distance < roll) {
distance += step;
node.scrollTop = distance;
setTimeout(rollDown.bind(this, node), 10);
} else {
node.scrollTop = roll;
}
}
// node为滚动区域节点
function rollUp(node) {
if (distance > roll) {
distance -= step;
node.scrollTop = distance;
setTimeout(rollUp.bind(this, node), 10);
} else {
node.scrollTop = roll;
}
}
},
},
};
</script>
<style scoped>
.popper__arrow {
z-index: 1000;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
}
.popper__arrow,
.popper__arrow:after {
position: absolute;
display: block;
width: 0;
height: 0;
top: -6px;
left: 35px !important;
margin-right: 3px;
border-left: 0 solid transparent;
border-right: 0 solid transparent;
border-bottom: 0 solid #fff;
border-width: 6px;
}
.menu {
/* position: relative; */
display: flex;
justify-content: space-between;
}
.item_tit {
width: 100%;
height: 26px;
background: #f7f7f7;
line-height: 26px;
padding-left: 8px;
}
.item_wort {
cursor: pointer;
text-align: center;
height: 20px;
line-height: 20px;
/* padding: 4px 8px; */
}
.city_menu_container {
width: 90%;
max-height: 540px;
overflow: scroll;
position: relative;
}
.city_menu_container::-webkit-scrollbar {
width: 4px !important;
height: 1px !important;
}
.city_menu_container::-webkit-scrollbar-thumb:horizontal:hover {
background-color: #c5c5c5 !important;
transition: 0.5s all;
}
.city_menu_container::-webkit-scrollbar-thumb:vertical:hover {
background-color: #c5c5c5 !important;
transition: 0.5s all;
}
.city_menu_container::-webkit-scrollbar-thumb:vertical {
height: 1px;
background-color: #d9d9d9 !important;
-webkit-border-radius: 5px !important;
}
.city_menu_container::-webkit-scrollbar-thumb:horizontal {
width: 4px;
background-color: #d9d9d9 !important;
-webkit-border-radius: 0px !important;
}
.item {
width: 100%;
height: 30px;
line-height: 30px;
padding-left: 8px;
}
.item span {
cursor: pointer;
}
.item:hover {
background-color: #f5f7fa;
}
.city_list_container {
list-style-type: none;
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
top: 1.72rem;
right: 12px;
bottom: 0;
z-index: 999999;
max-height: 500px;
}
.active {
color: #48c07f;
}
.active_brand {
color: #48c07f;
}
.high_light {
background: linear-gradient(
90deg,
rgba(72, 192, 127, 0.15) 0%,
rgba(72, 192, 127, 0.03) 100%
) !important;
color: #48c07f;
}
</style>
到此基本就可以使用了,若有不足之处请点评,谢幕!!!!