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 // 到达最大值时回调(单选无效,可不传);