✍️ 作者简介: 一个每天中午去抽风的前端开发。
🐈⬛ 两只猫🐱和一只狗的铲屎官🐶
🧣 微博: GuoJ阝阝(fu)
前言
最近开发一个选择电器的功能,电器分很多大类,而每一类又区分单选和多选。我想只通过一个组件实现这个功能,于是使用了vant框架中的van-checkbox组件。
另外,每一种类的电器都支持可折叠,方便查看。当然其他框架的复选框组件实现也类似。
一、实现效果
二、实现步骤
注意:后台给我的数据是没有分类的,但是每一条数据都有type属性,前端根据这个参数判断类型。
1、代码实现
<template>
<van-collapse class="layout-collapse" v-model="activeNames">
<van-checkbox-group class="layout-checkbox-group" :max="singleCheck(item.value)" v-model="ElectricalChecked[item.value]" v-for="item in ElectricalRequireList" :key="item.value" @change="changeUserCheckedElectrical(ElectricalChecked)">
<van-collapse-item :title="singleCheckTitle(item)" :name="item.value">
<van-checkbox
shape="square"
v-for="subItem in item.children"
:key="subItem.value"
:name="subItem.id"
@click="changeSingleCheck(item, subItem)"
>
<template>
<van-image fit="cover" :src="subItem.url" />
<span class="name">{{ subItem.name }}</span>
</template>
</van-checkbox>
</van-collapse-item>
</van-checkbox-group>
</van-collapse>
<template>
data() {
return {
activeNames: [],
ElectricalRequireList: [],
}
},
computed: {
singleCheck: () => {
return (value) => ((value === 'shuicao' || value === 'cooking' || value === 'yanji') ? 1 : 0);
},
},
methods: {
getAllAppliances() {
request("getAllAppliances").then((res) => {
if (res && res.code === 0) {
// 获取电器数据列表
const allArr = res.data
// 预定义电器种类
let typeList = []
// 预定义最终数据格式
let resultArray = []
allArr.map(item => {
// 定义处理后的数据格式:电器类型的key/value,和该类数据集合
const typeObj = {
name: item.typeName,
value: item.typeCode,
children: [],
};
// 遍历数据,把所有电器类型筛选出来,对应类型的数据放进children
if (!typeList.includes(item.typeCode)) {
typeList.push(item.typeCode)
// 提前定义好v-modle中的数据类型,存放选中的电器集合
this.$set(this.ElectricalChecked, item.typeCode, [])
typeObj.children.push(item)
return resultArray.push(typeObj)
} else {
resultArray.forEach((subItem) => {
if (subItem.value === item.typeCode) {
subItem.children.push(item);
}
});
return;
}
});
// 定义初始展开的折叠区域,这里存入所有类型,默认全部展开
this.activeNames = this.activeNames.concat(typeList);
// 获得最终的数据,双向绑定到组件中
this.ElectricalRequireList = resultArray;
}
});
},
changeSingleCheck (item, subItem) {
// 判断是否是单选项
let singleFlag = 0
if (item.value === 'shuicao' || item.value === 'cooking' || item.value === 'yanji') {
singleFlag = 1
}
if (singleFlag === 1) {
// 单选项中如果有其他项,取消其他项,改为当前项
if (this.ElectricalChecked[item.value].length && !this.ElectricalChecked[item.value].includes(subItem.id)) {
this.ElectricalChecked[item.value] = [subItem.id]
}
}
}
}
2、代码解析
只能说这里没有一条代码是多余的,而且都是经过踩坑之后,解决了所有bug之后的。
(1)、<van-checkbox-group>
<van-checkbox-group class="layout-checkbox-group" :max="singleCheck(item.value)" v-model="ElectricalChecked[item.value]" v-for="item in ElectricalRequireList" :key="item.value">
- 这段代码是这个组件的核心,把复选框组作为循环;
- 每个复选框组到底是单选,还是多选,这是根据max属性来做判断。
- max使用计算属性来判断,这里需要给计算属性传参数,涉及到一个闭包的问题。
- v-model绑定的值是一个对象,对象包含多个属性,每个属性对应每一个复选框组的值。注意:复选框组的值是一个数组,所以v-model是一个包含多个数组的对象。
- ElectricalRequireList是所有数据的集合,是一个数组,每一项的数据都是
{name: '烟机', value: 'yanji', children: []}
。
(2)、<van-collapse-item>
这个没啥可说的,就是加个折叠的功能,像我这种展示图片的,高度会占用很大空间,有必要加个折叠。
(3)、<van-checkbox>
<van-checkbox shape="square"
v-for="subItem in item.children"
:key="subItem.value"
:name="subItem.id"
@click="changeSingleCheck(item, subItem)">
这地方说一下click事件的意义吧。
- 如果不加click事件,用复选框实现单选功能会有一个问题:只有取消上一次选中的才能再选。
- 这个函数不难理解:判断是否为单选的组,把选中的值改为最新值就可以了。
三、增加【更多】功能
客户增加需求,每个种类后,根据后台返回的数据,判断是否有更多的电器,如图:
1、代码实现
<van-collapse class="layout-collapse" v-model="activeNames">
<van-checkbox-group class="layout-checkbox-group" :max="singleCheck(item.value)" v-model="ElectricalChecked[item.value]" v-for="item in ElectricalRequireList" :key="item.value" @change="handleUserCheckedElectrical(ElectricalChecked)">
<van-collapse-item :title="singleCheckTitle(item)" :name="item.value">
<div class="van-collapse-item">
<div class="van-collapse-item__title">
{{singleCheckTitle(item)}}
<div class="layout-button-more" v-if="item.showMore">
<span class="layout-button-more-text" @click="handleMore(item)">更多 > </span>
</div>
</div>
<div class="van-collapse-item__wrapper">
<van-checkbox
v-for="subItem in item.children"
:key="subItem.id"
:name="subItem.id"
@click="changeSingleCheck(item, subItem, mutexValue)"
:disabled="mutexValue[item.value].includes(subItem.id)"
>
<template>
<van-image fit="cover" :src="subItem.url" />
<span class="name">{{ subItem.name }}</span>
</template>
</van-checkbox>
</div>
</div>
</van-collapse-item>
</van-checkbox-group>
</van-collapse>
<!-- 更多需求 -->
<van-popup v-model="moreRequirementShow" position="bottom" :lazy-render="false" round style="height: 80%">
<div class="more-require-wrapper">
<div class="more-require-title">
<div class="more-require-title-line"></div>
</div>
<div class="more-require-content">
<div class="more-require-panel">
<van-collapse class="layout-collapse" v-model="activeMoreNames">
<van-checkbox-group class="layout-checkbox-group" :max="moreSingleCheck(item.value)" v-model="moreElectricalChecked[item.value]" v-for="item in caseList" :key="item.value" @change="handleUserCheckedMore(moreElectricalChecked)">
<van-collapse-item :title="moreSingleCheckTitle(item)" :name="item.value">
<div class="van-collapse-item">
<div class="van-collapse-item__title">
{{singleCheckTitle(item)}}
</div>
<div class="van-collapse-item__wrapper">
<van-checkbox v-for="subItem in item.children" :key="subItem.id" :name="subItem.id"
:disabled="mutexValueMore[item.value].includes(subItem.id)">
<template>
<van-image fit="cover" :src="subItem.url"/>
<span class="name">{{subItem.name}}</span>
</template>
</van-checkbox>
</div>
</div>
</van-collapse-item>
</van-checkbox-group>
</van-collapse>
</div>
</div>
<div class="more-require-button">
<van-button round type="primary" @click="confirmMore"
>确定</van-button
>
</div>
</div>
</van-popup>
data() {
return {
activeNames: [],
ElectricalRequireList: [],
ElectricalChecked: {},
moreRequirementShow: false,
mutexValue: {},
}
},
computed: {
singleCheck: () => {
return (value) => ((value === 'shuicao' || value === 'cooking' || value === 'yanji') ? 1 : 0);
},
singleCheckTitle: () => {
return (item) => ((item.value === 'shuicao' || item.value === 'cooking' || item.value === 'yanji') ? item.name + "(单选)" : item.name);
},
},
watch: {
ElectricalChecked: {
handler (val) {
// 处理互斥操作
this.handleMutexValue()
},
deep: true
},
}
methods: {
getAllAppliances() {
request("getAllAppliances").then((res) => {
if (res && res.code === 0) {
const allArr = res.data.filter(
(col) => col.isMain === 0
);
this.ElectricalRequireBaseList = res.data
// 获取【更多】中的数据,用来判读是否显示每个类型下的【更多】按钮
let allMoreArr = res.data.filter(
(col) => col.isMain === 1
);
this.moreElectricalRequireBaseList = allMoreArr
let typeMoreList = allMoreArr.map(item => {
return item.typeCode
})
let typeList = []
let resultArray = []
allArr.map(item => {
let typeObj = {
name: item.typeName,
value: item.typeCode,
showMore: false,
children: [],
};
// 如果【更多】中有该类型的数据,则显示【更多】按钮
if(typeMoreList.includes(typeObj.value)) {
typeObj.showMore = true
}
if (!typeList.includes(item.typeCode)) {
typeList.push(item.typeCode)
this.$set(this.ElectricalChecked, item.typeCode, [])
this.$set(this.moreElectricalChecked, item.typeCode, [])
this.$set(this.mutexValue, item.typeCode, [])
this.$set(this.mutexValueMore, item.typeCode, [])
typeObj.children.push(item)
return resultArray.push(typeObj)
} else {
resultArray.forEach((subItem) => {
if (subItem.value === item.typeCode) {
subItem.children.push(item);
}
});
return;
}
});
this.activeNames = this.activeNames.concat(typeList);
this.ElectricalRequireList = resultArray;
}
});
},
changeSingleCheck (item, subItem, mutexValue) {
// 判断是否是单选项
let singleFlag = 0
if (item.value === 'shuicao' || item.value === 'cooking' || item.value === 'yanji') {
singleFlag = 1
}
if (singleFlag === 1 && !mutexValue[item.value].includes(subItem.id)) {
// 单选项中如果有其他项,取消其他项,改为当前项
if (this.ElectricalChecked[item.value].length && !this.ElectricalChecked[item.value].includes(subItem.id)) {
this.ElectricalChecked[item.value] = [subItem.id]
}
}
},
// 监听外部选中的物品,同步【更多】中的选中状态
handleUserCheckedElectrical(ElectricalChecked) {
this.changeUserCheckedElectrical(ElectricalChecked)
Object.keys(ElectricalChecked).forEach((key) => {
Object.keys(this.moreElectricalChecked).forEach((allKey) => {
if (key == allKey) {
// 如果里边存在,外部不存在,则删除内部的数据
this.moreElectricalChecked[allKey].forEach(newValItem => {
if(!ElectricalChecked[key].includes(newValItem)) {
let index = this.moreElectricalChecked[allKey].indexOf(newValItem)
this.moreElectricalChecked[allKey].splice(index, 1)
}
})
}
})
})
},
handleMore() {
this.moreRequirementShow = true;
},
// 处理互斥操作
handleMutexValue() {
// 处理选择电器时的互斥项
const allSelectId = []
Object.keys(this.mutexValue).forEach(key => {
this.mutexValue[key] = []
this.mutexValueMore[key] = []
})
Object.keys(this.ElectricalChecked).forEach(key => {
allSelectId.push(...this.ElectricalChecked[key])
})
Object.keys(this.moreElectricalChecked).forEach(key => {
allSelectId.push(...this.moreElectricalChecked[key])
})
// 根据所有选中的电器,获取互斥的所有电器
allSelectId.forEach(item => {
this.ElectricalRequireBaseList.forEach(subItem => {
if(item == subItem.id && subItem.mutualExclusion) {
const mutualExclusionList = subItem.mutualExclusion.split(",").map(item => { return Number(item)})
Object.keys(this.mutexValue).forEach(key => {
this.mutexValue[key].push(...mutualExclusionList)
this.mutexValueMore[key].push(...mutualExclusionList)
this.mutexValue[subItem.typeCode] = []
})
}
})
})
},
confirmMore() {
this.moreRequirementShow = false;
},
// 获取【更多】页面中显示的数据
getCaseList() {
request("getAllAppliances").then((res) => {
if (res && res.code === 0) {
let allMoreArr = res.data.filter((col) => col.isMain === 1);
this.moreElectricalRequireBaseList = allMoreArr
let typeMoreList = []
let resultMoreArray = []
allMoreArr.map(item => {
const typeObj = {
name: item.typeName,
value: item.typeCode,
children: []
}
if (!typeMoreList.includes(item.typeCode)) {
typeMoreList.push(item.typeCode)
typeObj.children.push(item)
return resultMoreArray.push(typeObj)
} else {
resultMoreArray.forEach(subItem => {
if (subItem.value === item.typeCode) {
subItem.children.push(item)
}
})
return
}
})
this.activeMoreNames = this.activeMoreNames.concat(typeMoreList)
this.caseList = resultMoreArray
// 判断模板案例中是否有【更多】中的电器
this.handleSetMoreCheck()
}
});
},
// 获取案例详细信息
getCaseById(caseId) {
request("getCaseById", { id: caseId }).then((res) => {
if (res && res.code === 0) {
this.caseLabelAppliances = res.data.label.appliances.data;
// 调用遍历数据的方法,传递三个参数:当前案例中的需求,全部需求,需要选中的需求
this.handleCheck(
this.caseLabelAppliances,
this.ElectricalRequireBaseList,
this.ElectricalChecked
);
// 从vuex判断有没有用户之前勾选的数据
Object.keys(this.userCheckedElectrical).forEach(key => {
this.ElectricalChecked[key] = this.userCheckedElectrical[key];
})
// 判断模板案例中是否有【更多】中的电器
this.handleSetMoreCheck()
}
});
},
handleCheck(part, all, checked) {
// 遍历电器列表
part.forEach((item) => {
all.forEach((allItem) => {
if (allItem.typeCode == item.code) {
// 具体类别下的已选电器
const caseElecs = item.data;
caseElecs.forEach((subItem) => {
if (subItem.id == allItem.id) {
checked[allItem.typeCode].push(subItem.id);
}
});
}
});
});
},
// 判断模板案例中是否有【更多】中的电器
handleSetMoreCheck() {
this.handleCheck(
this.caseLabelAppliances,
this.moreElectricalRequireBaseList,
this.moreElectricalChecked
);
// 从vuex判断有没有用户之前勾选的数据
Object.keys(this.userCheckedMore).forEach(key => {
this.moreElectricalChecked[key] = this.userCheckedMore[key];
})
// 初始状态判断【更多】中是否有选中的数据,有则展示到外部
this.setMoreSelectToAll()
},
// 初始状态判断【更多】中是否有选中的数据,有则展示到外部
setMoreSelectToAll() {
this.oldMoreElectricalChecked = JSON.parse(JSON.stringify(this.moreElectricalChecked))
Object.keys(this.moreElectricalChecked).forEach((key) => {
Object.keys(this.ElectricalChecked).forEach((allKey) => {
if (key == allKey && this.moreElectricalChecked[key].length) {
let selectKey = []
this.caseList.forEach(item => {
if (item.value == key) {
selectKey = item.children.filter(itemValue => this.moreElectricalChecked[key].includes(itemValue.id) )
}
})
this.ElectricalRequireList.forEach(item => {
if (item.value == key) {
item.children = item.children.concat(selectKey)
const map = new Map()
item.children = item.children.filter(itemKey => !map.has(itemKey.id) && map.set(itemKey.id, 1))
}
})
this.ElectricalChecked[allKey] = Object.assign(this.ElectricalChecked[allKey], this.moreElectricalChecked[key])
}
})
})
},
}
mounted() {
this.$nextTick(()=>{
this.getCaseById(curCaseId);
})
},
created() {
this.getAllAppliances();
this.getCaseList();
},
filters: {
ellipsis(value) {
if (!value) return "";
if (value.length > 8) {
return value.slice(0, 8) + "...";
}
return value;
},
},
2、代码解析
新增的代码中多了很多逻辑:
(1)、初始进入页面,会调用两个接口:一个是获取主页面的电器,另一个是获取【更多】中的电器。
(2)、进入页面后,会自动勾选一些项,这是根据接口返回的数据勾选的。
this.$nextTick(()=>{
this.getCaseById(curCaseId);
})
这里要在页面渲染完毕后,再勾选。
(3)、在【更多】里勾选的电器,要同步更新到主页面。这需要把【更多】里选中的电器的数据增加到主页面数据上,还要把勾选的值添加到主页面已选项中。
// 监听【更多】中选中的物品,同步到外部展示
handleUserCheckedMore (moreElectricalChecked) {
this.changeUserCheckedMore(moreElectricalChecked)
Object.keys(moreElectricalChecked).forEach((key) => {
Object.keys(this.oldMoreElectricalChecked).forEach((allKey) => {
if (key == allKey) {
// 如果newVal存在,oldVal不存在,则是新增的电器
moreElectricalChecked[key].forEach(newValItem => {
if(!this.oldMoreElectricalChecked[key].includes(newValItem)) {
let selectKey = []
this.caseList.forEach(item => {
if (item.value == key) {
selectKey = item.children.filter(itemValue => itemValue.id == newValItem )
}
})
this.ElectricalRequireList.forEach(item => {
if (item.value == key) {
// item.children = item.children ? item.children : []
item.children = item.children.concat(selectKey)
}
})
this.ElectricalChecked[allKey].push(newValItem)
}
})
// 如果newVal不存在,oldVal存在,则是减少的电器
this.oldMoreElectricalChecked[key].forEach(oldValItem => {
if(!moreElectricalChecked[key].includes(oldValItem)) {
this.ElectricalRequireList.forEach(item => {
if (item.value == key) {
item.children.forEach((ele, index) => {
if (ele.id == oldValItem) {
item.children.splice(index, 1)
}
})
}
})
}
})
}
})
})
this.oldMoreElectricalChecked = JSON.parse(JSON.stringify(this.moreElectricalChecked))
},
(4)、当取消选中的电器时,如果取消的是当时从【更多】选过来的电器,则把该电器从主页面删除,同时删除【更多】里的选中状态
// 监听外部选中的物品,同步【更多】中的选中状态
handleUserCheckedElectrical(ElectricalChecked) {
this.changeUserCheckedElectrical(ElectricalChecked)
Object.keys(ElectricalChecked).forEach((key) => {
Object.keys(this.moreElectricalChecked).forEach((allKey) => {
if (key == allKey) {
// 如果里边存在,外部不存在,则删除内部的数据
this.moreElectricalChecked[allKey].forEach(newValItem => {
if(!ElectricalChecked[key].includes(newValItem)) {
let index = this.moreElectricalChecked[allKey].indexOf(newValItem)
this.moreElectricalChecked[allKey].splice(index, 1)
}
})
}
})
})
}
总结
好像没有什么需要解释的了。
以上就是今天要讲的内容。