vue3 select 组件

目录

1. 新建select目录

2. 编写select.vue

 3. 编写select-option.vue

4. 指定组件接收的参数

5. 编写select.css

6.效果展示

6.1 多选

6.2 单选

7. props

7.1 select

7.2 select-option


1. 新建select目录

这是我的select目录结构

2. 编写select.vue

这是我使用addEventListener给dom添加监听事件,你也可以使用vue提供的方法代码会简洁许多,好看。

<script lang="ts">
import {defineComponent,ref,onMounted,watch,computed,onUpdated} from "vue";
import {SelectProps} from "./attribute";
export default defineComponent({
  name:"CptSelect",
  props:SelectProps,
  emits:['update:modelValue','change'],
  setup(props,{emit:e}){
    const multiple = ref();
    const iptD = ref();
    const text:any = ref({
      value:props.modelValue,
    });
    const select = ref();
    const multilineArr:any = ref([]);
    const optionShow = ref();
    
    const inputFocus = () => {
      if(!optionShow.value){
        iptD.value.focus();
        // 防止移动端点击调起键盘
        iptD.value.setAttribute('readonly','readonly');
        if(props.multiline){
          setTimeout(() => {
            iptD.value.removeAttribute('readonly')
          }, 200);
        }
      } else {
        iptD.value.blur();
      }
    }
    function optionClick(e){
      if(props.multiline){
        e.stopPropagation();
      }
    }
    function iptDBlurFn(){
      if(optionShow.value)
        multiple.value.classList.remove('multipleFocus');
        optionShow.value = false;
    }
    function iptFocusFn(){
      if(!optionShow.value)
        optionShow.value = true;
        multiple.value.classList.add('multipleFocus');
    }
    function preventDefaults(e: { preventDefault: () => void; }){
      e.preventDefault();
      return false;
    }
    function labelRemove(item){
      multilineArr.value.splice(multilineArr.value.indexOf(item),1);
      e('update:modelValue', multilineArr.value);
      if(multilineArr.value.length == 0)text.value = {};
    }
    // 按下回退键删除最后一位
    const popstate = (e: any) => {
      if (e.keyCode === 8) {
        multilineArr.value.splice(multilineArr.value.length - 1,1);
      }
    }
    onMounted(() => {
      select.value.addEventListener('mousedown',preventDefaults);
      iptD.value.addEventListener("blur", iptDBlurFn);
      iptD.value.addEventListener("focus", iptFocusFn);
      if(props.multiline){
        iptD.value.addEventListener('keydown',popstate)
      }
      if(props.size != 'defalut'){
        multiple.value.classList.add(props.size)
      }
    })
    const multilineValueArr = ref(computed(() =>{return multilineArr.value.map((item:any) => {return item.value})}))
    watch(() => text.value, (newVal) => {
      if(props.multiline && newVal.value){
        let index = -1;
        for (let i = 0; i < multilineArr.value.length; i++) {
          if (multilineArr.value[i].label === newVal.label) {
            index = i;
            break;
          }
        }
        if (index === -1) {
          multilineArr.value.push(JSON.parse(JSON.stringify(newVal)));
        } else {
          multilineArr.value.splice(index, 1);
        }
      }
      // modelvalue改变时传递change事件
      e('update:modelValue', props.multiline ? multilineValueArr.value : newVal.value);
      e('change',true)
    },{deep:true});
    return {
      inputFocus,
      multiple,
      optionClick,
      iptD,
      optionShow,
      text,
      select,
      multilineArr,
      labelRemove,
    }
  }
})
</script>
<template>
  <!-- DOM被点击时input获取焦点 -->
  <div class="cpt-select" @click="inputFocus" ref="select">
    <!-- 选择器 -->
    <div ref="multiple" class="multiple">
      <div class="input">
        <div class="box">
          <div v-if="multiline" class="label" :style="{'background':labelBg,'color':labelColor}" v-for="(item,index) in multilineArr" :key="index">
            {{ item.label }}
            <span @click.stop="labelRemove(item)" class="close">×</span>
          </div>
          <span class="label text" v-if="!multiline && text.value ">{{ text.label }}</span>
          <input  oninput="value=value.replace(/.*/g,'')" ref="iptD"/>
        </div>

        <span v-if="multiline && multilineArr.length == 0" id="placeholder">{{ placeholder }}</span>
        <span v-else-if="!multiline && !text.label" id="placeholder">{{ placeholder }}</span>
      </div>
    </div>
    <Transition name="slide-fade" v-show="optionShow">
        <div class="option">
          <ul @click="optionClick">
            <!-- 给选项预留插槽 -->
            <slot></slot>
          </ul>
        </div>
    </Transition>
  </div>
</template>
<style scoped>
/* index.css公共css */
@import url("../../index.css");
@import url('../css/style.css')
</style>

 3. 编写select-option.vue

这个组件用于select组件预留的slot位,你可以使用它像这样子

<script lang="ts">
import {defineComponent,onMounted,getCurrentInstance,ref,watch } from "vue";
import {SelectOptionProps} from "./attribute";
export default defineComponent({
  name:"CptOption",
  props:SelectOptionProps,
  setup(props) {
    const select = ref();
    const arr = ref<any>([]);
    const text:any = ref({});
    const multiline = ref(false);
    let once = false;

    const selectValue = () => {
      if(props.disabled)return;
      select.value.text.value = props.value;
      select.value.text.label = props.label;
    }
    onMounted(() => {
      if(select.value)return;
      // 获取父组件实例用于修改数据
      const {proxy}:any = getCurrentInstance();
      select.value = proxy.$parent.$parent;
      arr.value = select.value.multilineArr;
      multiline.value = select.value.multiline;
      text.value = select.value.text;
      init();
      watch(() => select.value.modelValue,() => {
        // 防止多次init
        if(once)return;
        init();
        once = true;
      })
    })
    const init = () => {
      if(!select.value.multiline && props.value == select.value.modelValue){
        text.value.label = props.label;
        text.value.value = props.label;
      }else if(select.value.modelValue && Array.isArray(select.value.modelValue) && select.value.modelValue.length > 0){
        select.value.modelValue.map((item:any,index:number) =>{
          if(item === props.value){
            let obj = {
              value:props.value,
              label:props.label,
              id:Date.now()
            }
            select.value.multilineArr[index] = obj ;
          }
        })
      }
    }
    const multilineFn = () => {
      for(let i = 0; i < arr.value.length; i++) {
        const item = arr.value[i];
        if(item.value === props.value) {
          return true;
        }
      }
      return false;
    }
    const textFn = () => {
      return text.value.label === props.label;
    }

    return {
      multiline,
      selectValue,
      multilineFn,
      textFn
    };
  },
})
</script>
<template>
  <li 
    ref="li" 
    :class="{
      'active':multiline ? multilineFn() : textFn(),
      'disabled':disabled
    }" 
    class="li" 
    @click="selectValue"
  >{{ label }}</li>
</template>
<style scoped>
.li{
  cursor:pointer;
  transition:background 0.2s;
  border-radius:5px;
  padding:5px 12px;
}
.li:hover{
  background:#f5f5f5;
}
.disabled{
  background:#FAFAFA !important;
  color:#929292;
  cursor:not-allowed;
}
.active{
  background:#e6f4ff !important;
  position: relative;
  font-weight:600;
}
</style>

4. 指定组件接收的参数

 /这里的modelValue没有规定类型,你可以在组件中判断值的类型

const SelectProps = {
  modelValue:{},                                      // v-model参数
  size: {type: String, default: 'default'},           // 组件大小
  labelBg: {type: String, default: '#f0f0f0'},        // 多选下label背景色
  labelColor: {type: String, default: '#000'},        // 多选下label字体色
  multiline: {type: Boolean, default: false},         // 多选
  placeholder:{type:String,default:'Please select'}
}
const SelectOptionProps = {
  value: {required: true},                            // 最终的值
  label: {required: true},                            // 展示的值
  disabled: {type: Boolean, default: false},          // 禁用
}
export { SelectProps,SelectOptionProps };

5. 编写select.css

--transition 是公共css里定义的

.cpt-select{
  --radius:8px;
  --padding:10px;
  box-sizing:border-box;
  transition:var(--transition);
  position:relative;
  border-radius:var(--radius);
  cursor:text;
  transition:var(--transition);
  font-size: 14px;
}

.cpt-select .multiple{
  border:1px solid var(--grey);
  border-radius:var(--radius);
  position:relative;
  transition:var(--transition);
  box-sizing: border-box;
  min-height: 32px;
}

.cpt-select .multipleFocus{
  border-color:var(--blue);
}
.cpt-select .multiple .input{
  box-sizing:border-box;
  min-height:32px;
  padding:3px var(--padding);
  padding-top: 0;
  display: flex;
  align-items: center;
}
.cpt-select .multiple .box{
  box-sizing: border-box;
  height:100%;display:flex;flex-wrap:wrap;align-items: center;
}
.cpt-select .multiple .input .label{
  background-color: #f0f0f0;
  user-select: none;
  font-size: 14px;
  margin-top: 3px;
  margin-right: 4px;
  padding:2px 4px;
  border-radius: 4px;
}
.cpt-select .multiple .input .label .close{
  cursor: pointer;
  user-select: none;
}
.cpt-select .multiple .input .text{
  background-color: rgba(0, 0, 0, 0);
}
.cpt-select .multiple #placeholder{
  position:absolute;
  top:50%;
  left: 15px;
  z-index:1;
  transform:translateY(calc(-50%));
  color: rgba(0, 0, 0, 0.25);
  font-size:15px;
}
.cpt-select .multiple input {
  box-sizing:border-box;
  user-select: none;
  outline:none;
  border:none;
  width:4px;
  height:100%;
  padding:0;
  border-radius:var(--radius);
  text-align:right;
  transform: translateX(-4px) translateY(2px);
}
.cpt-select .option{
  position:absolute;
  z-index:1100;
  background:var(--white);
  border-radius:var(--radius);
  border:1px solid var(--grey);
  box-shadow:0 2px 12px 0 rgba(0,0,0,0.1);
  width:100%;
  top:calc(100% + 10px);
  max-height:180px;
  overflow-y:auto;
  overflow-x:hidden;
  padding:5px;
  box-sizing:border-box;
  user-select: none;
}

.slide-fade-enter-active {
  transition: all 0.2s ease-out;
}
.slide-fade-leave-active {
  transition: all 0.2s;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
  transform-origin:top;
  transform: rotateX(90deg);
  opacity: 0;
}

.cpt-select .small{
  min-height: 22px;
}
.cpt-select .small .input{
  min-height: 22px;
}
.cpt-select .small .input .label{
  padding: 0;
  line-height: 14px;
}
.cpt-select .large{
  min-height: 40px;
}
.cpt-select .large .input{
  min-height: 40px;
}

6.效果展示

6.1 多选

6.2 单选

7. props

7.1 select

modelValue

string | array

size

stringdefault

 'large' | 'default' | 'small'

labelBg

string

#f0f0f0

''

labelColor

string

#000

''

multiline

booleanfalsetrue | false

placeholder

string

Please select

''

7.2 select-option

value

string''''

label

string''''

disabled

booleanfalsetrue | false

持续更新中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值