基于Vue 自定义指令的多选菜单实现 v-item-select

1. 最近刚学了 vue 自定义指令,突发奇想做一个多选菜单(因为这样的需求其实蛮普遍的),代码高复用低耦合,花了一天时间完成,源码如下:

2. 举个栗子 src/views/DirectiveDemo.vue:

<template>
    <van-nav-bar title="自定义指令" left-arrow  @click-left="onClickLeft"/>
    <h1># 自定义指令</h1>
    <my-item-select
        :list="list"
        :initailIndex="initailIndex"
        >
    </my-item-select>
</template>


<script setup>
import { ref, defineProps, getCurrentInstance } from 'vue';
import MyTab from '@/components/MyTab.vue';
import MyTabSelect from '../components/MyTabSelect.vue';
import MyItemSelect from '../components/MyItemSelect.vue';


const onClickLeft = () => history.back();

// const instance = getCurrentInstance();
// console.log(instance.type.__file);

const props = defineProps({
    list: {
        type: Array,
        // eslint-disable-next-line vue/require-valid-default-prop
        default: [
            { id: 1, title: "选项1", content: "内容1", },
            { id: 2, title: "选项2", content: "内容2", },
            { id: 3, title: "选项3", content: "内容3", },
            { id: 4, title: "选项4", content: "内容4", },
            { id: 5, title: "选项5", content: "内容5", },
            { id: 6, title: "选项6", content: "内容6", },

        ],
    },
    initailIndex: {
        type: [Number, String],
        default: 1
    }
})

</script>

3. 菜单组件 src/components/MyItemSelect.vue:

在这里设置选中已经未选中样式:

<template>
    <h1>单选/多选 v-item-select</h1>
    <div>
        <div v-item-select="{
            itemClass: 'item',
            selectClass: 'item-select',
            currentIndex: currentIdx,
            isMultiple: true,
            min: 1,
            max: 5,
            list: list,
            minblock: minblock,
            maxblock: maxblock,
            block: block,
            }">
            <a
                class='item'
                v-for="(item, idx) in list" :key="idx"
                @click="changeItem(idx)"
                >
                {{item.title}}
            </a>
        </div>    
    </div>
</template>


<script setup>
import { Toast } from 'vant';
import { ref, reactive, getCurrentInstance, computed, defineProps } from 'vue';


const instance = getCurrentInstance();
console.log(instance.type.__file, instance);

const props = defineProps({
    list: {
        type: Array,
        // eslint-disable-next-line vue/require-valid-default-prop
        default () {
            return [];
        },
    },
    initailIndex: {
        type: [Number, String],
        default: 0
    }
})

const currentIndex = ref(props.initailIndex);

const currentContent = computed(() => {
   return props.list[currentIndex.value].content;
})


const changeIndex = (index) => {
    currentIndex.value = index;
}


const currentIdx = ref(props.initailIndex);

const selectItems = reactive([props.list[props.initailIndex]])

const changeItem = (idx) => {
    currentIdx.value = idx;
    // console.log(currentIdx.value, selectIndexs);
    console.log(instance.type.__file, currentIdx.value, selectItems);
}

const block = (indexs, items, idx) => {
    console.log("block", instance.type.__file, indexs, items.map((e) => {
        return e.title
    }));
}

const minblock = (val) => {
    Toast(`数量不能小于 ${val}`)
}

const maxblock = (val) => {
    Toast(`数量不能大于 ${val}`)
}

</script>


<style scoped lang="scss">
    
a{
    font-size: 1rem;
    margin: 8px;
    &.active{
        text-decoration: none;
        color: #000;
        // border: 1px solid #000;
        border-bottom: 1.5px solid #000;
    }
}

// .item{
//     &.item-select{
//         color: red;
//         // border-bottom: 1.5px solid #000;
//         text-decoration: underline;
//     }
// }

.item{
    margin: 8px;

    color: red;
}

.item-select{
    border: 1.5px solid #000;
}

</style>

4. 指令源码:

directives/itemSelect.js


export default { 
    mounted (el, bindings, vnode) {
        // console.log(el, bindings, vnode);
        const {itemClass, selectClass, currentIndex, isMultiple, min, max, list, block, minblock, maxblock} = bindings.value;
        
        el.itemClass = itemClass;
        el.selectClass = selectClass;
        el.items = el.getElementsByClassName(itemClass);
        el.items[currentIndex].className = `${itemClass} ${selectClass}`

        el.isMultiple = isMultiple;
        el.block = block;
        el.list = list;

        block([currentIndex], [list[currentIndex]], currentIndex)
        // console.log("el.items", typeof el.items, el.items);        
        if (!el.isMultiple) {
            return
        }

        var items = Array.from(el.items);

        // console.log("arr", typeof items)
        items.forEach((e, i) => {
            // console.log(e, i);
            e.addEventListener("click", (t) => {
                // console.log("addEventListener >>>", e.target.__vnode.key);
                const idx = t.target.__vnode.key;
                // console.log("addEventListener >>>", idx, el.items[idx].className);
                const isSelected = items[idx].className.endsWith(selectClass);

                let indexs = []
                items.forEach((e, j) => {
                    if (e.className.endsWith(selectClass)) {
                        indexs.push(j) 
                    }
                })
                
                if (min !== undefined && min > 0 
                    && max !== undefined && max > 0) {
                    if (isSelected) {
                        if (indexs.length > min) {
                            items[idx].className = `${itemClass}`
                            indexs.remove(idx)
                        } else {
                            minblock ? minblock(min) : alert(`数量不能小于 ${min}`);
                        }
                    } else {
                        if (indexs.length < max) {
                            items[idx].className = `${itemClass} ${selectClass}`
                            indexs.push(idx) 
                        } else {
                            maxblock ? maxblock(max) : alert(`数量不能大于 ${max}`);
                        }
                    } 
                } else {
                    if (isSelected) {
                        items[idx].className = `${itemClass}`
                        indexs.remove(idx)
                    } else {
                        items[idx].className = `${itemClass} ${selectClass}`
                        indexs.push(idx) 
                    }
                }

                let selectItems = indexs.sort().map((k) => list[k])
                block(indexs.sort(), selectItems, idx)
            });
        });
    },
    updated(el, bindings) {
        const { currentIndex } = bindings.value;
        const oldIndex = bindings.oldValue.currentIndex;
        const {itemClass, selectClass, items } = el;
        // console.log(currentIndex, oldIndex, itemClass,  selectClass, oitems);

        if (el.isMultiple) {
            return
        }
        items[oldIndex].className = itemClass;
        items[currentIndex].className = `${itemClass} ${selectClass}`
        el.block([currentIndex], [el.list[currentIndex]], currentIndex)
    },
}

// const itemSelect = (el, binding) => {
//     el.style.border = "1px solid blue";
// }
  
// export default itemSelect

Array.prototype.remove = function(val) {
    const index = this.indexOf(val);
    if (index >= 0) {
        this.splice(index, 1);
    }
    return this;
}

5. 备注

指令支持多选及单选, 组件参数如下:

itemClass, //子元素 class;
selectClass, //选中状态下子元素 class;
currentIndex, //当前选择索引;
isMultiple, //是否支持多选, 仅为真支持多选,不传或者假单选;
min, //最小选择个数(单选无效,可不传);
max, //最大选择个数(单选无效,可不传);
list, // item 集合;
block, //选择回调;
minblock, // 到达最小值时回调(单选无效,可不传);
maxblock // 到达最大值时回调(单选无效,可不传);

github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容