因为element-ui给的下拉图标不合适,所以尝试封装了一个下拉组件,做个记录
主要思路
- input为只读,获取焦点展开下拉列表
- 鼠标划出下拉框收缩列表
- 数据如果为后台拉取,需要监听下拉项,重新设置
- input在有值的情况下,如果设置了清除,可以清除
- 借助el-scroll-bar 实现下拉项超出滚动
- 为了遍历对象的数据,做了一个转换
效果:
禁用:
接下来就是枯燥乏味的代码:
selector.vue
html
<template>
<div class="selector" @mouseleave.stop="dropBoxBlur">
<div
class="input-wrapper"
:class="{
focus: optionsVisible,
'show-clear': currentOption !== '' && isClear,
'show-disable': disabled
}"
>
<input
v-model="currentOption"
:placeholder="placeHolder"
type="text"
@click="inputFocus"
readonly
/>
<div v-if="isClear && currentOption !== ''" class="clear" @click="clear">
X
</div>
<!-- 下拉列表面板 -->
<div v-show="optionsVisible" class="list-box">
<el-scrollbar wrap-class="scroll-viewPort-select">
<ul ref="list">
<li
v-for="(value, k) in options"
:key="k"
@click.stop="check(value)"
>
<span>{{ value[1] }}</span>
</li>
</ul>
</el-scrollbar>
</div>
</div>
</div>
</template>
JS
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
interface ObjectData {
[propName: string]: any
}
@Component
export default class Selector extends Vue {
// 下拉选项
@Prop({ required: true }) optionData!: Array<any>;
// 提示文字
@Prop({ default: "请选择" }) placeHolder!: string;
// 默认项
@Prop({ default: "" }) currentItem!: string | number;
// 是否可清除
@Prop({ default: "false", type: String || Boolean }) clearable!:
| string
| boolean;
@Prop({ default: false }) disable!: string | boolean;
private currentOption = this.currentItem || "";
private disabled = this.disable;
private optionsVisible = false;
private options: Array<any> = this.convert2Array(this.optionData);
private inputFocus(): void {
if (this.disabled) return;
this.optionsVisible = true;
}
private dropBoxBlur(): void {
if (this.optionsVisible) this.optionsVisible = !this.optionsVisible;
}
private clear() {
this.currentOption = "";
this.$emit("change", "");
}
private check(val: Array<any>): void {
this.currentOption = val[1];
this.$emit("change", Number(val[0]) || val[0]);
this.optionsVisible = false;
}
// 监听外部选项变化,
@Watch("currentItem")
currentChange(val: string) {
this.currentOption = val;
console.log("wa ", val);
this.setDefaultOption();
}
/*
监听接收对象或者数组
*/
@Watch("optionData", { deep: true })
optionDataChange(data: any) {
this.options = this.convert2Array(data);
this.setDefaultOption();
}
// 最后都转化为数组,如:
// [[key, value],[key, value],...]
private convert2Array(origin: Array<any> | ObjectData): Array<any> {
if (Object.prototype.toString.call(origin).slice(8, -1) === "Object") {
const arr: Array<any> = [];
for (const k in origin) {
arr.push([k, (origin as ObjectData)[k]]);
}
return [...arr];
} else {
return [...(origin as Array<any>)];
}
}
// 设置默认项
private setDefaultOption(): void {
console.log(this.options, this.currentOption);
this.options.forEach((item: Array<any>) => {
if (item[0] == this.currentOption) this.currentOption = item[1];
});
}
// 是否显示清除图标
get isClear(): boolean {
return !!this.clearable && this.clearable !== "false" ? true : false;
}
}
</script>
css
<style lang="less" scoped>
@selector_color: #767676;
@color_normal: #a99;
.selector {
display: inline-block;
vertical-align: middle;
width: 100%;
height: 0.3rem;
.input-wrapper {
position: relative;
width: 100%;
height: 100%;
line-height: 0.3rem;
input {
width: 100%;
height: 100%;
box-sizing: border-box;
outline: none;
text-indent: 0.05rem;
cursor: pointer;
border: 1px solid gray;
}
&:hover {
&.show-clear .clear {
display: inline-block;
}
&.show-clear::after {
display: none;
}
&.show-disable input {
cursor: not-allowed;
}
}
/* 三角 */
&::after {
content: "";
position: absolute;
right: 0.08rem;
top: 50%;
transform: translateY(-50%);
display: block;
border-top: 0.08rem solid @selector_color;
border-left: 0.06rem solid transparent;
border-right: 0.06rem solid transparent;
transition: all 0.5s;
}
&.focus {
.clear {
display: none;
}
&::after {
display: block;
transform: rotate(180deg);
transform-origin: 50% 25%;
transition: all 0.5s;
}
&:not(.is-empty)::after {
display: block;
}
}
/* 清除图标 */
.clear {
display: none;
position: absolute;
padding-top: 1px;
right: 5px;
top: 50%;
transform: translateY(-50%);
width: 0.15rem;
height: 0.15rem;
line-height: 0.15rem;
color: @color_normal;
border: 1px solid @color_normal;
border-radius: 50%;
font-size: 12px;
z-index: 2;
cursor: pointer;
box-sizing: border-box;
}
.list-box {
width: 100%;
width: 100%;
top: 100%;
position: absolute;
overflow: hidden;
z-index: 10;
box-sizing: border-box;
border: 1px solid #888;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background-color: #fff;
ul {
width: 100%;
list-style: none;
margin: 0;
padding: 0;
li {
text-align: left;
text-indent: 0.02rem;
letter-spacing: 1px;
width: 100%;
line-height: 0.28rem;
cursor: default;
&:hover {
background-color: #eef;
}
}
}
}
}
}
</style>
<style lang="less">
.scroll-viewPort-select {
max-height: 2rem;
}
</style>
父组件
<template>
<div class="views">
<div class="box">
<Selector :option-data="optionsData1" clearable="true" />
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import Selector from "../components/selector.vue";
@Component({
components: {
Selector
}
})
export default class Views extends Vue {
private optionsData1 = [
[1, "vue"],
[2, "react"],
[3, "angular"],
[1, "vue"],
[2, "react"],
[3, "angular"],
[1, "vue"],
[2, "react"],
[3, "angular"]
];
}
</script>
<style lang="less" scoped>
.box {
margin: 1rem auto;
width: 2rem;
}
</style>
需要注意
- selector父级盒子需要给宽度
- html设置了根字体 font-size: calc(100px + 1*(100vw - 1000px)/46);,不然样式会错乱
需要完善的地方:
- 多选,将选的值做个累加即可
- 分组,数据源加个类别在下拉列表处理一下样式就行
我还有个问题就是,怎么才能让下拉列表高度展开时有个过渡的效果,因为下拉项不是不确定吗,后面参考element select的实现把这个效果实现