目录
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 | string | default | 'large' | 'default' | 'small' |
labelBg | string | #f0f0f0 | '' |
labelColor | string | #000 | '' |
multiline | boolean | false | true | false |
placeholder | string | Please select | '' |
7.2 select-option
value | string | '' | '' |
label | string | '' | '' |
disabled | boolean | false | true | false |
持续更新中