Bootstrap

vue用复选框实现组件,支持单选和多选操作

✍️ 作者简介: 一个每天中午去抽风的前端开发。

🐈‍⬛ 两只猫🐱和一只狗的铲屎官🐶

🧣 微博: 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)
          }
        })
      }
    })
  })
}

总结

好像没有什么需要解释的了。
以上就是今天要讲的内容。

;