阅读 1965

自己动手丰衣足食——自定义下拉框 vue 组件

在页面制作的过程中,经常需要使用到下拉框组件,有时候使用原生的 select 标签十分不便,因为存在 shadow root,shadow root 导致我们有时候修改样式时会非常麻烦

《ShadowRoot介绍》

使用 element UI 下拉框时,又遇到了选择的选项文字内容过长,不能够及时变省略号的现象,需要再次用鼠标点击后才能生效(果然 bug 依旧是程序员不变的主题)

在这里插入图片描述

虽然网上还有其他 UI 组件,但是因为当时我总体使用的是 element UI,不方便改用其他 UI 组件了。

没有条件那就只有自己创造,自己动手丰衣足食。

之前我也写过一篇下拉框组件的博客:自定义一个适应vue的下拉框组件

这一次稍做修改,现在的下拉框组件由两个组件构成

实现的效果: 在这里插入图片描述




正文开始

构思

下拉框组件准备分成两个模块:

  • 第一块是用户能直接看到的内容框,有一个三角形箭头

  • 第二块是用户点击内容框后触发的下拉列表框

在这里插入图片描述

外层

首先,定义一个外部的模块组件 wzc-select.vue

旁边的三角符号为 font-awesome 图形,font-awesome是一个免费的图标字体库

  • 使用npm install font-awesome下载

  • 如果使用 main.js 的话,添加 import 'font-awesome/css/font-awesome.min.css';

  • 使用 CDN 的话,用 link 引入:<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

  • 使用时直接在 class 中添加相应的图表名,如<i class="imgthree fa fa-caret-up" ></i>

外部组件调用时,可以对宽高还有 placeholder 进行设置

这里的数据传值由父组件传给子组件数值 props 来完成。

如果想要了解组件之间传值的方式,可以参考此文章:《Vue 组件通信的 8 种方式》

使用 props 接收 width、height、placeholder 三个值,可以使用 default 去设置默认值,如果外部没有传值,就会使用 default 中的数值

props: {
  placeholder: {
    type: String,
    default: '请选择'
  },
  width: {
    type: Number,
    default: 180
  },
    height: {
      type: Number,
        default: 40
    },
}
复制代码

部分属性使用:root 的方式添加,这里可以自己了解 《:root - CSS》

在 vue 中,可以使用 compute 去设置一个styleVar对象

computed: {
  styleVar() {
    return {
      '--select-height': this.height + 'px',
      '--select-width': this.width + 'px'
    }
  }
}
复制代码

在 div 中通过:style 绑定styleVar

<div class="wzc_select" :style="styleVar" >

这样就可以在下面的 style 中写 CSS 时直接使用这些宽高

.wzc_select {
  border: 1px solid #E6E6E6;
  border-radius: 5px;
  height: var(--select-height);
  width: var(--select-width);
  line-height: var(--select-height);
}
复制代码

外层的分为两部分,下面的 Selectlist 下拉框列表默认是隐藏的。

<template>
    <div class="wzc_select" :style="styleVar" >
        <!-- 选择框 -->
        <div class="divSelect" :class="{ 'drop_down': isListShow }" ref="divSelect" >
            <div class="divSelectinput" @click="dropDownSelect">
                <!-- 选中后的内容 -->
                <div class="selectinfos" :title="label" :class="{ 'no_select': label == '请选择' }"> {{ label }} </div>
                <!-- 三角形图标 isListShow判断三角形图标是否旋转 -->
                <i class="imgthree fa fa-caret-up" :class="{ 'is-reverse': isListShow }"></i>
            </div>
        </div>
        <!-- 下拉框列表 -->
        <transition name="drop-down" >
          	<!-- 下拉框列表isListShow来决定是否收起 -->
            <div class="Selectlist" v-show="isListShow" ref="dropDown">
                <div class="select_triangle"></div>
                <ul class="wzc_option_list">
                    <slot name="wzc_option"></slot>
                </ul>
            </div>
        </transition>
    </div>
</template>
复制代码

如果需要在点击弹出下拉框时增加一些动作效果的话,可以使用<transition>框住下拉框。

在 CSS 中写入 transition 动画效果

.drop-down-enter {
  opacity: 0;
  transform:translate(0px, -80px) scaleY(0.2);
}
.drop-down-leave-to {
  opacity: 0;
  transform:translate(0px, -80px) scaleY(0.2);
}
.drop-down-enter-active {
	transition: all 0.5s ease-in;
}
.drop-down-leave-active {
	transition: all 0.5s ease;
}
复制代码

在点击弹出和收起下拉框时需要做一个 document 的点击判断,如果点击在页面其他部分,就将下拉框收起

document.addEventListener("click", function( e ){
  if(_this.$refs.divSelect) {
    if ( !!_this.$refs.divSelect.contains(e.target) || !!_this.$refs.dropDown.contains(e.target) ) 
      return;
    else
      _this.isListShow = false;
  }   
})
复制代码

这样经过一些的考虑和操作之后,外层的部分就已经写完了

让我们使用importwzc-select.vue导入到页面中去吧,注意要在 components 中注册

  • 导入:import wzcSelect from './wzc-select'

  • 注册:components:{ wzcSelect }

  • 调用:<wzc-select class="wzcs" :width="240" :height="40"></wzc-select>

目前当前的效果已经有了,不过比较基础 在这里插入图片描述


内层

内层代码其实相对来说要简单的多,只需要展示外部传入的数据即可

<template>
  <li class="wzc_option" :style="styleVar" @click="currentSelect">
    <div class="wzc_option_dropdown_item">{{ label }}</div>
  </li>
</template>
复制代码

在 props 当中接收 CSS 的宽高属性,和 label 内容与 optionid 属性

props: {
  // 宽
  width: {
    type: Number,
    default: -1,
  },
  // 高
  height: {
    type: Number,
    default: 34,
  },
  // 内容
  label: {
    type: String,
  },
  // id
  optionid: {
    type: String,
   },
},
复制代码

在点击选择时,使用 $parent 去传递数据给外层 wzc-select.vue 组件

currentSelect() {
      this.$parent.label = this.label;
      this.$parent.optionid = this.optionid;
      this.$parent.isListShow = !this.$parent.isListShow;
}
复制代码

当然别忘记导入内层组件 import wzcOption from './wzc-option'


外层和内层结合

内层主要是一个<li></li>对象,在外层使用时,可以使用 slot 去让内层存放在相应显示的位置

有关于 slot 插槽的介绍可以参考 ( 当然具体的学习得自己慢慢过一遍 ):

在父组件中调用时就可以添加完全了

<wzc-select class="wzcs" :width="240" :height="40">
  <template v-slot:wzc_option>
    <wzc_option
      v-for="item in showlist"
      :key="item.item_id"
      :label="item.item_name"
      :optionid="item.item_id"
    ></wzc_option>
  </template>
</wzc-select>
复制代码

使用 showlist 为测试数据

showlist: [
  {
    item_name: "选项00000000000000000000000000000",
    item_id: "0",
  },
  {
    item_name: "选项11111111111111111111111111111",
    item_id: "1",
  },
  {
    item_name: "选项222222222222222222222222222222",
    item_id: "2",
  },
  {
    item_name: "选项33333333333333333333333333333333",
    item_id: "3",
  },
],
复制代码

好了,现在下拉框的实现效果就达到了需要的样式了

在这里插入图片描述




外层 wzc-select.vue 完整代码

<template>
    <div class="wzc_select" :style="styleVar" >
        <div class="divSelect" :class="{ 'drop_down': isListShow }" ref="divSelect" >
            <div class="divSelectinput" @click="dropDownSelect">
                <!-- 选中后的内容 -->
                <div class="selectinfos" :title="label" :class="{ 'no_select': label == '请选择' }"> {{ label }} </div>
                <!-- 三角形图标 -->
                <i class="imgthree fa fa-caret-up" :class="{ 'is-reverse': isListShow }"></i>
            </div>
        </div>
        <!-- 下拉框列表 -->
        <transition name="drop-down" >
            <div class="Selectlist" v-show="isListShow" ref="dropDown">
                <div class="select_triangle"></div>
                <ul class="wzc_option_list">
                    <slot name="wzc_option"></slot>
                </ul>
            </div>
        </transition>
    </div>
</template>

<script>
export default {
    name:'wzc_select',
    components: {},
    props: {
        placeholder: {
            type: String,
            default: '请选择'
        },
        width: {
            type: Number,
            default: 180
        },
        height: {
            type: Number,
            default: 40
        },
    },
    data() {
        return {
            label: '',
            isListShow: false,
            optionid: ''
        };
    },
    created() {
        this.label = this.placeholder;
    },
    mounted() {
        let _this = this;
        document.addEventListener("click", function( e ){
            if(_this.$refs.divSelect) {
                if ( !!_this.$refs.divSelect.contains(e.target) || !!_this.$refs.dropDown.contains(e.target) ) 
                    return;
                else
                    _this.isListShow = false;
            }   
        })
    },
    computed: {
        styleVar() {
            return {
                '--select-height': this.height + 'px',
                '--select-width': this.width + 'px'
            }
        }
    },
    methods: {
        dropDownSelect() {
            this.isListShow = !this.isListShow;
        },
    },
};
</script>
<style scoped>
    .wzc_select {
        border: 1px solid #E6E6E6;
        border-radius: 5px;
        height: var(--select-height);
        width: var(--select-width);
        line-height: var(--select-height);
    }
    .divSelect {
        width: 100%;
        height: 100%;
        border-radius: 5px;
    }
    .drop_down {
        box-shadow: 0px 0px 6px #709DF7;
    }
    .divSelectinput {
        width: calc(100% - 20px);
        height: 100%;
        margin: 0 5px 0 15px;
        display: flex;
    }
    .selectinfos {
        width: 87.5%;
        cursor: pointer;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .no_select {
        color: #D3DCE6;
    }
    .imgthree {
        width: 12.5%;
        line-height: var(--select-height);
        text-align: center;
        transform: rotate(180deg);
        transition: all 0.3s;
    }
    .imgthree:before {
        cursor: pointer;
        color: #D3DCE6;
    }
    .imgthree.is-reverse {
        transform: rotate(0deg);
    }

    .Selectlist {
        margin-top: 10px;
        z-index: 800;
        position: relative;
        background-color: #fff;
    }
    .wzc_option_list {
        border-radius:5px;
        border:1px solid #E4E7ED;
        width: 100%; 
        padding: 3px 0px;
        box-shadow: 0px 0px 6px #709DF7;
        background-color: #fff;
        margin: 0;
    }
    .select_triangle {
        width: 14px;
        height: 7px;
        position: relative;
        left: 15px;
    }
    .select_triangle::before {
        position: absolute;
        content: "";
        left: 0px;
        width: 0;
        height: 0;
        border-top: 0px solid transparent;
        border-left: 9px solid transparent;
        border-right: 9px solid transparent;
        border-bottom: 8px solid #EBEEF5;
    }
    .select_triangle::after {
        position: absolute;
        left: 2px;
        top: 2px;
        content: "";
        width: 0;
        height: 0;
        border-top: 0px solid transparent;
        border-left: 7px solid transparent;
        border-right: 7px solid transparent;
        border-bottom: 8px solid #fff;  
    }
    .drop-down-enter {
        opacity: 0;
        transform:translate(0px, -80px) scaleY(0.2);
    }
    .drop-down-leave-to {
        opacity: 0;
        transform:translate(0px, -80px) scaleY(0.2);
    }
    .drop-down-enter-active {
        transition: all 0.5s ease-in;
    }
    .drop-down-leave-active {
        transition: all 0.5s ease;
    }
</style>
复制代码



内层 wzc-option.vue 完整代码

<template>
  <li class="wzc_option" :style="styleVar" @click="currentSelect">
    <div class="wzc_option_dropdown_item">{{ label }}</div>
  </li>
</template>

<script>
export default {
  name: "wzc_select",
  components: {},
  props: {
    width: {
      type: Number,
      default: -1,
    },
    height: {
      type: Number,
      default: 34,
    },
    label: {
      type: String,
    },
    optionid: {
      type: String,
    },
  },
  data() {
    return {};
  },
  created() {},
  mounted() {},
  watch: {},
  computed: {
    styleVar() {
      return {
        "--option-height": this.height + "px",
        "--option-width": this.width == -1? "100%" : this.width + "px",
      };
    },
  },
  methods: {
    currentSelect() {
      this.$parent.label = this.label;
      this.$parent.optionid = this.optionid;
      this.$parent.isListShow = !this.$parent.isListShow;
      // this.$emit('slot-content', {label: this.label, optionid: this.optionid} );
    }
  },
};
</script>
<style scoped>
.wzc_option {
  list-style: none;
  height: var(--option-height);
  width: var(--option-width);
  
}
.wzc_option:hover {
  color: #409eff;
  font-weight: 700;
  background-color: #f5f7fa;
}
.wzc_option_dropdown_item {
  height: 100%;
  width: calc(100% - 30px);
  line-height: var(--option-height);
  cursor: pointer;
  margin: 0 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

</style>
复制代码
文章分类
前端
文章标签