小程序选择控制——模仿企业微信实现多级不规则嵌套
很多系统都有选择联系人的需求,市场上也没有很好的参考。产品经理看到企业微信的选择非常好用,就参照这个做一个吧。
算了,我们试着去做吧。企业微信的候选人做得不错,不得不佩服。
先来看看效果图。可以进行多级不规则嵌套。
一、设计解读
整个界面分为三个部分:
顶部按钮中间显示部门和人员列表,底部显示所选人员的页脚并进行操作。为什么要增加一个按钮回到上一级?
我也觉得很丑,但是小程序不能直接控制左上角的后退按钮(自定义Title好像还行,没试过)。单击左上角的后退箭头退出候选控件并转到上一页。
我们的需求是点击一个文件夹,通过刷新当前列表进入下一个目录。感觉我们进入了另一个页面,其实不是,只是列表的数据变了。从而实现对部门和人员的不确定层级和不规则嵌套的支持。
例如,首先点击第一个屏幕数据的第二项,其索引为1,然后在indexList中存储1;返回上一级时删除最后一个元素。
选中一个人或部门时,所有选中的人或部门的名称将显示在底部的框中。当文字超过屏幕宽度时,可以无限向右滑动,底部的页脚始终保持一行。
最终选定的人员将基于底部页脚中显示的人员。点击〖确定〗按钮,根据业务需要,将所选人员的数据发送至所需界面。
二、功能逻辑分析
先看数据格式。
{id: TEACHER_ID,name: '教师',家长id: ' ',checked : false,ISPEOPLE 3360 false,孩子:[{ ID 3360 TEACHER _ department _ ID,Name: '部门',家长ID : '教师',Checked : false,ISPEOPLE 3360 false,孩子: []},{ID 3360 teacher _ subject _ ID,姓名:
不要说身份证和名字。parentId表示它的父节点,children包含它所有的子节点,checked用来判断checked的状态,isPeople用来判断是部门还是个人,因为他们的图标不一样。
注意:
该控件采用数据分步加载的方式,除了顶层固定的几个类别外,其他各层的数据都是点击的。
具体的部门后才去请求服务器加载本部门下的数据的,然后再拼接到原始数据树上。这样可以提高加载速度,提升用户体验。我也试了一次性把所有数据都拉下来,一是太慢,得三五秒,二是数据量太大的话(我这里应该是超过1000,阈值多少没测过),setData()的时候就会报错:
超过最大长度了。。。所以只能分步加载数据。
当然如果你的数据量小,几十人或几百人,也可以选择一次性加载。
这个控件逻辑上还是比较复杂的,要考虑的细节太多……下面梳理一下主要的逻辑点
主要逻辑点
1.%20需要一个数组存储所有被点击的部门在当前列表的索引index,这里用indexList表示
点击某个部门进入下一层目录时,将被点击部门的index索引push进indexList中。点击返回上一层按钮时,删除indexList中最后一个元素。
2.%20要动态的更新当前列表currentList
每进入新的一层,或返回上一层,都需要刷新currentList来实现页面的更新。知道下一层数据很容易,直接取被点击item的children赋值给currentList即可。
但如何还原上一层的数据呢?
第一点记录的indexList就发挥作用了,原始数据树为originalList,循环遍历indexList,根据索引依次取出每层的currentList直到indexList的最后一个元素,就得到了返回上一层需要显示的数据。
3.%20每一次勾选或取消选中都要更新原始的数据树originalList
页面是根据每个item的checked属性判断是否选中的,所以每次改变勾选状态都要设置被改变的item的checked属性,然后更新originalList。这样即使返回上一层了,再进到当前层级选中状态还会被保留,否则刷新currentList后已选状态将丢失。
4.%20列表中选择状态的改变与底部footer的双联动
我们期望的效果是,选中currentList列表的某一项,底部footer会自动添加被选人的名字。取消选中,底部footer也会自动删除。
也可以通过footer来删除已选人,点击footer中人名,会将此人从已选列表中删除,currentList列表中也会自动取消勾选状态。
嗯,这个功能比较耗性能,每一次都需要大量的计算。考虑到性能和速度因素,本次只做了从footer删除只更新currentList的勾选状态。
什么意思呢?假如有两层,A%20和%20B,B%20是%20A%20的下一层数据,即%20A%20是%20B%20的父节点。在%20A%20中选中了一个部门校长室,点击下一层到%20B,在%20B%20中又选了两个人张三和李四,这时底部footer里显示的应该是三个:校长室、张三、李四。此时点击footer的张三,footer会把张三删除,中间列表中张三会被置为未选中状态,这没问题。但点击footer的校长室,%20在footer中是把校长室删除了,但再返回到上一层时,中间列表中的校长室依然是勾选状态,因为此时没有更新原始数据树originalList。如果觉得这是个bug,%20可以加个更新originalList的操作。这样就要遍历originalList的每个元素判断与本次删除的%20id%20是否相等,然后改变checked值,如果数据量很大,会非常慢。我做了妥协……
关键的逻辑就这四块了,当然还有很多小细节,直接看代码吧,注释写的也比较详细。
三、代码
目录结构:
footer文件夹下是抽离出的footer组件,userSelect是选人控件的主要逻辑。把这几个文件复制过去就可以用了。
把userSelect.js里网络请求的代码替换为你的请求代码,注意数据的字段名是否一致。
userSelect 的代码
userSelect.js
import API from '../../../utils/API.js'import ArrayUtils from '../../../utils/ArrayUtils.js'import EventBus from '../../../components/NotificationCenter/WxNotificationCenter.js'let TEACHER_ID = 'teacher';let TEACHER_DEPARTMENT_ID = 't_department';let TEACHER_SUBJECT_ID = 't_subject';let TEACHER_GRADECLASS_ID = 't_gradeclass';let STUDENT_ID = 'student';let PARENT_ID = 'parent'let TEACHER = { id: TEACHER_ID, name: '教师', parentId: '', checked: false, isPeople: false, children: [ { id: TEACHER_DEPARTMENT_ID, name: '部门', parentId: 'teacher', checked: false, isPeople: false, children: [] }, { id: TEACHER_SUBJECT_ID, name: '学科', parentId: 'teacher', checked: false, isPeople: false, children: [] }, { id: TEACHER_GRADECLASS_ID, name: '年级班级', parentId: 'teacher', checked: false, isPeople: false, children: [] }, ]}let STUDENT = { id: STUDENT_ID, name: '学生', parentId: '', checked: false, isPeople: false, children: []}let PARENT = { id: PARENT_ID, name: '家长', parentId: '', checked: false, isPeople: false, children: []}let ORIGINAL_DATA = [ TEACHER, STUDENT, PARENT]Page({ data: { currentList: [], //当前展示的列表 selectList: [], //已选择的元素列表 originalList: [], //最原始的数据列表 indexList: [], //存储目录层级的数组,用于准确的返回上一层 selectList: [], //已选中的人员列表 }, onLoad: function (options) { wx.setNavigationBarTitle({ title: '选人控件' }) this.init(); }, init(){ //用户的单位id this.unitId = getApp().globalData.userInfo.unitId; //用户类型 this.userType = 0; //上次选中的列表,用于判断是不是取消选中了 this.lastTimeSelect = [] this.setData({ currentList: ORIGINAL_DATA, //当前展示的列表 originalList: ORIGINAL_DATA, //最原始的数据列表 }) }, clickItem(res){ console.log(res) let index = res.currentTarget.id; let item = this.data.currentList[index] console.log("item", item) if (!item.isPeople) { //点击教师,下一层数据是写死的,不用请求接口 if (item.id === TEACHER_ID) { this.userType = 2; this.setData({ currentList: item.children }) } else if (item.id === TEACHER_SUBJECT_ID) { if (item.children.length === 0){ this._getTeacherSubjectData() }else{ //children的长度不为0时,更新 currentList this.setData({ currentList: item.children }) } } else if (item.id === TEACHER_DEPARTMENT_ID) { if (item.children.length === 0) { this._getTeacherDepartmentData() } else { //children的长度不为0时,更新 currentList this.setData({ currentList: item.children }) } } else if (item.id === TEACHER_GRADECLASS_ID) { if (item.children.length === 0) { this._getTeacherGradeClassData() } else { //children的长度不为0时,更新 currentList this.setData({ currentList: item.children }) } } else if (item.id === STUDENT_ID) { this.userType = 1; if (item.children.length === 0) { this._getStudentGradeClassData() } else { //children的长度不为0时,更新 currentList this.setData({ currentList: item.children }) } } else if (item.id === PARENT_ID) { this.userType = 3; if (item.children.length === 0) { this._getParentGradeClassData() } else { //children的长度不为0时,更新 currentList this.setData({ currentList: item.children }) } } else{ //children的长度为0时,请求服务器 if(item.children.length === 0){ this._getUserByGroup(item) }else{ //children的长度不为0时,更新 currentList this.setData({ currentList: item.children }) } } //将当前的索引存入索引目录中。索引多一个表示目录多一级 let indexes = this.data.indexList indexes.push(index) //是目录不是具体的用户 this.setData({ indexList: indexes }) //清空上次选中的元素列表,并设置上一层的选中状态给lastTimeSelect this.setLastTimeSelectList(); } }, //返回按钮 goBack() { let indexList = this.data.indexList if (indexList.length > 0) { //返回时删掉最后一个索引 indexList.pop() if (indexList.length == 0) { //indexList长度为0说明回到了最顶层 this.setData({ currentList: this.data.originalList, indexList: indexList }) } else { //循环将当前索引的对应数组赋值给currentList let list = this.data.originalList for (let i = 0; i < indexList.length; i++) { let index = indexList[i] list = list[index].children } this.setData({ currentList: list, indexList: indexList }) } //清空上次选中的元素列表,并设置上一层的选中状态给lastTimeSelect this.setLastTimeSelectList(); } }, //清空上次选中的元素列表,并设置上一层的选中状态给lastTimeSelect setLastTimeSelectList(){ this.lastTimeSelect = [] this.data.currentList.forEach(item => { if (item.checked) { this.lastTimeSelect.push(item) } }) }, //获取教师部门数据 _getTeacherDepartmentData() { this._commonRequestMethod(2, 'department') }, //请求教师的学科数据 _getTeacherSubjectData(){ this._commonRequestMethod(2, 'subject') }, //请求教师的年级班级 _getTeacherGradeClassData() { this._commonRequestMethod(2, 'gradeclass') }, //请求学生的年级班级 _getStudentGradeClassData() { this._commonRequestMethod(1, 'gradeclass') }, //请求家长的年级班级 _getParentGradeClassData() { this._commonRequestMethod(3, 'gradeclass') }, //根据部门查询人 _getUserByGroup(item){ let params = { userType: this.userType, unitId: this.unitId, groupType: item.type, groupId: item.id } console.log('params', params) getApp().get(API.selectUserByGroup(), params, result => { console.log('result', result) let list = this.transformData(result.data.data, item.id) this.setData({ currentList: list }) this.addList2DataTree() //清空上次选中的元素列表,并设置上一层的选中状态给lastTimeSelect。写在这里防止异步请求时执行顺序问题 this.setLastTimeSelectList(); }) }, //通用的请求部门方法 _commonRequestMethod(userType, groupType){ wx.showLoading({ title: '', }) let params = { userType: userType, unitId: this.unitId, groupType: groupType } console.log('params', params) getApp().get(API.selectUsersByUserGroupsTree(), params, result => { console.log('result', result) wx.hideLoading() let data = result.data.data this.setData({ currentList: data }) this.addList2DataTree(); //清空上次选中的元素列表,并设置上一层的选中状态给lastTimeSelect。写在这里防止异步请求时执行顺序问题 this.setLastTimeSelectList(); }) }, //将请求的数据转化为需要的格式 transformData(list, parentId){ //先将数据转化为固定的格式 let newList = [] for(let i=0; i<list.length; i++){ let item = list[i] newList.push({ id: item.id, name: item.realName, parentId: parentId, checked: false, isPeople: true, userType: item.userType, gender: item.gender, children: [] }) } return newList; }, //将当前列表挂载在原数据树上, 目前支持5层目录,如需更多接着往下写就好 addList2DataTree(){ let currentList = this.data.currentList; let originalList = this.data.originalList; let indexes = this.data.indexList switch (indexes.length){ case 1: originalList[indexes[0]].children = currentList break; case 2: originalList[indexes[0]].children[indexes[1]].children = currentList break; case 3: originalList[indexes[0]].children[indexes[1]].children[indexes[2]].children = currentList break; case 4: originalList[indexes[0]].children[indexes[1]].children[indexes[2]].children[indexes[3]].children = currentList break; case 5: originalList[indexes[0]].children[indexes[1]].children[indexes[2]].children[indexes[3]].children[indexes[4]].children = currentList break; } this.setData({ originalList: originalList }) console.log("originalList", originalList) }, //选框变化回调 checkChange(res){ console.log(res) let values = res.detail.value let selectItems = [] //将值取出拼接成 id,name 格式 values.forEach(value => { let arrs = value.split(",") selectItems.push({id: arrs[0], name: arrs[1]}) }) console.log("selectItems", selectItems) console.log("lastTimeSelect", this.lastTimeSelect) //将本次选择的与上次选择的比对,本次比上次多说明新增了,本次比上次少说明删除了,找出被删除的那条数据,在footer中也删除 if (selectItems.length > this.lastTimeSelect.length){ //将 selectList 与 selectItems 拼接并去重 let newList = this.data.selectList.concat(selectItems) newList = ArrayUtils.checkRepeat(newList) this.setData({ selectList: newList }) }else{ //找出取消勾选的item,从selectList中删除 //比对出取消勾选的是哪个元素 let diffItem = {} this.lastTimeSelect.forEach(item => { let flag = false; selectItems.forEach(item2 => { if(item.id === item2.id){ flag = true } }) if(!flag){ diffItem = item console.log("diff=", item) } }) //找出被删除的元素在 selectList 中的位置 let list = this.data.selectList let delIndex = 0; for(let i=0; i<list.length; i++){ if (list[i].id === diffItem.id){ delIndex = i; break; } } //从list中删除这个元素 list.splice(delIndex, 1) this.setData({ selectList: list }) } console.log("selectList", this.data.selectList) //更新 currentList 选中状态并重新挂载在数据树上,以保存选择状态 this.updateCurrentList(this.data.currentList, this.data.selectList) }, //footer点击删除回调 footerDelete(res){ console.log(res) this.setData({ selectList: res.detail.selectList }) console.log('selectList', this.data.selectList) this.updateCurrentList(this.data.currentList, res.detail.selectList) }, //点击 footer 的确定按钮提交数据 submitData(res){ let selectList = this.data.selectList //通过 WxNotificationCenter 发送选择的结果通知 EventBus.postNotificationName("SelectPeopleDone", selectList) //将选择结果存入 app.js 的 globalData getApp().globalData.selectPeopleList = selectList //返回 wx.navigateBack({ delta: 1 }) console.log("selectdone", selectList) }, //更新 currentList 并将更新后的列表挂载在数据树上 updateCurrentList(currentList, selectList){ let newList = [] currentList.forEach(item => { let flag = false; selectList.forEach(item2 => { if (item.id === item2.id) { flag = true } }) if (flag) { item.checked = true } else { item.checked = false } newList.push(item) }) this.setData({ currentList: newList }) this.addList2DataTree() this.setLastTimeSelectList() }})复制代码
userSelect.wxml
<view class='container'> <view class='btn-wrapper'> <button bindtap='goBack'>返回上一层</button> </view> <view class='people-wrapper'> <scroll-view scroll-y class='scrollview'> <checkbox-group bindchange="checkChange"> <view class='item' wx:for='{{currentList}}' wx:key='{{item.id}}'> <checkbox checked='{{item.checked}}' value='{{item.id + "," + item.name}}'> </checkbox> <view id='{{index}}' class='item-content' bindtap='clickItem'> <image class='img' wx:if='{{!item.isPeople}}' src='../../../assets/file.png'></image> <image class='avatar' wx:if='{{item.isPeople}}' src='../../../assets/avatar.png'></image> <text class='itemtext'>{{item.name}}</text> </view> </view> </checkbox-group> <view class='no-data' wx:if='{{currentList.length===0}}'>暂无数据</view> </scroll-view> </view> <view class='footer'> <footer list='{{selectList}}' binddelete='footerDelete' bindsubmit="submitData"/> </view></view>复制代码userSelect.wxss.container { width: 100%; height: 100%; display: flex; flex-direction: column; padding: 20rpx; overflow-x: hidden; box-sizing: border-box; background-color: #fff;}.btn-wrapper { width: 100%; padding: 0 20rpx; box-sizing: border-box;}.btn { font-size: 24rpx; width: 100%;}.people-wrapper { width: 100%; margin-top: 10rpx; margin-bottom: 100rpx;}.scrollview { width: 100%; display: flex; flex-direction: column;}.item { width: 100%; display: flex; flex-direction: row; align-items: center; padding: 30rpx 0; margin: 0 20rpx; border-bottom: 1rpx solid rgba(7, 17, 27, 0.1);}.item-content { width: 100%; display: flex; flex-direction: row; align-items: center; margin-left: 20rpx;}.itemtext { font-size: 36rpx; color: #333; margin-left: 20rpx; text-align: center;}.img { width: 50rpx; height: 40rpx;}.avatar { width: 50rpx; height: 50rpx;}.footer { position: absolute; left: 0; bottom: 0; width: 100%;}.no-data{ width: 100%; font-size: 32rpx; text-align: center; padding: 40rpx 0;}
userSelect.json
{ "usingComponents": { "footer": "footer/footer" }}
footer 的代码
footer.js
Component({ /** * 组件的属性列表 */ properties: { list: { type: Array } }, /** * 组件的初始数据 */ data: { }, /** * 组件的方法列表 */ methods: { delete(res){ console.log(res) let index = res.currentTarget.id let list = this.data.list list.splice(index,1) this.setData({list: list}) this.triggerEvent("delete", {selectList: list}) }, /** * 点击确定按钮 */ confirm(){ this.triggerEvent("submit", "") } }})复制代码
footer.wxml
<view class='container'> <view class='scroll-wrapper'> <scroll-view scroll-x style='scroll'> <text id='{{index}}' class='text' wx:for='{{list}}' wx:key='{{index}}' bindtap='delete'>{{item.name}}</text> </scroll-view> </view> <text class='btn' bindtap='confirm'>确定</text></view>
footer.wxss
.container { width: 100%; height: 100rpx; display: flex; flex-direction: row; padding: 20rpx; box-sizing: border-box; background-color: #fff; align-items: center; overflow-x: hidden; white-space: nowrap; border-top: 2rpx solid rgba(7, 17, 27, 0.1)}.scroll-wrapper { flex: 1; overflow-x: hidden; white-space: nowrap;}.scroll { width: 100%;}.text { font-size: 32rpx; color: #333; padding: 40rpx 20rpx; margin-right: 10rpx; background-color: #f5f5f5;}.btn { padding: 10rpx 20rpx; background-color: rgb(26, 173, 25); border-radius: 10rpx; font-size: 32rpx; color: #fff;}复制代码
footer.json
{ "component": true, "usingComponents": {}}复制代码
再补一个用到的ArrayUtils的代码
export default{ /** * 给数组去重 */ checkRepeat(list) { let noRepList = [list[0]] for (let i = 0; i < list.length; i++) { let repeat = false for (let j = 0; j < noRepList.length; j++) { if (noRepList[j].id === list[i].id) { repeat = true break } } if (!repeat) { noRepList.push(list[i]) } } return noRepList }, //删除list中id为 delId 的元素 deleteItemById(list, delId){ for (let i = 0; i < list.length; i++) { if (list[i].id == delId) { list.splice(i, 1) return list; } } return list; }}复制代码
由于时间紧张,还没有把这个控件单独从项目中抽出来写个 Demo,有时间了会给 github 地址的。
代码还有很多可以优化的地方,比如有几个方法太长了,不符合单一职责原则等等,不想改了,以后再优化吧。。
水平有限,各位大侠请轻喷~
有问题或发现Bug请在评论区留言,毕竟刚写完就分享出来了,还没经过严格的测试。不过应该没什么大的问题。。。有些细节可能没注意到。
版权声明:小程序选择控制——模仿企业微信实现多级不规则嵌套是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。